Compare commits

...

65 Commits
v1.0 ... master

Author SHA1 Message Date
8442c89944 feat(RES-33): Generation de l'architecture compatible Swift 6 (#13)
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #13
2025-04-30 10:22:34 +02:00
57cedd37bb Update rsvgConvertNotFound error to update brew install command line
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2025-04-08 15:27:10 +02:00
09556ba6e0 [DEVTOOLS-186] Exporter les images de resgen en svg
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #12
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-07-17 15:18:13 +02:00
dea57dc1e2 Maj du readme
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-07-11 10:29:32 +02:00
07575bd2bf DEVTOOLS-195 Ne pas générer les svg en template par défaut
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-07-11 10:08:47 +02:00
8686ae974c Suppression des anciens assets si svg
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-06-21 14:54:20 +02:00
be4c561ea8 DEVTOOLS-192 Resgen iOS vector
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-06-21 09:18:51 +02:00
2357a40fff DEVTOOLS-186 Exporter les images de resgen en svg
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-04-22 12:05:24 +02:00
d4afa9c9e9 Maj du Readme suite aux devs sur le fichier string catalog
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-22 09:48:21 +02:00
76ef0a2d59 Merge pull request 'DEVTOOLS-185 Remplacer le json en dur des images resgen' (#11) from DEVTOOLS-186/SVG_resgen into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #11
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-04-19 17:02:26 +02:00
129eb135f1 DEVTOOLS-185 Remplacer le json en dur des images resgen
Some checks are pending
gitea-openium/resgen.swift/pipeline/pr-master Build started...
2024-04-19 17:02:00 +02:00
4ad15fcded Merge pull request 'xcstrings' (#10) from xcstrings into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #10
Reviewed-by: Thibaut Schmitt <t.schmitt@openium.fr>
2024-04-19 16:59:03 +02:00
fb2ddb2227 DEVTOOLS-181 Gérer le tag noTranslation pour les xcstrings 2024-04-17 09:44:09 +02:00
27f86f5c4d Correction de l'option pour le fichier xcstrings 2024-04-15 16:51:18 +02:00
209ba49e3f Gestion des commentaires 2024-04-12 17:15:15 +02:00
ba07005b13 Fix equatable properties for arrays 2024-04-12 16:45:09 +02:00
6c3f3a8982 Correction du test testGenerateXcStringsRootObject 2024-04-12 16:25:10 +02:00
0d651b810f Première implémentation des xcstrings 2024-04-12 16:09:54 +02:00
1d7fc76340 Changed the condition for moe visibility 2024-04-11 15:49:07 +02:00
5d4e461933 Affichage du commentaire même si nil ou empty 2024-04-11 14:20:54 +02:00
55264d61ad Modification de l'affichage des commentaires
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2024-04-11 11:18:57 +02:00
d21ad9d1ea Update JenkinsFile Xcode version
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-11 10:19:10 +02:00
0bd6c3c2d4 Correction des commentaires des strings
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2024-04-11 10:16:28 +02:00
eed20367b9 fix: Edit for NSObject in Firebase
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
PixeeBox/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 11:24:21 +01:00
43b5111d79 Actualiser Sources/ResgenSwift/Analytics/Generator/AnalyticsGenerator.swift
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:39:12 +01:00
2983093a9c Add Swiftlint 2023-12-13 10:39:12 +01:00
b4bbaa3bfd Fix Image 2023-12-13 10:39:12 +01:00
498c8fa4ae Fix Font 2023-12-13 10:39:12 +01:00
2957da6233 Fix Color 2023-12-13 10:39:12 +01:00
d79af06c38 docs: Add Analytics section in README
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-13 10:27:35 +01:00
d8937f2de6 docs: Add Analytics section in README 2023-12-13 10:23:31 +01:00
9b27f24197 docs: Add Analytics section in README 2023-12-13 10:20:53 +01:00
1d58fd5510 Edit sample file
Some checks failed
gitea-openium/resgen.swift/pipeline/pr-master There was a failure building this commit
2023-12-12 16:58:25 +01:00
f6c49bf626 Add parse error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 11:19:19 +01:00
f1b62d83c4 Add missing print error
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:29:19 +01:00
ee5055efa5 Retours sur la structure du code
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:24:42 +01:00
6f8e3b6664 Add error handling
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-11 10:09:24 +01:00
1f2933950b Add \n at the end of R.swift
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 15:16:50 +01:00
3b90387e10 fix: Import + empty parameters
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 15:03:28 +01:00
1ee4998ec6 test: Edit test
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 14:22:17 +01:00
ca763cd5d0 fix: Rebase tags
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-08 11:30:05 +01:00
3fc2fd9bac fix: Tags -> Anlytics 2023-12-08 11:30:05 +01:00
09c153ba65 Actualiser Jenkinsfile
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-06 14:51:01 +01:00
2a144fc00e Test Tags generation
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-06 14:48:47 +01:00
6aef8bc2de Add FirebaseManager 2023-12-06 11:23:26 +01:00
3e133773a9 Add Manager + MatomoManager
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-06 10:34:27 +01:00
5fd680110c Fix forgotten code
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-05 17:09:24 +01:00
ce274219fc Add new tag generation
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-12-05 16:56:44 +01:00
fa5bf192e8 Delete lang option and add target option 2023-12-05 16:55:45 +01:00
1a45ec7b0d New version of tags.txt 2023-12-05 16:54:44 +01:00
7d6bb4fcb9 Merge pull request 'v1.2.2' (#5) from v1.2.2 into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #5
2023-06-07 09:53:38 +02:00
844a8aec45 Add availability to get keyPath from generated strings Key enumeration
Some checks are pending
gitea-openium/resgen.swift/pipeline/head Build started...
2023-06-07 09:52:56 +02:00
beb28e652d Add availability to get keyPath from generated strings Key enumeration 2023-06-07 09:52:36 +02:00
78be15d57d Merge pull request 'v1.2.1' (#4) from v1.2.1 into master
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Reviewed-on: #4
2023-06-05 09:33:14 +02:00
d6c4702390 Make R2String string Key enum unique by adding extnesion suffix (+ make extension suffix required)
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-05-12 11:59:46 +02:00
1e073af5df Fix architecture generation and add enum key of each string in R2String
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2023-05-12 11:33:36 +02:00
188178fe6a Update Changelog with v1.2
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2022-11-24 09:13:30 +01:00
c31d0b1618 Fix typo in CHANGELOG 2022-11-22 17:55:32 +01:00
b662fc64f3 Update Readme
Populate CHANGELOG.md based on previous versions
2022-11-22 17:54:20 +01:00
9ab7e74991 Renaming errors property: localizedDescription -> description
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Re enabling generation of ressources on commond
Architecture will now generate property of subobject as non-static. It will allow usage like R.sub_object.sub_property.property
2022-11-22 17:37:24 +01:00
fc427733ee Fix error/warning message to be shown in Xcode.IssueNavigator
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2022-11-22 17:14:13 +01:00
5a3d273acc Passage version en 1.2
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
Ajout de la définition d'une architecture de classe pour éviter de définir manuellement les classes R/R2Image/R2Fonts.... (optionel)
2022-11-18 17:20:21 +01:00
a7a850799d Update Readme to add install/uninstall steps 2022-11-07 11:15:38 +01:00
7d3652f1f9 Update version to 1.1
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
2022-11-07 10:47:26 +01:00
41733d2680 Squashed commit of the following:
Some checks failed
gitea-openium/resgen.swift/pipeline/head There was a failure building this commit
commit aa59ef28ea56315eb421ba044ddaf0c4066bfff8
Author: Thibaut Schmitt <t.schmitt@openium.fr>
Date:   Mon Nov 7 10:26:28 2022 +0100

    Add trailing carrier at the end of generated extension files

commit e985950aa1e39d81d4938e15f8724c0f7723b003
Author: Thibaut Schmitt <t.schmitt@openium.fr>
Date:   Fri Nov 4 16:37:34 2022 +0100

    Replace installation script by script that install completion file
    Setup a Makefile to install resgen

commit d574384c151259610a4c2f837b14068bb7716e44
Author: Thibaut Schmitt <t.schmitt@openium.fr>
Date:   Fri Nov 4 14:33:39 2022 +0100

    Refactor
    Improve testability
    Add tests on SwiftUI generated code
    Add tests on  command

commit d9e76632c3037da0ed9e1dd37056685416579da9
Author: Thibaut Schmitt <t.schmitt@openium.fr>
Date:   Thu Nov 3 15:43:47 2022 +0100

    Fixing bad merge on FontOptions

commit 76b5ebfcd1cde7a7d4c83f516a4fc937841e7715
Author: Thibaut Schmitt <t.schmitt@openium.fr>
Date:   Thu Nov 3 15:37:28 2022 +0100

    Squashed commit of the following:

    commit 085f1ffc3347d3c48af91ffb00a1a9b381ce47d1
    Author: Thibaut Schmitt <t.schmitt@openium.fr>
    Date:   Thu Nov 3 15:00:27 2022 +0100

        Refactor SwiftUI extension generation and generation SwiftUI extension for images

    commit 4f7d7e18b138343a07cbb0bb47213818678ced6b
    Author: Thibaut Schmitt <t.schmitt@openium.fr>
    Date:   Mon Oct 31 16:21:32 2022 +0100

        Génération de composant SwiftUI: Color et Image

    commit 0797667b2510f6fd45b9845f2d29c0c1e31da877
    Author: Thibaut Schmitt <t.schmitt@openium.fr>
    Date:   Mon Oct 31 16:21:12 2022 +0100

        Génération de composant SwiftUI: Color et Image

commit 417a2630925841dd486ae1d684d28ab7dca240e0
Author: Thibaut Schmitt <t.schmitt@openium.fr>
Date:   Wed Oct 19 17:13:03 2022 +0200

    Update Info.plist UIAppFonts key when generating fonts (if plist path if defined)
2022-11-07 10:32:17 +01:00
127 changed files with 6917 additions and 1333 deletions

View File

@ -102,6 +102,20 @@
ReferencedContainer = "container:">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "generate"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;$(PROJECT_DIR)/../SampleFiles/resgenConfiguration.yml&quot;"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--project-directory &quot;$(PROJECT_DIR)&quot;"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@ -0,0 +1,34 @@
# v1.2 - Architecture generation
## New
- New section in configuration file: `architecture`. Define your ressources accessors achitecture and let ResgenSwift generate it for you. Check out Readme to know more.
## Fixes
- Errors and Warnings are now shown in Xcode Issue Navigator
---
# v1.1 - SwiftUI compatibility
## New
- Update plist `UIAppFonts` when generated fonts (use plistBuddy)
- New parameter: `infoPlistPaths`
- Generate SwiftUI extensions for colors, fonts and images
- New parameter: `extensionNameUIKit`
- Adding Makefile to install, unsintall and create man page.
## Fixes
Fix SwiftLint rule `trailing_newline`
---
# v1.0 - Configuration file
## Major
- A new command has been added: `generate`. Instead of runnning every single command, it will run all necessary command based on a `yaml` configuration file. Check out Readme to know more.
## Minors
- Code refactoring
- Huge performance improvements
- Readme.md update
- Add option to generate static properties/methods (`staticMembers`)
- Add option to specify the project directory (`--project-directory`). It allows to run ResgenSwift from anywhere
- Add `install.sh` script to install ResgenSwift in `/usr/local/bin`

2
Jenkinsfile vendored
View File

@ -1,6 +1,6 @@
library "openiumpipeline"
env.DEVELOPER_DIR="/Applications/Xcode_13.3.0.app/Contents/Developer"
env.DEVELOPER_DIR="/Applications/Xcode-15.4.0.app/Contents/Developer"
//env.SIMULATOR_DEVICE_TYPES="iPad--7th-generation-"
env.IS_PACKAGE_SWIFT=1
env.TARGETS_MACOS=1

63
Makefile Normal file
View File

@ -0,0 +1,63 @@
SHELL = /bin/zsh
#INSTALL_DIR ?= /usr/local/bin
INSTALL_DIR = /tmp/ResgenYolo
MAN_DIR := /usr/local/share/man
MAN_PAGE_NAME = resgen-swift.1
REPO_DIR = $(shell pwd)
BUILD_DIR = $(REPO_DIR)/.build
#
# Man pages
#
# create-man-files:
# swift package plugin generate-manual
# cp $(BUILDDIR)/plugins/GenerateManualPlugin/outputs/ResgenSwift/resgen-swift.1 $(REPODIR)/man/resgen-swift.1
# install-man-files:
# mkdir -p ${DESTDIR}${mandir}/man1
# cp $(REPODIR)/man/resgen-swift.1 ${DESTDIR}${mandir}/man1/resgen-swift.1
create-and-install-man-files:
swift package plugin generate-manual
mkdir -p ${MAN_DIR}/man1
cp $(BUILD_DIR)/plugins/GenerateManualPlugin/outputs/ResgenSwift/${MAN_PAGE_NAME} ${MAN_DIR}/man1/${MAN_PAGE_NAME}
#
# Build and install
#
build-debug:
@swift build \
-c debug \
--build-path "$(BUILD_DIR)"
build-release:
@swift build \
-c release \
--build-path "$(BUILD_DIR)"
.PHONY: install
install: build-release
@install -d "$(INSTALL_DIR)"
@install "$(wildcard $(BUILD_DIR)/**/release/ResgenSwift)" "$(INSTALL_DIR)/resgen-swift"
@make create-and-install-man-files
#
# Uninstall and cleaning
#
.PHONY: uninstall
uninstall:
@rm -rf "$(INSTALL_DIR)/resgen-swift"
@rm -rf ${MAN_DIR}/man1/${MAN_PAGE_NAME}
.PHONY: clean
distclean:
@rm -f $(BUILD_DIR)/release
.PHONY: clean
clean: distclean
@rm -rf $(BUILD_DIR)

View File

@ -1,25 +1,86 @@
{
"object": {
"pins": [
{
"package": "swift-argument-parser",
"repositoryURL": "https://github.com/apple/swift-argument-parser",
"state": {
"branch": null,
"revision": "e1465042f195f374b94f915ba8ca49de24300a0d",
"version": "1.0.2"
}
},
{
"package": "Yams",
"repositoryURL": "https://github.com/jpsim/Yams.git",
"state": {
"branch": null,
"revision": "01835dc202670b5bb90d07f3eae41867e9ed29f6",
"version": "5.0.1"
}
"pins" : [
{
"identity" : "collectionconcurrencykit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git",
"state" : {
"revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95",
"version" : "0.2.0"
}
]
},
"version": 1
},
{
"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",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.3"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"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"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3",
"version" : "5.0.6"
}
}
],
"version" : 2
}

View File

@ -9,7 +9,8 @@ let package = Package(
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/jpsim/Yams.git", from: "5.0.1"),
.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMajor(from: "0.54.0")),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@ -20,7 +21,8 @@ let package = Package(
"ToolCore",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"Yams"
]
],
plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]
),
// Helper targets

190
README.md
View File

@ -6,8 +6,9 @@ ResgenSwift is a package, fully written in Swift, to help you automatize ressour
## Fonts
Font generator generates an extension of `UIFont` (or a custom class). It also prints `UIAppFonts` to put in your project `.plist`.
iOS required to use the **real name** of the font, this name can be different from its filename. To get the **real name**, it uses `fc-scan`.
Font generator generates an extension of `UIFont` and `Font` (or custom classes). It also prints content of `UIAppFonts` from your project `.plist`. If project `.plist` is specified, it will update `UIAppFonts` content of all `.plist`.
iOS required to use the **real name** of the font, this name can be different from its filename. To get the **real name**, it uses `fc-scan`. So, be sure that the `$PATH` contains path of `fc-scan`.
**Example**
@ -15,8 +16,10 @@ iOS required to use the **real name** of the font, this name can be different fr
swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/fonts.txt" \
--extension-output-path "./Fonts/Generated" \
--extension-name "AppFont" \
--extension-name-ui-kit "UIAppFont" \
--extension-suffix "GreatApp" \
--static-members true
--static-members true \
--info-plist-paths "./path/one/to/Info.plist ./path/two/to/Info.plist"
```
**Parameters**
@ -24,9 +27,11 @@ swift run -c release ResgenSwift fonts $FORCE_FLAG "./Fonts/fonts.txt" \
1. `-f`: force generation
2. Font input folder, it will search for every `.ttf` and `.otf` files specified in `fonts.txt`
3. `--extension-output-path`: path where to generate generated extension
4. `--extension-name` *(optional)* : name of thee class to add the extension
5. `--extension-suffix` *(optional)* : additional text which is added to the filename (ex: `AppFont+GreatApp.swift`)
6. `--static-members` *(optional)*: generate static properties or not
4. `--extension-name` *(optional)* : name of the class to add SwiftUI getters
5. `--extension-name-ui-kit` *(optional)* : name of the class to add UIKit getters
6. `--extension-suffix` *(optional)* : additional text which is added to the filename (ex: `AppFont+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not
8. `--info-plist-paths` *(optional)*: array of `.plist`, you can specify multiple `Info.plist` for multiple targets
## Colors
@ -39,6 +44,7 @@ swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/colors.txt" \
--xcassets-path "./Colors/colors.xcassets" \
--extension-output-path "./Colors/Generated/" \
--extension-name "AppColor" \
--extension-name-ui-kit "UIAppColor" \
--extension-suffix "GreatApp" \
--static-members true
```
@ -49,9 +55,10 @@ swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/colors.txt" \
2. Input colors file
3. `--style` can be `all` or `light`
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppColor+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not
5. `--extension-name` *(optional)* : name of the class to add SwiftUI getters
6. `--extension-name-ui-kit` *(optional)* : name of the class to add UIKit getters
7. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppColor+GreatApp.swift`)
8. `--static-members` *(optional)*: generate static properties or not
## Strings
@ -74,7 +81,7 @@ swift run -c release ResgenSwift strings twine $FORCE_FLAG "./Twine/strings.txt"
2. Input translations file (must be Twine formatted)
3. `--langs`: langs to generate (string with space between each lang)
4. `--default-lang`: default lang that will be in `Base.lproj`. It must be in `langs` as well
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-output-path`: path where to generate generated extension
### Stringium (recommended)
@ -86,6 +93,7 @@ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/string
--extension-output-path "./Strings/Generated" \
--extension-name "AppString" \
--extension-suffix "GreatApp" \
--xcStrings true
--static-members true
```
@ -98,6 +106,7 @@ swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/string
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppString+GreatApp.swift`)
6. `--xcStrings`*(optional)* : generate string catalog
7. `--static-members` *(optional)*: generate static properties or not
@ -126,15 +135,81 @@ swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
> ⚠️ If extension name is not set or is `Tags`, it will generate the following typaloas `typealias Tags = String`.
## Analytics
Analytics will generate all you need to analyze UX with Matomo or Firebase Analytics. Input files are formatted in YAML. This command will generate a manager for each target and an AnalyticsManager. This is this one you will need to use. And it will generate a method for all tags you have declared in the YAML file. Next, you will need to use the `configure()` method of AnalyticsManager and if you want to use matomo to set up the `siteId` and the `url` of the site.
```sh
swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/tags.txt" \
--target "matomo firebase" \
--extension-output-path "./Analytics/Generated" \
--extension-name "AppAnalytics" \
--extension-suffix "GreatApp" \
--static-members true
```
**Parameters**
1. `-f`: force generation
2. Input tags file (must be YAML formatted)
3. `--target`: target with you will log UX
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppAnalytics+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ If extension name is not set or is `Analytics`, it will generate the following typaloas `typealias Analytics = String`.
### YAML
```
- id: s1_def_one
name: s1 def one _TITLE_
path: s1_def_one/_TITLE_
action: Tap
category: User
tags: ios,droid
comments:
parameters:
- name: title
type: String
replaceIn: name,path
```
1. `id`: name of the method (method name will be composed of `log` + `Event|Screen` + `id`)
2. `name`: name of the tag
3. `path` *(optional with firebase)* : needed for matomo but not with firebase (log screen)
4. `action` *(optional with firebase)* : needed for matomo but not with firebase (log event)
5. `category` *(optional with firebase)* : needed for matomo but not with firebase (log event)
6. `tags`: which platform target
7. `comments` *(optional)*
8. `parameters` *(optional)*
**Parameters**
You can use parameters in generate methods.
1. `name`: name of the parameter
2. `type`: type of the parameter (Int, String, Bool, Double)
3. `replaceIn` *(optional)*
**Replace in**
This is section is equivalent of `%s | %d | %f | %@`. You can put the content of the parameter in *name*, *path*, *action*, *category*.
You need to put `_` + `NAME OF THE PARAMETER` + `_` in the target and which target you want in the value of `replaceIn`. (name need to be in uppercase)
## Images
Images generator will generate images assets along with extensions to access those images easily.
Images generator will generate images assets along with extensions to access those images easily.
```sh
swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \
--xcassets-path "./Images/app.xcassets" \
--extension-output-path "./Images/Generated" \
--extension-name "AppImage" \
--extension-name-ui-kit "UIAppImage" \
--extension-suffix "GreatApp" \
--static-members true
```
@ -145,10 +220,12 @@ swift run -c release ResgenSwift images $FORCE_FLAG "./Images/images.txt" \
2. Input images definitions file
3. `--xcassets-path`: xcasset path where to generate imagesets
4. `--extension-output-path`: path where to generate generated extension
5. `--extension-name` *(optional)* : name of class to add the extension
5. `--extension-name` *(optional)* : name of the class to add SwiftUI getters
6. `--extension-name-ui-kit` *(optional)* : name of the class to add UIKit getters
6. `--extension-suffix` *(optional)* : additional text which is added to filename (ex: `AppImage+GreatApp.swift`)
7. `--static-members` *(optional)*: generate static properties or not
> ⚠️ Svg images will be copied in the assets and rendered as "Original", however if those images are not rendered correctly you can force the png generation by adding the key word "png" like this: id arrow_back 15 ? png
## All at once
@ -167,6 +244,7 @@ colors:
xcassetsPath: String
extensionOutputPath: String
extensionName: String?
extensionNameUIKit: String?
extensionSuffix: String?
staticMembers: Bool?
@ -175,15 +253,18 @@ fonts:
inputFile: String
extensionOutputPath: String
extensionName: String?
extensionNameUIKit: String?
extensionSuffix: String?
staticMembers: Bool?
infoPlistPaths: [String]
images:
-
inputFile: String
xcassetsPath: String
extensionOutputPath: String
extensionName: String?
extensionNameUIKit: String?
extensionSuffix: String?
staticMembers: Bool?
@ -223,6 +304,7 @@ colors:
xcassetsPath: String
extensionOutputPath: String
extensionName: String?
extensionNameUIKit: String?
extensionSuffix: String?
staticMembers: Bool?
-
@ -231,6 +313,7 @@ colors:
xcassetsPath: String
extensionOutputPath: String
extensionName: String?
extensionNameUIKit: String?
extensionSuffix: String?
staticMembers: Bool?
...
@ -246,6 +329,67 @@ tags: []
...
```
### File architecture
ResgenSwift generate extension of classes. Those classes must exists in your project. You can create them yourself OR you can let ResgenSwift create them by specifying what you want. Do as follow:
```
architecture:
  property: R *(required but not used)*
  classname: R
  path: ./path/to/generate
  children:
    - property: images
      classname: R2Image
    - property: strings
      classname: R2String
    - property: fonts
      classname: R2Font
    - property: images
      classname: R2Image
    - property: uikit
      classname: R2UI
      children:
        - property: images
          classname: R2UIImage
        - property: fonts
          classname: R2UIFont
        - property: images
          classname: R2UIImage
```
This will generate a file named as the architecture classname: `R.swift`. Based on the previous architecture, it will generate:
```
class R {
static let images = R2Image()
static let strings = R2String()
static let fonts = R2Font()
static let images = R2Image()
static let uikit = R2UI()
}
class R2Image {}
class R2String {}
class R2Font {}
class R2Image {}
class R2UI {
let images = R2UIImage()
let fonts = R2UIFont()
let images = R2UIImage()
}
class R2UIImage {}
class R2UIFont {}
class R2UIImage {}
```
### Usage
```sh
@ -255,12 +399,22 @@ swift run -c release ResgenSwift generate path/to/configuration.yml --project-di
> ⚠️ Every path in `configuration.yml` will be prepended by content of `--project-directory` if they are relative path (not starting with `/`)
## Installation
## Binary usage
Simple run `./install.sh`. Binary will be install in `/usr/local/bin`. Then, use ResgenSwift like any other command:
### Installation
Example:
Run `make install`. Binary will be install in `/usr/local/bin`.
Usage:
```sh
ResgenSwift generate path/to/configuration.yml --project-directory ${PROJECT_DIR}
```
resgen-swift generate path/to/configuration.yml --project-directory ${PROJECT_DIR}
```
### Man page
Commands parameters and details can be find by running `resgen-swift --help`. If you prefer, a man page is also available. Run `man resgen-swift`. Man page is installed on `make install` but you can install manually by running `make create-and-install-man-file`.
### Uninstallation
To uninstall ResgenSwift: `make uninstall`.

View File

@ -0,0 +1,21 @@
// Generated by ResgenSwift.Color 1.2
import SwiftUI
extension ColorYolo {
/// Color red is #FF0000 (light) or #FF0000 (dark)"
var red: Color {
Color("red")
}
/// Color green_alpha_50 is #A000FF00 (light) or #A000FF00 (dark)"
var green_alpha_50: Color {
Color("green_alpha_50")
}
/// Color blue_light_dark is #0000FF (light) or #0000AA (dark)"
var blue_light_dark: Color {
Color("blue_light_dark")
}
}

View File

@ -1,21 +0,0 @@
// Generated from ColorToolCore at 2021-12-20 15:16:18 +0000
import UIKit
extension UIColor {
/// Color red is #FF0000 (light) or #FF0000 (dark)"
static var red: UIColor {
UIColor(named: "red")!
}
/// Color green_alpha_50 is #A000FF00 (light) or #A000FF00 (dark)"
static var green_alpha_50: UIColor {
UIColor(named: "green_alpha_50")!
}
/// Color blue_light_dark is #0000FF (light) or #0000AA (dark)"
static var blue_light_dark: UIColor {
UIColor(named: "blue_light_dark")!
}
}

View File

@ -1,21 +0,0 @@
// Generated from ColorToolCore at 2022-02-14 09:30:19 +0000
import UIKit
extension UIColor {
/// Color red is #FF0000 (light) or #FF0000 (dark)"
static var red: UIColor {
UIColor(named: "red")!
}
/// Color green_alpha_50 is #A000FF00 (light) or #A000FF00 (dark)"
static var green_alpha_50: UIColor {
UIColor(named: "green_alpha_50")!
}
/// Color blue_light_dark is #0000FF (light) or #0000AA (dark)"
static var blue_light_dark: UIColor {
UIColor(named: "blue_light_dark")!
}
}

View File

@ -1,21 +0,0 @@
// Generated from ColorToolCore at 2021-12-20 15:17:10 +0000
import UIKit
extension UIColor {
/// Color red is #FF0000 (light) or #FF0000 (dark)"
static var red: UIColor {
UIColor(named: "red")!
}
/// Color green_alpha_50 is #A000FF00 (light) or #A000FF00 (dark)"
static var green_alpha_50: UIColor {
UIColor(named: "green_alpha_50")!
}
/// Color blue_light_dark is #0000FF (light) or #0000AA (dark)"
static var blue_light_dark: UIColor {
UIColor(named: "blue_light_dark")!
}
}

View File

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

View File

@ -1,8 +1,8 @@
// Generated from ColorToolCore at 2021-12-20 15:34:55 +0000
// Generated by ResgenSwift.Color 1.2
import UIKit
extension R2Color {
extension UIColorYolo {
/// Color red is #FF0000 (light) or #FF0000 (dark)"
@objc var red: UIColor {
@ -14,8 +14,8 @@ extension R2Color {
UIColor(named: "green_alpha_50")!
}
/// Color blue_light_dark is #0000FF (light) or #0000FF (dark)"
/// Color blue_light_dark is #0000FF (light) or #0000AA (dark)"
@objc var blue_light_dark: UIColor {
UIColor(named: "blue_light_dark")!
}
}
}

View File

@ -0,0 +1,61 @@
// Generated by ResgenSwift.Fonts 1.2
import SwiftUI
extension FontYolo {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
func LatoItalic(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoItalic.rawValue, size: size)
}
func LatoLightItalic(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoLightItalic.rawValue, size: size)
}
func LatoHairline(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoHairline.rawValue, size: size)
}
func LatoBold(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoBold.rawValue, size: size)
}
func LatoBlack(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoBlack.rawValue, size: size)
}
func LatoRegular(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoRegular.rawValue, size: size)
}
func LatoBlackItalic(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoBlackItalic.rawValue, size: size)
}
func LatoBoldItalic(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoBoldItalic.rawValue, size: size)
}
func LatoLight(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoLight.rawValue, size: size)
}
func LatoHairlineItalic(withSize size: CGFloat) -> Font {
Font.custom(FontName.LatoHairlineItalic.rawValue, size: size)
}
}

View File

@ -1,62 +0,0 @@
// Generated from FontToolCore
import UIKit
extension R2Font {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
func LatoItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
func LatoLightItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
func LatoHairline(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
func LatoBold(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
func LatoBlack(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
func LatoRegular(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
func LatoBlackItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
func LatoBoldItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
func LatoLight(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
func LatoHairlineItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -1,62 +0,0 @@
// Generated from FontToolCore
import UIKit
extension R2Font {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
func LatoItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
func LatoLightItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
func LatoHairline(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
func LatoBold(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
func LatoBlack(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
func LatoRegular(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
func LatoBlackItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
func LatoBoldItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
func LatoLight(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
func LatoHairlineItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -1,63 +0,0 @@
// Lato/Lato-Italic.ttf Lato/Lato-LightItalic.ttf Lato/Lato-Thin.ttf Lato/Lato-Bold.ttf Lato/Lato-Black.ttf Lato/Lato-Regular.ttf Lato/Lato-BlackItalic.ttf Lato/Lato-BoldItalic.ttf Lato/Lato-Light.ttf Lato/Lato-ThinItalic.ttf
// Generated from FontToolCore
import UIKit
extension UIFont {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
static let LatoItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
static let LatoLightItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
static let LatoHairline: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
static let LatoBold: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
static let LatoBlack: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
static let LatoRegular: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
static let LatoBlackItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
static let LatoBoldItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
static let LatoLight: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
static let LatoHairlineItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -1,62 +0,0 @@
// Generated from FontToolCore
import UIKit
extension UIFont {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
static let LatoItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
static let LatoLightItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
static let LatoHairline: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
static let LatoBold: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
static let LatoBlack: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
static let LatoRegular: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
static let LatoBlackItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
static let LatoBoldItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
static let LatoLight: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
static let LatoHairlineItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -1,61 +0,0 @@
// Generated by ResgenSwift.FontTool 1.0.0
import UIKit
extension UIFont {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
static let LatoItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
static let LatoLightItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
static let LatoHairline: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
static let LatoBold: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
static let LatoBlack: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
static let LatoRegular: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
static let LatoBlackItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
static let LatoBoldItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
static let LatoLight: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
static let LatoHairlineItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -0,0 +1,61 @@
// Generated by ResgenSwift.Fonts 1.2
import UIKit
extension UIFontYolo {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
func LatoItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
func LatoLightItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
func LatoHairline(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
func LatoBold(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
func LatoBlack(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
func LatoRegular(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
func LatoBlackItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
func LatoBoldItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
func LatoLight(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
func LatoHairlineItalic(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -1,62 +0,0 @@
// Generated from FontToolCore
import UIKit
extension UIFont {
enum FontName: String {
case LatoItalic = "Lato-Italic"
case LatoLightItalic = "Lato-LightItalic"
case LatoHairline = "Lato-Hairline"
case LatoBold = "Lato-Bold"
case LatoBlack = "Lato-Black"
case LatoRegular = "Lato-Regular"
case LatoBlackItalic = "Lato-BlackItalic"
case LatoBoldItalic = "Lato-BoldItalic"
case LatoLight = "Lato-Light"
case LatoHairlineItalic = "Lato-HairlineItalic"
}
// MARK: - Getter
static let LatoItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoItalic.rawValue, size: size)!
}
static let LatoLightItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLightItalic.rawValue, size: size)!
}
static let LatoHairline: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairline.rawValue, size: size)!
}
static let LatoBold: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBold.rawValue, size: size)!
}
static let LatoBlack: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlack.rawValue, size: size)!
}
static let LatoRegular: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoRegular.rawValue, size: size)!
}
static let LatoBlackItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBlackItalic.rawValue, size: size)!
}
static let LatoBoldItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoBoldItalic.rawValue, size: size)!
}
static let LatoLight: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoLight.rawValue, size: size)!
}
static let LatoHairlineItalic: ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.LatoHairlineItalic.rawValue, size: size)!
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NewArr</key>
<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>
</array>
</dict>
</plist>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NewArr</key>
<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>
</array>
</dict>
</plist>

View File

@ -0,0 +1,31 @@
// Generated by ResgenSwift.Images 1.2
// Images from sampleImages
import SwiftUI
extension ImageYolo {
var article_notification_pull_detail: Image {
Image("article_notification_pull_detail")
}
var article_notification_pull: Image {
Image("article_notification_pull")
}
var new_article: Image {
Image("new_article")
}
var welcome_background: Image {
Image("welcome_background")
}
var article_trash: Image {
Image("article_trash")
}
var ic_close_article: Image {
Image("ic_close_article")
}
}

View File

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

View File

@ -1,32 +0,0 @@
// Generated from Imagium at 2022-02-14 09:30:23 +0000
// Images from sampleImages
import UIKit
extension UIImage {
static var article_notification_pull_detail: UIImage {
UIImage(named: "article_notification_pull_detail")!
}
static var article_notification_pull: UIImage {
UIImage(named: "article_notification_pull")!
}
static var new_article: UIImage {
UIImage(named: "new_article")!
}
static var welcome_background: UIImage {
UIImage(named: "welcome_background")!
}
static var article_trash: UIImage {
UIImage(named: "article_trash")!
}
static var ic_close_article: UIImage {
UIImage(named: "ic_close_article")!
}
}

View File

@ -1,37 +0,0 @@
// Generated from StringToolCore at 2022-01-10 08:27:11 +0000
import UIKit
fileprivate let kStringsFileName = "sampleStrings"
extension MyString {
// MARK: - Webservice
/// Translation in en :
/// en
var param_lang: String {
NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
}
// MARK: - Generic
/// Translation in en :
/// Back
var generic_back: String {
NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
}
/// Translation in en :
/// Loading data...
var generic_loading_data: String {
NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
}
/// Translation in en :
/// Welcome %@ !
var generic_welcome_firstname_format: String {
NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome %@ !", comment: "")
}
}

View File

@ -1,4 +1,4 @@
// Generated by ResgenSwift.Strings.Stringium 1.0.0
// Generated by ResgenSwift.Strings.Stringium 1.2
import UIKit
@ -6,60 +6,79 @@ fileprivate let kStringsFileName = "sampleStrings"
extension String {
// MARK: - Webservice
enum KeyGenAllScript: String {
case param_lang = "param_lang"
case generic_back = "generic_back"
case generic_loading_data = "generic_loading_data"
case generic_welcome_firstname_format = "generic_welcome_firstname_format"
case test_equal_symbol = "test_equal_symbol"
case placeholders_test_one = "placeholders_test_one"
var keyPath: KeyPath<String, String> {
switch self {
case .param_lang: return \String.param_lang
case .generic_back: return \String.generic_back
case .generic_loading_data: return \String.generic_loading_data
case .generic_welcome_firstname_format: return \String.generic_welcome_firstname_format
case .test_equal_symbol: return \String.test_equal_symbol
case .placeholders_test_one: return \String.placeholders_test_one
}
}
}
// MARK: - Webservice
/// Translation in en :
/// en
static var param_lang: String {
var param_lang: String {
NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
}
// MARK: - Generic
// MARK: - Generic
/// Translation in en :
/// Back
static var generic_back: String {
var generic_back: String {
NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
}
/// Translation in en :
/// Loading data...
static var generic_loading_data: String {
var generic_loading_data: String {
NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
static var generic_welcome_firstname_format: String {
var generic_welcome_firstname_format: String {
NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
static func generic_welcome_firstname_format(arg0: String) -> String {
String(format: Self.generic_welcome_firstname_format, arg0)
func generic_welcome_firstname_format(arg0: String) -> String {
String(format: self.generic_welcome_firstname_format, arg0)
}
// MARK: - EqualSymbol
// MARK: - EqualSymbol
/// Translation in en :
/// 1 = 1 point !
static var test_equal_symbol: String {
var test_equal_symbol: String {
NSLocalizedString("test_equal_symbol", tableName: kStringsFileName, bundle: Bundle.main, value: "1€ = 1 point !", comment: "")
}
// MARK: - Placeholders
// MARK: - Placeholders
/// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d
static var placeholders_test_one: String {
var placeholders_test_one: String {
NSLocalizedString("placeholders_test_one", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "")
}
/// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d
static func placeholders_test_one(arg0: String, arg1: String, arg2: Int) -> String {
String(format: Self.placeholders_test_one, arg0, arg1, arg2)
func placeholders_test_one(arg0: String, arg1: String, arg2: Int) -> String {
String(format: self.placeholders_test_one, arg0, arg1, arg2)
}
}
}

View File

@ -1,57 +0,0 @@
// Generated from Strings-Stringium at 2022-03-07 11:02:15 +0000
import UIKit
fileprivate let kStringsFileName = "sampleStrings"
extension String {
// MARK: - Webservice
/// Translation in en :
/// en
static var param_lang: String {
NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
}
// MARK: - Generic
/// Translation in en :
/// Back
static var generic_back: String {
NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
}
/// Translation in en :
/// Loading data...
static var generic_loading_data: String {
NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
static var generic_welcome_firstname_format: String {
NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
static func generic_welcome_firstname_format(arg0: String) -> String {
String(format: Self.generic_welcome_firstname_format, arg0)
}
// MARK: - Placeholders
/// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d
static var placeholders_test_one: String {
NSLocalizedString("placeholders_test_one", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "")
}
/// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d
static func placeholders_test_one(arg0: String, arg1: String, arg2: Int) -> String {
String(format: Self.placeholders_test_one, arg0, arg1, arg2)
}
}

View File

@ -1,37 +0,0 @@
// Generated from StringToolCore at 2022-01-10 08:39:52 +0000
import UIKit
fileprivate let kStringsFileName = "sampleStrings"
extension String {
// MARK: - Webservice
/// Translation in en :
/// en
static var param_lang: String {
NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
}
// MARK: - Generic
/// Translation in en :
/// Back
static var generic_back: String {
NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
}
/// Translation in en :
/// Loading data...
static var generic_loading_data: String {
NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
static var generic_welcome_firstname_format: String {
NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
}
}

View File

@ -1,57 +0,0 @@
// Generated from Strings-Stringium at 2022-03-07 11:00:52 +0000
import UIKit
fileprivate let kStringsFileName = "sampleStrings"
extension StringTest {
// MARK: - Webservice
/// Translation in en :
/// en
var param_lang: String {
NSLocalizedString("param_lang", tableName: kStringsFileName, bundle: Bundle.main, value: "en", comment: "")
}
// MARK: - Generic
/// Translation in en :
/// Back
var generic_back: String {
NSLocalizedString("generic_back", tableName: kStringsFileName, bundle: Bundle.main, value: "Back", comment: "")
}
/// Translation in en :
/// Loading data...
var generic_loading_data: String {
NSLocalizedString("generic_loading_data", tableName: kStringsFileName, bundle: Bundle.main, value: "Loading data...", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
var generic_welcome_firstname_format: String {
NSLocalizedString("generic_welcome_firstname_format", tableName: kStringsFileName, bundle: Bundle.main, value: "Welcome \"%@\" !", comment: "")
}
/// Translation in en :
/// Welcome \"%@\" !
func generic_welcome_firstname_format(arg0: String) -> String {
String(format: self.generic_welcome_firstname_format, arg0)
}
// MARK: - Placeholders
/// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d
var placeholders_test_one: String {
NSLocalizedString("placeholders_test_one", tableName: kStringsFileName, bundle: Bundle.main, value: "You %%: %2$@ %1$@ Age: %3$d", comment: "")
}
/// Translation in en :
/// You %%: %2$@ %1$@ Age: %3$d
func placeholders_test_one(arg0: String, arg1: String, arg2: Int) -> String {
String(format: self.placeholders_test_one, arg0, arg1, arg2)
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,205 @@
// Generated by ResgenSwift.Analytics 1.2
import MatomoTracker
import FirebaseAnalytics
// MARK: - Protocol
protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String)
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
)
}
// MARK: - Matomo
class MatomoAnalyticsManager: AnalyticsManagerProtocol {
// MARK: - Properties
private var tracker: MatomoTracker
// MARK: - Init
init(siteId: String, url: String) {
debugPrint("[Matomo service] Server URL: \(url)")
debugPrint("[Matomo service] Site ID: \(siteId)")
tracker = MatomoTracker(
siteId: siteId,
baseURL: URL(string: url)!
)
#if DEBUG
tracker.dispatchInterval = 5
#endif
#if DEBUG
tracker.logger = DefaultLogger(minLevel: .verbose)
#endif
debugPrint("[Matomo service] Configured with content base: \(tracker.contentBase?.absoluteString ?? "-")")
debugPrint("[Matomo service] Opt out: \(tracker.isOptedOut)")
}
// MARK: - Methods
func logScreen(name: String, path: String) {
guard !tracker.isOptedOut else { return }
guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
let urlString = URL(string: "\(trackerUrl)" + "/" + "\(path)" + "iOS")
tracker.track(
view: [name],
url: urlString
)
}
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
guard !tracker.isOptedOut else { return }
tracker.track(
eventWithCategory: category,
action: action,
name: name,
number: nil,
url: nil
)
}
}
// MARK: - Firebase
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
func logScreen(name: String, path: String) {
var parameters = [
AnalyticsParameterScreenName: name as NSObject
]
Analytics.logEvent(
AnalyticsEventScreenView,
parameters: parameters
)
}
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
var parameters: [String:NSObject] = [
"action": action as NSObject,
"category": category as NSObject,
]
if let supplementaryParameters = params {
for (newKey, newValue) in supplementaryParameters {
if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
}
}
Analytics.logEvent(
name.replacingOccurrences(of: [" "], with: "_"),
parameters: parameters
)
}
}
// MARK: - Manager
class AnalyticsManager {
static var shared = AnalyticsManager()
// MARK: - Properties
var managers: [AnalyticsManagerProtocol] = []
private var isEnabled: Bool = true
// MARK: - Methods
func setAnalyticsEnabled(_ enable: Bool) {
isEnabled = enable
}
func configure(siteId: String, url: String) {
managers.append(
MatomoAnalyticsManager(
siteId: siteId,
url: url
)
)
managers.append(FirebaseAnalyticsManager())
}
private func logScreen(name: String, path: String) {
guard isEnabled else { return }
managers.forEach { manager in
manager.logScreen(name: name, path: path)
}
}
private func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
guard isEnabled else { return }
managers.forEach { manager in
manager.logEvent(
name: name,
action: action,
category: category,
params: params
)
}
}
// MARK: - section_one
func logScreenS1DefOne(title: String) {
logScreen(
name: "s1 def one \(title)",
path: "s1_def_one/\(title)"
)
}
func logEventS1DefTwo(title: String, count: String) {
logEvent(
name: "s1 def two",
action: "test",
category: "test",
params: [
"title": title,
"count": count
]
)
}
// MARK: - section_two
func logScreenS2DefOne() {
logScreen(
name: "s2 def one",
path: "s2_def_one/"
)
}
}

View File

@ -1,23 +1,20 @@
// Generated by ResgenSwift.Strings.Tags 1.0.0
// typelias Tags = String
// Generated by ResgenSwift.Strings.Tags 1.2
import UIKit
extension Tags {
// MARK: - ScreenTag
// MARK: - ScreenTag
/// Translation in ium :
/// Ecran un
static var screen_one: String {
var screen_one: String {
"Ecran un"
}
/// Translation in ium :
/// Ecran deux
static var screen_two: String {
var screen_two: String {
"Ecran deux"
}
}
}

View File

@ -1,23 +0,0 @@
// Generated from Strings-Tags at 2022-02-14 09:30:20 +0000
// typelias Tags = String
import UIKit
extension Tags {
// MARK: - ScreenTag
/// Translation in ium :
/// Ecran un
static var screen_one: String {
"Ecran un"
}
/// Translation in ium :
/// Ecran deux
static var screen_two: String {
"Ecran deux"
}
}

View File

@ -0,0 +1,52 @@
---
categories:
- id: section_one
screens:
- id: s1_def_one
name: s1 def one _TITLE_
path: s1_def_one/_TITLE_
tags: ios,droid
parameters:
- name: title
type: String
replaceIn: name,path
events:
- id: s1_def_two
name: s1 def two
action: test
category: test
tags: ios
parameters:
- name: title
type: String
- name: count
type: String
- id: section_two
screens:
- id: s2_def_one
name: s2 def one
path: s2_def_one/
tags: ios
events:
- id: s2_def_two
name: s2 def two
action: test
category: test
tags: droid
- id: section_three
screens:
- id: s3_def_one
name: s3 def one
path: s3_def_one/
tags: droid
events:
- id: s3_def_two
name: s3 def two
action: test
category: test
tags: droid

View File

@ -2,56 +2,69 @@
FORCE_FLAG="$1"
# Font
swift run -c release FontTool $FORCE_FLAG "./Fonts/sampleFontsAll.txt" \
--extension-output-path "./Fonts/Generated" \
--extension-name "UIFont" \
--extension-suffix "GenAllScript"
## 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"
#
#echo "\n-------------------------\n"
#
## Color
#swift run -c release ResgenSwift colors $FORCE_FLAG "./Colors/sampleColors1.txt" \
# --style all \
# --xcassets-path "./Colors/colors.xcassets" \
# --extension-output-path "./Colors/Generated/" \
# --extension-name "ColorYolo" \
# --extension-name-ui-kit "UIhkjhkColorYolo" \
# --extension-suffix "GenAllScript"
#
#echo "\n-------------------------\n"
#
## Twine
#swift run -c release ResgenSwift strings twine $FORCE_FLAG "./Twine/sampleStrings.txt" \
# --output-path "./Twine/Generated" \
# --langs "fr en en-us" \
# --default-lang "en" \
# --extension-output-path "./Twine/Generated"
echo "\n-------------------------\n"
#echo "\n-------------------------\n"
# Color
swift run -c release ColorTool $FORCE_FLAG "./Colors/sampleColors1.txt" \
--style all \
--xcassets-path "./Colors/colors.xcassets" \
--extension-output-path "./Colors/Generated/" \
--extension-name "UIColor" \
--extension-suffix "GenAllScript"
## Strings
#swift run -c release ResgenSwift strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
# --output-path "./Strings/Generated" \
# --langs "fr en en-us" \
# --default-lang "en" \
# --extension-output-path "./Strings/Generated" \
# --extension-name "String" \
# --extension-suffix "GenAllScript"
echo "\n-------------------------\n"
#echo "\n-------------------------\n"
# Twine
swift run -c release Strings twine $FORCE_FLAG "./Twine/sampleStrings.txt" \
--output-path "./Twine/Generated" \
--langs "fr en en-us" \
--default-lang "en" \
--extension-output-path "./Twine/Generated"
## Tags
#swift run -c release ResgenSwift strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \
# --lang "ium" \
# --extension-output-path "./Tags/Generated" \
# --extension-name "Tags" \
# --extension-suffix "GenAllScript"
echo "\n-------------------------\n"
#echo "\n-------------------------\n"
# Strings
swift run -c release Strings stringium $FORCE_FLAG "./Strings/sampleStrings.txt" \
--output-path "./Strings/Generated" \
--langs "fr en en-us" \
--default-lang "en" \
--extension-output-path "./Strings/Generated" \
--extension-name "String" \
--extension-suffix "GenAllScript"
echo "\n-------------------------\n"
# Tags
swift run -c release Strings tags $FORCE_FLAG "./Tags/sampleTags.txt" \
--lang "ium" \
# Analytics
swift run -c release ResgenSwift analytics $FORCE_FLAG "./Tags/sampleTags.yml" \
--target "matomo firebase" \
--extension-output-path "./Tags/Generated" \
--extension-name "Tags" \
--extension-name "Analytics" \
--extension-suffix "GenAllScript"
echo "\n-------------------------\n"
# Images
swift run -c release Imagium $FORCE_FLAG "./Images/sampleImages.txt" \
--xcassets-path "./Images/imagium.xcassets" \
--extension-output-path "./Images/Generated" \
--extension-name "UIImage" \
--extension-suffix "GenAllScript"
#echo "\n-------------------------\n"
#
## Images
#swift run -c release ResgenSwift images $FORCE_FLAG "./Images/sampleImages.txt" \
# --xcassets-path "./Images/imagium.xcassets" \
# --extension-output-path "./Images/Generated" \
# --extension-name "ImageYolo" \
# --extension-name-ui-kit "UIImageYolo" \
# --extension-suffix "GenAllScript"

View File

@ -0,0 +1,103 @@
---
#
# Class architecture
#
architecture:
property: R
classname: R
path: ./Tags
children:
- property: images
classname: R2Image
- property: strings
classname: R2String
- property: fonts
classname: R2Font
- property: images
classname: R2Image
- property: ui
classname: R2UI
children:
- property: images
classname: R2UIImage
- property: fonts
classname: R2UIFont
- property: images
classname: R2UIImage
#
# Strings
#
strings:
-
inputFile: ./Strings/sampleStrings.txt
outputPath: ./Strings/Generated
langs: "fr en en-us"
defaultLang: en
extensionOutputPath: ./Strings/Generated
extensionName: String
extensionSuffix: GenAllScript
#
# Images
#
images:
-
inputFile: ./Images/sampleImages.txt
xcassetsPath: ./Images/imagium.xcassets
extensionOutputPath: ./Images/Generated
extensionName: ImageYolo
extensionNameUIKit: UIImageYolo
extensionSuffix: GenAllScript
#
# Colors
#
colors:
-
inputFile: ./Colors/sampleColors1.txt
style: all
xcassetsPath: ./Colors/colors.xcassets
extensionOutputPath: ./Colors/Generated/
extensionName: ColorYolo
extensionNameUIKit: UIColorYolo
extensionSuffix: GenAllScript
#
# Tags
#
tags:
-
inputFile: ./Tags/sampleTags.txt
lang: ium
extensionOutputPath: ./Tags/Generated
extensionName: Tags
extensionSuffix: GenAllScript
#
# Analytics
#
analytics:
-
inputFile: ./Tags/sampleTags.yml
target: "matomo firebase"
extensionOutputPath: ./Tags/Generated
extensionName: Analytics
extensionSuffix: GenAllScript
#
# Fonts
#
fonts:
-
inputFile: ./Fonts/sampleFontsAll.txt
extensionOutputPath: ./Fonts/Generated
extensionName: FontYolo
extensionNameUIKit: UIFontYolo
extensionSuffix: GenAllScript
infoPlistPaths: "./Fonts/Generated/test.plist ./Fonts/Generated/test2.plist"

View File

@ -0,0 +1,86 @@
//
// Analytics.swift
//
//
// Created by Loris Perret on 08/12/2023.
//
import ToolCore
import Foundation
import ArgumentParser
struct Analytics: ParsableCommand {
// MARK: - Command Configuration
static var configuration = CommandConfiguration(
abstract: "Generate analytics extension file.",
version: ResgenSwiftVersion
)
// MARK: - Static
static let toolName = "Analytics"
static let defaultExtensionName = "Analytics"
// MARK: - Command Options
@OptionGroup var options: AnalyticsOptions
// MARK: - Run
mutating func run() {
print("[\(Self.toolName)] Starting analytics generation")
print("[\(Self.toolName)] Will use inputFile \(options.inputFile) to generate analytics for target: \(options.target)")
// Check requirements
guard checkRequirements() else { return }
print("[\(Self.toolName)] Will generate analytics")
// Check requirements
guard checkRequirements() else { return }
// Parse input file
let sections = AnalyticsFileParser.parse(options.inputFile, target: options.target)
// Generate extension
AnalyticsGenerator.writeExtensionFiles(sections: sections,
target: options.target,
tags: ["ios", "iosonly"],
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
print("[\(Self.toolName)] Analytics generated")
}
// MARK: - Requirements
private func checkRequirements() -> Bool {
let fileManager = FileManager()
// Input file
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = AnalyticsError.fileNotExists(options.inputFile)
print(error.description)
Analytics.exit(withError: error)
}
guard TrackerType.hasValidTarget(in: options.target) else {
let error = AnalyticsError.noValidTracker(options.target)
print(error.description)
Analytics.exit(withError: error)
}
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(force: options.forceGeneration,
inputFilePath: options.inputFile,
extensionFilePath: options.extensionFilePath) else {
print("[\(Self.toolName)] Analytics are already up to date :) ")
return false
}
return true
}
}

View File

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

View File

@ -0,0 +1,47 @@
//
// AnalyticsOptions.swift
//
//
// Created by Loris Perret on 08/12/2023.
//
import Foundation
import ArgumentParser
struct AnalyticsOptions: ParsableArguments {
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
var forceGeneration = false
@Argument(help: "Input files where tags ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var inputFile: String
@Option(help: "Target(s) analytics to generate. (\"matomo\" | \"firebase\")")
var target: String
@Option(help: "Path where to generate the extension.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var extensionOutputPath: String
@Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate a Analytics extension.")
var extensionName: String = Analytics.defaultExtensionName
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Analytics{extensionSuffix}.swift")
var extensionSuffix: String?
}
// MARK: - Computed var
extension AnalyticsOptions {
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
}
return "\(extensionName).swift"
}
var extensionFilePath: String {
"\(extensionOutputPath)/\(extensionFileName)"
}
}

View File

@ -0,0 +1,227 @@
//
// AnalyticsGenerator.swift
//
//
// Created by Loris Perret on 08/12/2023.
//
import Foundation
import ToolCore
import CoreVideo
class AnalyticsGenerator {
static var targets: [TrackerType] = []
static func writeExtensionFiles(sections: [AnalyticsCategory], target: String, tags: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
// Get target type from enum
let targetsString: [String] = target.components(separatedBy: " ")
TrackerType.allCases.forEach { enumTarget in
if targetsString.contains(enumTarget.value) {
targets.append(enumTarget)
}
}
// Get extension content
let extensionFileContent = Self.getExtensionContent(sections: sections,
tags: tags,
staticVar: staticVar,
extensionName: extensionName)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch let error {
let error = AnalyticsError.writeFile(extensionFilePath, error.localizedDescription)
print(error.description)
Analytics.exit(withError: error)
}
}
// MARK: - Extension content
static func getExtensionContent(sections: [AnalyticsCategory], tags: [String], staticVar: Bool, extensionName: String) -> String {
[
Self.getHeader(extensionClassname: extensionName, staticVar: staticVar),
Self.getProperties(sections: sections, tags: tags, staticVar: staticVar),
Self.getFooter()
]
.joined(separator: "\n")
}
// MARK: - Extension part
private static func getHeader(extensionClassname: String, staticVar: Bool) -> String {
"""
// Generated by ResgenSwift.\(Analytics.toolName) \(ResgenSwiftVersion)
\(Self.getImport())
\(Self.getAnalyticsProtocol())
// MARK: - Manager
class AnalyticsManager {
static var shared = AnalyticsManager()
// MARK: - Properties
var managers: [AnalyticsManagerProtocol] = []
\(Self.getEnabledContent())
\(Self.getAnalyticsProperties())
\(Self.getPrivateLogFunction())
"""
}
private static func getEnabledContent() -> String {
"""
private var isEnabled: Bool = true
// MARK: - Methods
func setAnalyticsEnabled(_ enable: Bool) {
isEnabled = enable
}
"""
}
private static func getImport() -> String {
var result: [String] = []
if targets.contains(TrackerType.matomo) {
result.append("import MatomoTracker")
}
if targets.contains(TrackerType.firebase) {
result.append("import FirebaseAnalytics")
}
return result.joined(separator: "\n")
}
private static func getPrivateLogFunction() -> String {
"""
private func logScreen(name: String, path: String) {
guard isEnabled else { return }
managers.forEach { manager in
manager.logScreen(name: name, path: path)
}
}
private func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
guard isEnabled else { return }
managers.forEach { manager in
manager.logEvent(
name: name,
action: action,
category: category,
params: params
)
}
}
"""
}
private static func getAnalyticsProperties() -> String {
var header = ""
var content: [String] = []
let footer = " }"
if targets.contains(TrackerType.matomo) {
header = "func configure(siteId: String, url: String) {"
} else if targets.contains(TrackerType.firebase) {
header = "func configure() {"
}
if targets.contains(TrackerType.matomo) {
content.append("""
managers.append(
MatomoAnalyticsManager(
siteId: siteId,
url: url
)
)
""")
}
if targets.contains(TrackerType.firebase) {
content.append(" managers.append(FirebaseAnalyticsManager())")
}
return [
header,
content.joined(separator: "\n"),
footer
]
.joined(separator: "\n")
}
private static func getAnalyticsProtocol() -> String {
let proto = """
// MARK: - Protocol
protocol AnalyticsManagerProtocol {
func logScreen(name: String, path: String)
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
)
}
"""
var result: [String] = [proto]
if targets.contains(TrackerType.matomo) {
result.append(MatomoGenerator.service)
}
if targets.contains(TrackerType.firebase) {
result.append(FirebaseGenerator.service)
}
return result.joined(separator: "\n")
}
private static func getProperties(sections: [AnalyticsCategory], tags: [String], staticVar: Bool) -> String {
sections
.compactMap { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
return nil// Go to next section
}
var res = "\n // MARK: - \(section.id)"
section.definitions.forEach { definition in
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
return // Go to next definition
}
if staticVar {
res += "\n\n\(definition.getStaticProperty())"
} else {
res += "\n\n\(definition.getProperty())"
}
}
return res
}
.joined(separator: "\n")
}
private static func getFooter() -> String {
"""
}
"""
}
}

View File

@ -0,0 +1,87 @@
//
// FirebaseGenerator.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
enum FirebaseGenerator {
static var service: String {
[
FirebaseGenerator.header,
FirebaseGenerator.logScreen,
FirebaseGenerator.logEvent,
FirebaseGenerator.footer
]
.joined(separator: "\n")
}
// MARK: - Private vars
private static var header: String {
"""
// MARK: - Firebase
class FirebaseAnalyticsManager: AnalyticsManagerProtocol {
"""
}
private static var logScreen: String {
"""
func logScreen(name: String, path: String) {
var parameters = [
AnalyticsParameterScreenName: name as NSObject
]
Analytics.logEvent(
AnalyticsEventScreenView,
parameters: parameters
)
}
"""
}
private static var logEvent: String {
"""
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
var parameters: [String:NSObject] = [
"action": action as NSObject,
"category": category as NSObject,
]
if let supplementaryParameters = params {
for (newKey, newValue) in supplementaryParameters {
if parameters.contains(where: { (key: String, value: NSObject) in
key == newKey
}) {
continue
}
parameters[newKey] = newValue as? NSObject
}
}
Analytics.logEvent(
name.replacingOccurrences(of: [" "], with: "_"),
parameters: parameters
)
}
"""
}
private static var footer: String {
"""
}
"""
}
}

View File

@ -0,0 +1,110 @@
//
// MatomoGenerator.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
enum MatomoGenerator {
static var service: String {
[
MatomoGenerator.header,
MatomoGenerator.setup,
MatomoGenerator.logScreen,
MatomoGenerator.logEvent,
MatomoGenerator.footer
]
.joined(separator: "\n")
}
// MARK: - Private vars
private static var header: String {
"""
// MARK: - Matomo
class MatomoAnalyticsManager: AnalyticsManagerProtocol {
// MARK: - Properties
private var tracker: MatomoTracker
"""
}
private static var setup: String {
"""
// MARK: - Init
init(siteId: String, url: String) {
debugPrint("[Matomo service] Server URL: \\(url)")
debugPrint("[Matomo service] Site ID: \\(siteId)")
tracker = MatomoTracker(
siteId: siteId,
baseURL: URL(string: url)!
)
#if DEBUG
tracker.dispatchInterval = 5
#endif
#if DEBUG
tracker.logger = DefaultLogger(minLevel: .verbose)
#endif
debugPrint("[Matomo service] Configured with content base: \\(tracker.contentBase?.absoluteString ?? "-")")
debugPrint("[Matomo service] Opt out: \\(tracker.isOptedOut)")
}
// MARK: - Methods
"""
}
private static var logScreen: String {
"""
func logScreen(name: String, path: String) {
guard !tracker.isOptedOut else { return }
guard let trackerUrl = tracker.contentBase?.absoluteString else { return }
let urlString = URL(string: "\\(trackerUrl)" + "/" + "\\(path)" + "iOS")
tracker.track(
view: [name],
url: urlString
)
}
"""
}
private static var logEvent: String {
"""
func logEvent(
name: String,
action: String,
category: String,
params: [String: Any]?
) {
guard !tracker.isOptedOut else { return }
tracker.track(
eventWithCategory: category,
action: action,
name: name,
number: nil,
url: nil
)
}
"""
}
private static var footer: String {
"""
}
"""
}
}

View File

@ -0,0 +1,32 @@
//
// AnalyticsCategory.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
class AnalyticsCategory {
let id: String // OnBoarding
var definitions = [AnalyticsDefinition]()
// MARK: - Init
init(id: String) {
self.id = id
}
// MARK: - Methods
func hasOneOrMoreMatchingTags(tags: [String]) -> Bool {
let allTags = definitions.flatMap { $0.tags }
let allTagsSet = Set(allTags)
let intersection = Set(tags).intersection(allTagsSet)
if intersection.isEmpty {
return false
}
return true
}
}

View File

@ -0,0 +1,157 @@
//
// AnalyticsDefinition.swift
//
//
// Created by Loris Perret on 05/12/2023.
//
import Foundation
import ToolCore
class AnalyticsDefinition {
let id: String
var name: String
var path: String = ""
var category: String = ""
var action: String = ""
var comments: String = ""
var tags: [String] = []
var parameters: [AnalyticsParameter] = []
var type: TagType
// MARK: - Init
init(id: String, name: String, type: TagType) {
self.id = id
self.name = name
self.type = type
}
// MARK: - Methods
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
if Set(inputTags).isDisjoint(with: tags) {
return false
}
return true
}
// MARK: - Private Methods
private func getFuncName() -> String {
var pascalCaseTitle: String = ""
id.components(separatedBy: "_").forEach { word in
pascalCaseTitle.append(contentsOf: word.uppercasedFirst())
}
return "log\(type == .screen ? "Screen" : "Event")\(pascalCaseTitle)"
}
private func getParameters() -> String {
var params = parameters
var result: String
if type == .screen {
params = params.filter { param in
!param.replaceIn.isEmpty
}
}
let paramsString = params.map { parameter in
"\(parameter.name): \(parameter.type)"
}
if paramsString.count > 2 {
result = """
(
\(paramsString.joined(separator: ",\n\t\t"))
)
"""
} else {
result = """
(\(paramsString.joined(separator: ", ")))
"""
}
return result
}
private func replaceIn() {
for parameter in parameters {
for rep in parameter.replaceIn {
switch rep {
case "name": name = name.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
case "path": path = path.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
case "category": category = category.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
case "action": action = action.replacingFirstOccurrence(of: "_\(parameter.name.uppercased())_", with: "\\(\(parameter.name))")
default: break
}
}
}
}
private func getlogFunction() -> String {
var params: [String] = []
var result: String
let supplementaryParams = parameters.filter { param in
param.replaceIn.isEmpty
}
supplementaryParams.forEach { param in
params.append("\"\(param.name)\": \(param.name)")
}
if params.count > 1 {
result = """
[
\(params.joined(separator: ",\n\t\t\t\t"))
]
"""
} else if params.count == 1 {
result = """
[\(params.joined(separator: ", "))]
"""
} else {
result = "[:]"
}
if type == .screen {
return """
logScreen(
name: "\(name)",
path: "\(path)"
)
"""
} else {
return """
logEvent(
name: "\(name)",
action: "\(action)",
category: "\(category)",
params: \(result)
)
"""
}
}
// MARK: - Raw strings
func getProperty() -> String {
replaceIn()
return """
func \(getFuncName())\(getParameters()) {
\(getlogFunction())
}
"""
}
func getStaticProperty() -> String {
replaceIn()
return """
static func \(getFuncName())\(getParameters()) {
\(getlogFunction())
}
"""
}
}

View File

@ -0,0 +1,45 @@
//
// AnalyticsFile.swift
//
//
// Created by Loris Perret on 06/12/2023.
//
import Foundation
struct AnalyticsFile: Codable {
var categories: [AnalyticsCategoryDTO]
}
struct AnalyticsCategoryDTO: Codable {
var id: String
var screens: [AnalyticsDefinitionScreenDTO]?
var events: [AnalyticsDefinitionEventDTO]?
}
struct AnalyticsDefinitionScreenDTO: Codable {
var id: String
var name: String
var tags: String
var comments: String?
var parameters: [AnalyticsParameterDTO]?
var path: String?
}
struct AnalyticsDefinitionEventDTO: Codable {
var id: String
var name: String
var tags: String
var comments: String?
var parameters: [AnalyticsParameterDTO]?
var category: String?
var action: String?
}
struct AnalyticsParameterDTO: Codable {
var name: String
var type: String
var replaceIn: String?
}

View File

@ -0,0 +1,21 @@
//
// AnalyticsParameter.swift
//
//
// Created by Loris Perret on 06/12/2023.
//
import Foundation
class AnalyticsParameter {
var name: String
var type: String
var replaceIn: [String] = []
// MARK: - Init
init(name: String, type: String) {
self.name = name
self.type = type
}
}

View File

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

View File

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

View File

@ -0,0 +1,178 @@
//
// AnalyticsFileParser.swift
//
//
// Created by Loris Perret on 06/12/2023.
//
import Foundation
import Yams
class AnalyticsFileParser {
private static var inputFile: String = ""
private static var target: String = ""
private static func parseYaml() -> AnalyticsFile {
guard let data = FileManager().contents(atPath: inputFile) else {
let error = AnalyticsError.fileNotExists(inputFile)
print(error.description)
Analytics.exit(withError: error)
}
do {
let tagFile = try YAMLDecoder().decode(AnalyticsFile.self, from: data)
return tagFile
} catch {
let error = AnalyticsError.parseFailed(error.localizedDescription)
print(error.description)
Analytics.exit(withError: error)
}
}
private static func getParameters(from parameters: [AnalyticsParameterDTO]) -> [AnalyticsParameter] {
parameters.map { dtoParameter in
// Type
let type = dtoParameter.type.uppercasedFirst()
guard
type == "String" ||
type == "Int" ||
type == "Double" ||
type == "Bool"
else {
let error = AnalyticsError.invalidParameter("type of \(dtoParameter.name)")
print(error.description)
Analytics.exit(withError: error)
}
let parameter = AnalyticsParameter(
name: dtoParameter.name,
type: type
)
if let replaceIn = dtoParameter.replaceIn {
parameter.replaceIn = replaceIn.components(separatedBy: ",")
}
return parameter
}
}
private static func getTagDefinition(
id: String,
name: String,
type: AnalyticsDefinition.TagType,
tags: String,
comments: String?,
parameters: [AnalyticsParameterDTO]?
) -> AnalyticsDefinition {
let definition = AnalyticsDefinition(id: id, name: name, type: type)
definition.tags = tags
.components(separatedBy: ",")
.map { $0.removeLeadingTrailingWhitespace() }
if let comments = comments {
definition.comments = comments
}
if let parameters = parameters {
definition.parameters = Self.getParameters(from: parameters)
}
return definition
}
private static func getTagDefinitionScreen(from screens: [AnalyticsDefinitionScreenDTO]) -> [AnalyticsDefinition] {
screens.map { screen in
let definition: AnalyticsDefinition = Self.getTagDefinition(
id: screen.id,
name: screen.name,
type: .screen,
tags: screen.tags,
comments: screen.comments,
parameters: screen.parameters
)
if target.contains(TrackerType.matomo.value) {
// Path
guard let path = screen.path else {
let error = AnalyticsError.missingElement("screen path")
print(error.description)
Analytics.exit(withError: error)
}
definition.path = path
}
return definition
}
}
private static func getTagDefinitionEvent(from events: [AnalyticsDefinitionEventDTO]) -> [AnalyticsDefinition] {
events.map { event in
let definition: AnalyticsDefinition = Self.getTagDefinition(
id: event.id,
name: event.name,
type: .event,
tags: event.tags,
comments: event.comments,
parameters: event.parameters
)
if target.contains(TrackerType.matomo.value) {
// Category
guard let category = event.category else {
let error = AnalyticsError.missingElement("event category")
print(error.description)
Analytics.exit(withError: error)
}
definition.category = category
// Action
guard let action = event.action else {
let error = AnalyticsError.missingElement("event action")
print(error.description)
Analytics.exit(withError: error)
}
definition.action = action
}
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

@ -21,7 +21,8 @@ struct Colors: ParsableCommand {
// MARK: - Static
static let toolName = "Color"
static let defaultExtensionName = "UIColor"
static let defaultExtensionName = "Color"
static let defaultExtensionNameUIKit = "UIColor"
static let assetsColorsFolderName = "Colors"
// MARK: - Command options
@ -43,7 +44,7 @@ struct Colors: ParsableCommand {
// Get colors to generate
let parsedColors = ColorFileParser.parse(options.inputFile,
colorStyle: options.colorStyle)
colorStyle: options.style)
// -> Time: 0.0020350217819213867 seconds
// Generate all colors in xcassets
@ -55,9 +56,16 @@ struct Colors: ParsableCommand {
ColorExtensionGenerator.writeExtensionFile(colors: parsedColors,
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
// -> Time: 0.0010340213775634766 seconds
extensionFilePath: options.extensionFilePath,
isSwiftUI: true)
// Generate extension
ColorExtensionGenerator.writeExtensionFile(colors: parsedColors,
staticVar: options.staticMembers,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: false)
print("[\(Self.toolName)] Colors generated")
}
@ -69,14 +77,21 @@ struct Colors: ParsableCommand {
// Check if input file exists
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = ColorsToolError.fileNotExists(options.inputFile)
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}
// Check if xcassets file exists
guard fileManager.fileExists(atPath: options.xcassetsPath) else {
let error = ColorsToolError.fileNotExists(options.xcassetsPath)
print(error.localizedDescription)
print(error.description)
Colors.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)
}
@ -102,7 +117,7 @@ struct Colors: ParsableCommand {
try fileManager.removeItem(atPath: assetsColorPath)
} catch {
let error = ColorsToolError.deleteExistingColors("\(options.xcassetsPath)/Colors")
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
enum ColorsToolError: Error {
case extensionNamesCollision(String)
case badFormat(String)
case writeAsset(String)
case createAssetFolder(String)
@ -18,26 +19,29 @@ enum ColorsToolError: Error {
var description: String {
switch self {
case .extensionNamesCollision(let extensionName):
return "error: [\(Fonts.toolName)] Error on extension names, extension name and SwiftUI extension name should be different (\(extensionName) is used on both)"
case .badFormat(let info):
return "error:[\(Colors.toolName)] Bad line format: \(info). Accepted format are: colorName=\"#RGB/#ARGB\"; colorName \"#RGB/#ARGB\"; colorName \"#RGB/#ARGB\" \"#RGB/#ARGB\""
return "error: [\(Colors.toolName)] Bad line format: \(info). Accepted format are: colorName=\"#RGB/#ARGB\"; colorName \"#RGB/#ARGB\"; colorName \"#RGB/#ARGB\" \"#RGB/#ARGB\""
case .writeAsset(let info):
return "error:[\(Colors.toolName)] An error occured while writing color in Xcasset: \(info)"
return "error: [\(Colors.toolName)] An error occured while writing color in Xcasset: \(info)"
case .createAssetFolder(let assetsFolder):
return "error:[\(Colors.toolName)] An error occured while creating colors folder `\(assetsFolder)`"
return "error: [\(Colors.toolName)] An error occured while creating colors folder `\(assetsFolder)`"
case .writeExtension(let filename, let info):
return "error:[\(Colors.toolName)] An error occured while writing extension in \(filename): \(info)"
return "error: [\(Colors.toolName)] An error occured while writing extension in \(filename): \(info)"
case .fileNotExists(let filename):
return "error:[\(Colors.toolName)] File \(filename) does not exists"
return "error: [\(Colors.toolName)] File \(filename) does not exists"
case .badColorDefinition(let lightColor, let darkColor):
return "error:[\(Colors.toolName)] One of these two colors has invalid synthax: -\(lightColor)- or -\(darkColor)-"
return "error: [\(Colors.toolName)] One of these two colors has invalid synthax: -\(lightColor)- or -\(darkColor)-"
case .deleteExistingColors(let assetsFolder):
return "error:[\(Colors.toolName)] An error occured while deleting colors folder `\(assetsFolder)`"
return "error: [\(Colors.toolName)] An error occured while deleting colors folder `\(assetsFolder)`"
}
}
}

View File

@ -16,7 +16,7 @@ struct ColorsToolOptions: ParsableArguments {
var inputFile: String
@Option(help: "Color style to generate: light for light colors only, or all for dark and light colors")
fileprivate var style: String
var style: ColorStyle
@Option(help: "Path of xcassets where to generate colors", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var xcassetsPath: String
@ -27,9 +27,12 @@ struct ColorsToolOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIColor extension. Using default extension name will generate static property.")
@Option(help: "Extension name. If not specified, it will generate an Color extension.")
var extensionName: String = Colors.defaultExtensionName
@Option(help: "SwiftUI Extension name. If not specified, it will generate an UIColor extension.")
var extensionNameUIKit: String = Colors.defaultExtensionNameUIKit
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+ColorsMyApp.swift")
var extensionSuffix: String?
}
@ -37,9 +40,8 @@ struct ColorsToolOptions: ParsableArguments {
// MARK: - Computed var
extension ColorsToolOptions {
var colorStyle: ColorStyle {
ColorStyle(rawValue: style) ?? .all
}
// MARK: - SwiftUI
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
@ -51,4 +53,17 @@ extension ColorsToolOptions {
var extensionFilePath: String {
"\(extensionOutputPath)/\(extensionFileName)"
}
// MARK: - UIKit
var extensionFileNameUIKit: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionNameUIKit)+\(extensionSuffix).swift"
}
return "\(extensionNameUIKit).swift"
}
var extensionFilePathUIKit: String {
"\(extensionOutputPath)/\(extensionFileNameUIKit)"
}
}

View File

@ -13,37 +13,47 @@ struct ColorExtensionGenerator {
let colors: [ParsedColor]
let extensionClassname: String
static func writeExtensionFile(colors: [ParsedColor], staticVar: Bool, extensionName: String, extensionFilePath: String) {
// MARK: - UIKit
static func writeExtensionFile(colors: [ParsedColor],
staticVar: Bool,
extensionName: String,
extensionFilePath: String,
isSwiftUI: Bool) {
// Create extension content
let extensionContent = Self.getExtensionContent(colors: colors,
staticVar: staticVar,
extensionName: extensionName)
extensionName: extensionName,
isSwiftUI: isSwiftUI)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
} catch let error {
let error = ColorsToolError.writeExtension(extensionFilePath, error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}
}
static func getExtensionContent(colors: [ParsedColor], staticVar: Bool, extensionName: String) -> String {
static func getExtensionContent(colors: [ParsedColor],
staticVar: Bool,
extensionName: String,
isSwiftUI: Bool) -> String {
[
Self.getHeader(extensionClassname: extensionName),
Self.getProperties(for: colors, withStaticVar: staticVar),
Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI),
Self.getProperties(for: colors, withStaticVar: staticVar, isSwiftUI: isSwiftUI),
Self.getFooter()
]
.joined(separator: "\n")
}
private static func getHeader(extensionClassname: String) -> String {
private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String {
"""
// Generated by ResgenSwift.\(Colors.toolName) \(ResgenSwiftVersion)
import UIKit
import \(isSwiftUI ? "SwiftUI" : "UIKit")
extension \(extensionClassname) {\n
"""
@ -52,15 +62,15 @@ struct ColorExtensionGenerator {
private static func getFooter() -> String {
"""
}
"""
}
private static func getProperties(for colors: [ParsedColor], withStaticVar staticVar: Bool) -> String {
private static func getProperties(for colors: [ParsedColor],
withStaticVar staticVar: Bool,
isSwiftUI: Bool) -> String {
colors.map {
if staticVar {
return $0.getColorStaticProperty()
}
return $0.getColorProperty()
$0.getColorProperty(isStatic: staticVar, isSwiftUI: isSwiftUI)
}
.joined(separator: "\n\n")
}

View File

@ -29,7 +29,7 @@ struct ColorXcassetHelper {
withIntermediateDirectories: true)
} catch {
let error = ColorsToolError.createAssetFolder(colorSetPath)
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}
}
@ -38,9 +38,9 @@ struct ColorXcassetHelper {
let contentsJsonPathURL = URL(fileURLWithPath: contentsJsonPath)
do {
try color.contentsJSON().write(to: contentsJsonPathURL, atomically: false, encoding: .utf8)
} catch (let error) {
} catch let error {
let error = ColorsToolError.writeAsset(error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}
}

View File

@ -6,8 +6,16 @@
//
import Foundation
import ArgumentParser
enum ColorStyle: String, Decodable {
enum ColorStyle: String, Decodable, ExpressibleByArgument {
case light
case all
static var allValueStrings: [String] {
[
Self.light.rawValue,
Self.all.rawValue
]
}
}

View File

@ -26,7 +26,7 @@ struct ParsedColor {
guard allComponents.contains(true) == false else {
let error = ColorsToolError.badColorDefinition(light, dark)
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}
@ -72,19 +72,20 @@ struct ParsedColor {
"""
}
func getColorProperty() -> String {
"""
// MARK: - UIKit
func getColorProperty(isStatic: Bool, isSwiftUI: Bool) -> String {
if isSwiftUI {
return """
/// Color \(name) is \(light) (light) or \(dark) (dark)"
\(isStatic ? "static " : "")var \(name): Color {
Color("\(name)")
}
"""
}
return """
/// Color \(name) is \(light) (light) or \(dark) (dark)"
@objc var \(name): UIColor {
UIColor(named: "\(name)")!
}
"""
}
func getColorStaticProperty() -> String {
"""
/// Color \(name) is \(light) (light) or \(dark) (dark)"
static var \(name): UIColor {
\(isStatic ? "static " : "@objc ")var \(name): UIColor {
UIColor(named: "\(name)")!
}
"""

View File

@ -37,7 +37,7 @@ class ColorFileParser {
guard colorContent.count >= 2 else {
let error = ColorsToolError.badFormat(colorLine)
print(error.localizedDescription)
print(error.description)
Colors.exit(withError: error)
}

View File

@ -21,16 +21,25 @@ struct FontsOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or methods")
var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIFont extension. Using default extension name will generate static property.")
@Option(help: "Extension name. If not specified, it will generate an Font extension.")
var extensionName: String = Fonts.defaultExtensionName
@Option(help: "Extension name. If not specified, it will generate an UIFont extension.")
var extensionNameUIKit: String = Fonts.defaultExtensionNameUIKit
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+FontsMyApp.swift")
var extensionSuffix: String = ""
@Option(name: .customLong("info-plist-paths"), help: "Info.plist paths (array). Will be used to update UIAppFonts content")
fileprivate var infoPlistPathsRaw: String = ""
}
// MARK: - Computed var
extension FontsOptions {
// MARK: - SwiftUI
var extensionFileName: String {
if extensionSuffix.isEmpty == false {
return "\(extensionName)+\(extensionSuffix).swift"
@ -41,4 +50,25 @@ extension FontsOptions {
var extensionFilePath: String {
"\(extensionOutputPath)/\(extensionFileName)"
}
// MARK: - UIKit
var extensionFileNameUIKit: String {
if extensionSuffix.isEmpty == false {
return "\(extensionNameUIKit)+\(extensionSuffix).swift"
}
return "\(extensionNameUIKit).swift"
}
var extensionFilePathUIKit: String {
"\(extensionOutputPath)/\(extensionFileNameUIKit)"
}
// MARK: -
var infoPlistPaths: [String] {
infoPlistPathsRaw
.split(separator: " ")
.map { String($0) }
}
}

View File

@ -21,7 +21,8 @@ struct Fonts: ParsableCommand {
// MARK: - Static
static let toolName = "Fonts"
static let defaultExtensionName = "UIFont"
static let defaultExtensionName = "Font"
static let defaultExtensionNameUIKit = "UIFont"
// MARK: - Command Options
@ -50,10 +51,17 @@ struct Fonts: ParsableCommand {
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames,
staticVar: options.staticMembers,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
extensionFilePath: options.extensionFilePath,
isSwiftUI: true)
print("Info.plist information:")
print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames))")
FontExtensionGenerator.writeExtensionFile(fontsNames: fontsNames,
staticVar: options.staticMembers,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: false)
print("Info.plist has been updated with:")
print("\(FontPlistGenerator.generatePlistUIAppsFontContent(for: fontsNames, infoPlistPaths: options.infoPlistPaths))")
print("[\(Self.toolName)] Fonts generated")
}
@ -66,7 +74,14 @@ struct Fonts: ParsableCommand {
// Check input file exists
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = FontsToolError.fileNotExists(options.inputFile)
print(error.localizedDescription)
print(error.description)
Fonts.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)
}

View File

@ -8,24 +8,28 @@
import Foundation
enum FontsToolError: Error {
case extensionNamesCollision(String)
case fcScan(String, Int32, String?)
case inputFolderNotFound(String)
case fileNotExists(String)
case writeExtension(String, String)
var localizedDescription: String {
var description: String {
switch self {
case .extensionNamesCollision(let extensionName):
return "error: [\(Fonts.toolName)] Error on extension names, extension name and SwiftUI extension name should be different (\(extensionName) is used on both)"
case .fcScan(let path, let code, let output):
return "error:[\(Fonts.toolName)] Error while getting fontName (fc-scan --format %{postscriptname} \(path). fc-scan exit with \(code) and output is: \(output ?? "no output")"
return "error: [\(Fonts.toolName)] Error while getting fontName (fc-scan --format %{postscriptname} \(path). fc-scan exit with \(code) and output is: \(output ?? "no output")"
case .inputFolderNotFound(let inputFolder):
return " error:[\(Fonts.toolName)] Input folder not found: \(inputFolder)"
return "error: [\(Fonts.toolName)] Input folder not found: \(inputFolder)"
case .fileNotExists(let filename):
return " error:[\(Fonts.toolName)] File \(filename) does not exists"
return "error: [\(Fonts.toolName)] File \(filename) does not exists"
case .writeExtension(let filename, let info):
return "error:[\(Fonts.toolName)] An error occured while writing extension in \(filename): \(info)"
return "error: [\(Fonts.toolName)] An error occured while writing extension in \(filename): \(info)"
}
}
}

View File

@ -39,7 +39,7 @@ class FontsToolHelper {
let fileManager = FileManager()
guard fileManager.fileExists(atPath: inputFolder) else {
let error = FontsToolError.inputFolderNotFound(inputFolder)
print(error.localizedDescription)
print(error.description)
Fonts.exit(withError: error)
}
@ -64,7 +64,7 @@ class FontsToolHelper {
guard let fontName = task.output, task.terminationStatus == 0 else {
let error = FontsToolError.fcScan(path, task.terminationStatus, task.output)
print(error.localizedDescription)
print(error.description)
Fonts.exit(withError: error)
}

View File

@ -6,12 +6,34 @@
//
import Foundation
import ToolCore
class FontPlistGenerator {
static func generatePlistUIAppsFontContent(for fonts: [FontName]) -> String {
var plistData = "<key>UIAppFonts</key>\n\t<array>\n"
fonts
static func generatePlistUIAppsFontContent(for fonts: [FontName], infoPlistPaths: [String]) -> String {
let fontsToAddToPlist = fonts
.compactMap { $0 }
// Update each plist
infoPlistPaths.forEach { infoPlist in
// Remove UIAppFonts value
Shell.shell(launchPath: "/usr/libexec/PlistBuddy",
["-c", "delete :UIAppFonts", infoPlist])
// Add UIAppFonts empty array
debugPrint("Will 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])
}
}
var plistData = "<key>UIAppFonts</key>\n\t<array>\n"
fontsToAddToPlist
.forEach {
plistData += "\t\t<string>\($0)</string>\n"
}

View File

@ -9,44 +9,7 @@ import Foundation
import ToolCore
class FontExtensionGenerator {
static func writeExtensionFile(fontsNames: [String], staticVar: Bool, extensionName: String, extensionFilePath: String) {
// Create extension content
let extensionContent = Self.getExtensionContent(fontsNames: fontsNames,
staticVar: staticVar,
extensionName: extensionName)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription)
print(error.localizedDescription)
Fonts.exit(withError: error)
}
}
static func getExtensionContent(fontsNames: [String], staticVar: Bool, extensionName: String) -> String {
[
Self.getHeader(extensionClassname: extensionName),
Self.getFontNameEnum(fontsNames: fontsNames),
Self.getFontMethods(fontsNames: fontsNames, staticVar: staticVar),
Self.getFooter()
]
.joined(separator: "\n")
}
private static func getHeader(extensionClassname: String) -> String {
"""
// Generated by ResgenSwift.\(Fonts.toolName) \(ResgenSwiftVersion)
import UIKit
extension \(extensionClassname) {\n
"""
}
private static func getFontNameEnum(fontsNames: [String]) -> String {
var enumDefinition = " enum FontName: String {\n"
@ -58,17 +21,58 @@ class FontExtensionGenerator {
return enumDefinition
}
private static func getFontMethods(fontsNames: [FontName], staticVar: Bool) -> String {
static func writeExtensionFile(fontsNames: [String],
staticVar: Bool,
extensionName: String,
extensionFilePath: String,
isSwiftUI: Bool) {
// Create extension content
let extensionContent = Self.getExtensionContent(fontsNames: fontsNames,
staticVar: staticVar,
extensionName: extensionName,
isSwiftUI: isSwiftUI)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch let error {
let error = FontsToolError.writeExtension(extensionFilePath, error.localizedDescription)
print(error.description)
Fonts.exit(withError: error)
}
}
static func getExtensionContent(fontsNames: [String],
staticVar: Bool,
extensionName: String,
isSwiftUI: Bool) -> String {
[
Self.getHeader(extensionClassname: extensionName, isSwiftUI: isSwiftUI),
Self.getFontNameEnum(fontsNames: fontsNames),
Self.getFontMethods(fontsNames: fontsNames, staticVar: staticVar, isSwiftUI: isSwiftUI),
Self.getFooter()
]
.joined(separator: "\n")
}
private static func getHeader(extensionClassname: String, isSwiftUI: Bool) -> String {
"""
// Generated by ResgenSwift.\(Fonts.toolName) \(ResgenSwiftVersion)
import \(isSwiftUI ? "SwiftUI" : "UIKit")
extension \(extensionClassname) {\n
"""
}
private static func getFontMethods(fontsNames: [FontName], staticVar: Bool, isSwiftUI: Bool) -> String {
let pragma = " // MARK: - Getter"
var propertiesOrMethods: [String] = fontsNames
.unique()
.map {
if staticVar {
return $0.staticProperty
} else {
return $0.method
}
$0.getProperty(isStatic: staticVar, isSwiftUI: isSwiftUI)
}
propertiesOrMethods.insert(pragma, at: 0)
@ -78,6 +82,7 @@ class FontExtensionGenerator {
private static func getFooter() -> String {
"""
}
"""
}
}

View File

@ -14,19 +14,33 @@ extension FontName {
self.removeCharacters(from: "[]+-_")
}
var method: String {
"""
func getProperty(isStatic: Bool, isSwiftUI: Bool) -> String {
if isSwiftUI {
if isStatic {
return """
static let \(fontNameSanitize): ((_ size: CGFloat) -> Font) = { size in
Font.custom(FontName.\(fontNameSanitize).rawValue, size: size)
}
"""
}
return """
func \(fontNameSanitize)(withSize size: CGFloat) -> Font {
Font.custom(FontName.\(fontNameSanitize).rawValue, size: size)
}
"""
}
// UIKit
if isStatic {
return """
static let \(fontNameSanitize): ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.\(fontNameSanitize).rawValue, size: size)!
}
"""
}
return """
func \(fontNameSanitize)(withSize size: CGFloat) -> UIFont {
UIFont(name: FontName.\(fontNameSanitize).rawValue, size: size)!
}
"""
}
var staticProperty: String {
"""
static let \(fontNameSanitize): ((_ size: CGFloat) -> UIFont) = { size in
UIFont(name: FontName.\(fontNameSanitize).rawValue, size: size)!
}
"""
}
}

View File

@ -10,6 +10,7 @@ import Foundation
extension String {
func prependIfRelativePath(_ prependPath: String) -> String {
// If path starts with "/", it's an absolute path
if self.hasPrefix("/") {
return self
}

View File

@ -34,14 +34,18 @@ struct Generate: ParsableCommand {
// Parse
let configuration = ConfigurationFileParser.parse(options.configurationFile)
print("Found configurations :")
print(" - \(configuration.colors.count) colors configuration")
print(" - \(configuration.fonts.count) fonts configuration")
print(" - \(configuration.images.count) images configuration")
print(" - \(configuration.strings.count) strings configuration")
print(" - \(configuration.tags.count) tags configuration")
print(" - \(configuration.analytics.count) analytics configuration(s)")
print(" - \(configuration.colors.count) colors configuration(s)")
print(" - \(configuration.fonts.count) fonts configuration(s)")
print(" - \(configuration.images.count) images configuration(s)")
print(" - \(configuration.strings.count) strings configuration(s)")
print(" - \(configuration.tags.count) tags configuration(s)")
print()
print("Input file: \(configuration.colors.first?.inputFile ?? "no input file")")
if let architecture = configuration.architecture {
ArchitectureGenerator.writeArchitecture(architecture,
projectDirectory: options.projectDirectory)
}
// Execute commands
configuration.runnableConfigurations

View File

@ -9,22 +9,26 @@ import Foundation
enum GenerateError: Error {
case fileNotExists(String)
case invalidConfigurationFile(String)
case invalidConfigurationFile(String, String)
case commandError([String], String)
case writeFile(String, String)
var localizedDescription: String {
var description: String {
switch self {
case .fileNotExists(let filename):
return " error:[\(Generate.toolName)] File \(filename) does not exists"
return "error: [\(Generate.toolName)] File \(filename) does not exists"
case .invalidConfigurationFile(let filename):
return " error:[\(Generate.toolName)] File \(filename) is not a valid configuration file"
case .invalidConfigurationFile(let filename, let underneathErrorDescription):
return "error: [\(Generate.toolName)] File \(filename) is not a valid configuration file. Underneath error: \(underneathErrorDescription)"
case .commandError(let command, let 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)"
return "error: [\(Generate.toolName)] An error occured while running command '\(readableCommand)'. Command terminate with status code: \(terminationStatus)"
case .writeFile(let filename, let info):
return "error: [\(Generate.toolName)] An error occured while writing file in \(filename): \(info)"
}
}
}

View File

@ -5,8 +5,6 @@
// Created by Thibaut Schmitt on 30/08/2022.
//
import Foundation
import Foundation
import ArgumentParser

View File

@ -0,0 +1,39 @@
//
// ArchitectureGenerator.swift
//
//
// Created by Thibaut Schmitt on 18/11/2022.
//
import ToolCore
import Foundation
struct ArchitectureGenerator {
static func writeArchitecture(_ architecture: ConfigurationArchitecture, projectDirectory: String) {
// Create extension content
var architectureContent = [
"// Generated by ResgenSwift.\(Generate.toolName) \(ResgenSwiftVersion)",
architecture.getClass()
]
.joined(separator: "\n\n")
architectureContent += "\n"
let filename = "\(architecture.classname).swift"
guard let filePath = architecture.path?.prependIfRelativePath(projectDirectory) else {
let error = GenerateError.writeFile(filename, "Path of file is not defined.")
print(error.description)
Generate.exit(withError: error)
}
// Write content
let architectureFilePathURL = URL(fileURLWithPath: "\(filePath)/\(filename)")
do {
try architectureContent.write(to: architectureFilePathURL, atomically: false, encoding: .utf8)
} catch let error {
let error = GenerateError.writeFile(filename, error.localizedDescription)
print(error.description)
Generate.exit(withError: error)
}
}
}

View File

@ -8,6 +8,8 @@
import Foundation
struct ConfigurationFile: Codable, CustomDebugStringConvertible {
var architecture: ConfigurationArchitecture?
var analytics: [AnalyticsConfiguration]
var colors: [ColorsConfiguration]
var fonts: [FontsConfiguration]
var images: [ImagesConfiguration]
@ -15,12 +17,15 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible {
var tags: [TagsConfiguration]
var runnableConfigurations: [Runnable] {
let runnables: [[Runnable]] = [colors, fonts, images, strings, tags]
let runnables: [[Runnable]] = [analytics, colors, fonts, images, strings, tags]
return Array(runnables.joined())
}
var debugDescription: String {
"""
\(analytics)
-----------
-----------
\(colors)
-----------
-----------
@ -38,11 +43,46 @@ struct ConfigurationFile: Codable, CustomDebugStringConvertible {
}
}
struct ConfigurationArchitecture: Codable {
let property: String
let classname: String
let path: String?
let children: [ConfigurationArchitecture]?
func getProperty(isStatic: Bool) -> String {
" \(isStatic ? "static " : "")let \(property) = \(classname)()"
}
func getClass(generateStaticProperty: Bool = true) -> String {
guard children?.isEmpty == false else {
return "final class \(classname): Sendable {}"
}
let classDefinition = [
"class \(classname) {",
children?.map { $0.getProperty(isStatic: generateStaticProperty) }.joined(separator: "\n"),
"}"
]
.compactMap { $0 }
.joined(separator: "\n")
return [classDefinition, "", getSubclass()]
.compactMap { $0 }
.joined(separator: "\n")
}
func getSubclass() -> String? {
guard let children else { return nil }
return children.compactMap { arch in
arch.getClass(generateStaticProperty: false)
}
.joined(separator: "\n\n")
}
}
struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
struct AnalyticsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let style: String
let xcassetsPath: String
let target: String
let extensionOutputPath: String
let extensionName: String?
let extensionSuffix: String?
@ -55,6 +95,67 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
return false
}
internal init(inputFile: String,
target: String,
extensionOutputPath: String,
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
self.inputFile = inputFile
self.target = target
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers
}
var debugDescription: String {
"""
Analytics configuration:
- Input file: \(inputFile)
- Target: \(target)
- Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-")
- Extension suffix: \(extensionSuffix ?? "-")
"""
}
}
struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let style: String
let xcassetsPath: String
let extensionOutputPath: String
let extensionName: String?
let extensionNameUIKit: String?
let extensionSuffix: String?
private let staticMembers: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
return staticMembers
}
return false
}
internal init(inputFile: String,
style: String,
xcassetsPath: String,
extensionOutputPath: String,
extensionName: String?,
extensionNameUIKit: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
self.inputFile = inputFile
self.style = style
self.xcassetsPath = xcassetsPath
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
self.extensionNameUIKit = extensionNameUIKit
self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers
}
var debugDescription: String {
"""
Colors configuration:
@ -63,6 +164,7 @@ struct ColorsConfiguration: Codable, CustomDebugStringConvertible {
- Xcassets path: \(xcassetsPath)
- Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-")
- Extension name UIKit: \(extensionNameUIKit ?? "-")
- Extension suffix: \(extensionSuffix ?? "-")
"""
}
@ -72,7 +174,9 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
let inputFile: String
let extensionOutputPath: String
let extensionName: String?
let extensionNameUIKit: String?
let extensionSuffix: String?
let infoPlistPaths: String?
private let staticMembers: Bool?
var staticMembersOptions: Bool {
@ -82,13 +186,31 @@ struct FontsConfiguration: Codable, CustomDebugStringConvertible {
return false
}
internal init(inputFile: String,
extensionOutputPath: String,
extensionName: String?,
extensionNameUIKit: String?,
extensionSuffix: String?,
infoPlistPaths: String?,
staticMembers: Bool?) {
self.inputFile = inputFile
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
self.extensionNameUIKit = extensionNameUIKit
self.extensionSuffix = extensionSuffix
self.infoPlistPaths = infoPlistPaths
self.staticMembers = staticMembers
}
var debugDescription: String {
"""
Fonts configuration:
- Input file: \(inputFile)
- Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-")
- Extension name UIKit: \(extensionNameUIKit ?? "-")
- Extension suffix: \(extensionSuffix ?? "-")
- InfoPlistPaths: \(infoPlistPaths ?? "-")
"""
}
}
@ -98,6 +220,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
let xcassetsPath: String
let extensionOutputPath: String
let extensionName: String?
let extensionNameUIKit: String?
let extensionSuffix: String?
private let staticMembers: Bool?
@ -108,6 +231,22 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
return false
}
internal init(inputFile: String,
xcassetsPath: String,
extensionOutputPath: String,
extensionName: String?,
extensionNameUIKit: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
self.inputFile = inputFile
self.xcassetsPath = xcassetsPath
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
self.extensionNameUIKit = extensionNameUIKit
self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers
}
var debugDescription: String {
"""
Images configuration:
@ -115,6 +254,7 @@ struct ImagesConfiguration: Codable, CustomDebugStringConvertible {
- Xcassets path: \(xcassetsPath)
- Extension output path: \(extensionOutputPath)
- Extension name: \(extensionName ?? "-")
- Extension name UIKit: \(extensionNameUIKit ?? "-")
- Extension suffix: \(extensionSuffix ?? "-")
"""
}
@ -129,13 +269,41 @@ struct StringsConfiguration: Codable, CustomDebugStringConvertible {
let extensionName: String?
let extensionSuffix: String?
private let staticMembers: Bool?
private let xcStrings: Bool?
var staticMembersOptions: Bool {
if let staticMembers = staticMembers {
return staticMembers
}
return false
}
var xcStringsOptions: Bool {
if let xcStrings = xcStrings {
return xcStrings
}
return false
}
internal init(inputFile: String,
outputPath: String,
langs: String,
defaultLang: String,
extensionOutputPath: String,
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?,
xcStrings: Bool?) {
self.inputFile = inputFile
self.outputPath = outputPath
self.langs = langs
self.defaultLang = defaultLang
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers
self.xcStrings = xcStrings
}
var debugDescription: String {
"""
@ -166,6 +334,20 @@ struct TagsConfiguration: Codable, CustomDebugStringConvertible {
return false
}
internal init(inputFile: String,
lang: String,
extensionOutputPath: String,
extensionName: String?,
extensionSuffix: String?,
staticMembers: Bool?) {
self.inputFile = inputFile
self.lang = lang
self.extensionOutputPath = extensionOutputPath
self.extensionName = extensionName
self.extensionSuffix = extensionSuffix
self.staticMembers = staticMembers
}
var debugDescription: String {
"""
Tags configuration:
@ -177,4 +359,3 @@ struct TagsConfiguration: Codable, CustomDebugStringConvertible {
"""
}
}

View File

@ -12,16 +12,19 @@ class ConfigurationFileParser {
static func parse(_ configurationFile: String) -> ConfigurationFile {
guard let data = FileManager().contents(atPath: configurationFile) else {
let error = GenerateError.fileNotExists(configurationFile)
print(error.localizedDescription)
print(error.description)
Generate.exit(withError: error)
}
guard let configuration = try? YAMLDecoder().decode(ConfigurationFile.self, from: data) else {
let error = GenerateError.invalidConfigurationFile(configurationFile)
print(error.localizedDescription)
do {
return try YAMLDecoder().decode(ConfigurationFile.self, from: data)
} catch {
let error = GenerateError.invalidConfigurationFile(
configurationFile,
error.localizedDescription.description
)
print(error.description)
Generate.exit(withError: error)
}
return configuration
}
}

View File

@ -1,13 +1,13 @@
//
// FontsConfiguration+Runnable.swift
// AnalyticsConfiguration+Runnable.swift
//
//
// Created by Thibaut Schmitt on 30/08/2022.
// Created by Loris Perret on 08/12/2023.
//
import Foundation
extension FontsConfiguration: Runnable {
extension AnalyticsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
var args = [String]()
@ -17,6 +17,8 @@ extension FontsConfiguration: Runnable {
args += [
inputFile.prependIfRelativePath(projectDirectory),
"--target",
target,
"--extension-output-path",
extensionOutputPath.prependIfRelativePath(projectDirectory),
"--static-members",
@ -29,7 +31,6 @@ extension FontsConfiguration: Runnable {
extensionName
]
}
if let extensionSuffix = extensionSuffix {
args += [
"--extension-suffix",
@ -37,6 +38,6 @@ extension FontsConfiguration: Runnable {
]
}
Fonts.main(args)
Analytics.main(args)
}
}

View File

@ -9,6 +9,11 @@ import Foundation
extension ColorsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
let args = getArguments(projectDirectory: projectDirectory, force: force)
Colors.main(args)
}
func getArguments(projectDirectory: String, force: Bool) -> [String] {
var args = [String]()
if force {
@ -33,6 +38,12 @@ extension ColorsConfiguration: Runnable {
extensionName
]
}
if let extensionNameUIKit = extensionNameUIKit {
args += [
"--extension-name-ui-kit",
extensionNameUIKit
]
}
if let extensionSuffix = extensionSuffix {
args += [
"--extension-suffix",
@ -40,6 +51,6 @@ extension ColorsConfiguration: Runnable {
]
}
Colors.main(args)
return args
}
}

View File

@ -0,0 +1,65 @@
//
// FontsConfiguration+Runnable.swift
//
//
// Created by Thibaut Schmitt on 30/08/2022.
//
import Foundation
extension FontsConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
let args = getArguments(projectDirectory: projectDirectory, force: force)
Fonts.main(args)
}
func getArguments(projectDirectory: String, force: Bool) -> [String] {
var args = [String]()
if force {
args += ["-f"]
}
args += [
inputFile.prependIfRelativePath(projectDirectory),
"--extension-output-path",
extensionOutputPath.prependIfRelativePath(projectDirectory),
"--static-members",
"\(staticMembersOptions)"
]
if let extensionName = extensionName {
args += [
"--extension-name",
extensionName
]
}
if let extensionNameUIKit = extensionNameUIKit {
args += [
"--extension-name-ui-kit",
extensionNameUIKit
]
}
if let extensionSuffix = extensionSuffix {
args += [
"--extension-suffix",
extensionSuffix
]
}
if let infoPlistPaths = infoPlistPaths {
let adjustedPlistPaths = infoPlistPaths
.split(separator: " ")
.map { String($0).prependIfRelativePath(projectDirectory) }
.joined(separator: " ")
args += [
"--info-plist-paths",
adjustedPlistPaths
]
}
return args
}
}

View File

@ -9,6 +9,11 @@ import Foundation
extension ImagesConfiguration: Runnable {
func run(projectDirectory: String, force: Bool) {
let args = getArguments(projectDirectory: projectDirectory, force: force)
Images.main(args)
}
func getArguments(projectDirectory: String, force: Bool) -> [String] {
var args = [String]()
if force {
@ -31,6 +36,12 @@ extension ImagesConfiguration: Runnable {
extensionName
]
}
if let extensionNameUIKit = extensionNameUIKit {
args += [
"--extension-name-ui-kit",
extensionNameUIKit
]
}
if let extensionSuffix = extensionSuffix {
args += [
"--extension-suffix",
@ -38,6 +49,6 @@ extension ImagesConfiguration: Runnable {
]
}
Images.main(args)
return args
}
}

View File

@ -10,4 +10,3 @@ import Foundation
protocol Runnable {
func run(projectDirectory: String, force: Bool)
}

View File

@ -14,7 +14,7 @@ extension StringsConfiguration: Runnable {
if force {
args += ["-f"]
}
args += [
inputFile.prependIfRelativePath(projectDirectory),
"--output-path",
@ -26,7 +26,9 @@ extension StringsConfiguration: Runnable {
"--extension-output-path",
extensionOutputPath.prependIfRelativePath(projectDirectory),
"--static-members",
"\(staticMembersOptions)"
"\(staticMembersOptions)",
"--xc-strings",
"\(xcStringsOptions)"
]
if let extensionName = extensionName {

View File

@ -12,19 +12,19 @@ extension FileManager {
var files = [String]()
guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
let error = ImagesError.unknown("Cannot enumerate file in \(directory)")
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isRegularFileKey])
let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if fileAttributes.isRegularFile! {
files.append(fileURL.relativePath)
}
} catch {
let error = ImagesError.getFileAttributed(fileURL.relativePath, error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
}
@ -35,19 +35,19 @@ extension FileManager {
var files = [String]()
guard let enumerator = self.enumerator(at: URL(string: directory)!, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
let error = ImagesError.unknown("Cannot enumerate imageset directory in \(directory)")
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys:[.isDirectoryKey])
let fileAttributes = try fileURL.resourceValues(forKeys: [.isDirectoryKey])
if fileAttributes.isDirectory! && fileURL.lastPathComponent.hasSuffix(".imageset") {
files.append(fileURL.lastPathComponent)
}
} catch {
let error = ImagesError.getFileAttributed(fileURL.relativePath, error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
}

View File

@ -10,68 +10,68 @@ import Foundation
class ImageExtensionGenerator {
// MARK: - pragm
// MARK: - UIKit
static func generateExtensionFile(images: [ParsedImage],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionFilePath: String) {
extensionFilePath: String,
isSwiftUI: Bool) {
// Create extension conten1t
let extensionContent = Self.getExtensionContent(images: images,
staticVar: staticVar,
extensionName: extensionName,
inputFilename: inputFilename)
inputFilename: inputFilename,
isSwiftUI: isSwiftUI)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
} catch let error {
let error = ImagesError.writeFile(extensionFilePath, error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
}
// MARK: - Extension content
static func getExtensionContent(images: [ParsedImage], staticVar: Bool, extensionName: String, inputFilename: String) -> String {
static func getExtensionContent(images: [ParsedImage],
staticVar: Bool,
extensionName: String,
inputFilename: String,
isSwiftUI: Bool) -> String {
[
Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName),
Self.getProperties(images: images, staticVar: staticVar),
Self.getHeader(inputFilename: inputFilename, extensionClassname: extensionName, isSwiftUI: isSwiftUI),
Self.getProperties(images: images, staticVar: staticVar, isSwiftUI: isSwiftUI),
Self.getFooter()
]
.joined(separator: "\n")
}
// MARK: - Extension part
private static func getHeader(inputFilename: String, extensionClassname: String) -> String {
private static func getHeader(inputFilename: String,
extensionClassname: String,
isSwiftUI: Bool) -> String {
"""
// Generated by ResgenSwift.\(Images.toolName) \(ResgenSwiftVersion)
// Images from \(inputFilename)
import UIKit
import \(isSwiftUI ? "SwiftUI" : "UIKit")
extension \(extensionClassname) {
"""
}
private static func getProperties(images: [ParsedImage], staticVar: Bool) -> String {
if staticVar {
return images
.map { "\n\($0.getStaticImageProperty())" }
.joined(separator: "\n")
}
return images
.map { "\n\($0.getImageProperty())" }
private static func getProperties(images: [ParsedImage], staticVar: Bool, isSwiftUI: Bool) -> String {
images
.map { "\n\($0.getImageProperty(isStatic: staticVar, isSwiftUI: isSwiftUI))" }
.joined(separator: "\n")
}
private static func getFooter() -> String {
"""
}
"""
}
}

View File

@ -8,10 +8,13 @@
import Foundation
import ToolCore
enum OutputImageExtension: String {
case png
case svg
}
class XcassetsGenerator {
static let outputImageExtension = "png"
let forceGeneration: Bool
// MARK: - Init
@ -48,7 +51,7 @@ class XcassetsGenerator {
}
}
let error = ImagesError.unknownImageExtension(parsedImage.name)
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}()
@ -60,13 +63,20 @@ class XcassetsGenerator {
generatedAssetsPaths.append(imagesetName)
// Generate output images path
let output1x = "\(imagesetPath)/\(parsedImage.name).\(XcassetsGenerator.outputImageExtension)"
let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(XcassetsGenerator.outputImageExtension)"
let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(XcassetsGenerator.outputImageExtension)"
let output1x = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.png.rawValue)"
let output2x = "\(imagesetPath)/\(parsedImage.name)@2x.\(OutputImageExtension.png.rawValue)"
let output3x = "\(imagesetPath)/\(parsedImage.name)@3x.\(OutputImageExtension.png.rawValue)"
// Check if we need to convert image
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x) else {
//print("\(parsedImage.name) -> Not regenerating")
var needToGenerateForSvg = false
if imageData.ext == "svg" && !parsedImage.imageExtensions.contains(.png) {
needToGenerateForSvg = true
}
guard self.shouldGenerate(inputImagePath: imageData.path, xcassetImagePath: output1x, needToGenerateForSvg: needToGenerateForSvg) else {
print("\(parsedImage.name) -> Not regenerating")
return
}
@ -77,32 +87,58 @@ class XcassetsGenerator {
withIntermediateDirectories: true)
} catch {
let error = ImagesError.createAssetFolder(imagesetPath)
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
} else {
do {
let documentsDirectory = try fileManager.contentsOfDirectory(atPath: imagesetPath)
for filePath in documentsDirectory {
try fileManager.removeItem(atPath: "\(imagesetPath)/\(filePath)")
}
} catch {
print("Error deleting previous assets")
}
}
// Convert image
let convertArguments = parsedImage.convertArguments
if imageData.ext == "svg" {
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png
var command1x = ["\(svgConverter)", "\(imageData.path)"]
var command2x = ["\(svgConverter)", "\(imageData.path)"]
var command3x = ["\(svgConverter)", "\(imageData.path)"]
self.addConvertArgument(command: &command1x, convertArgument: convertArguments.x1)
self.addConvertArgument(command: &command2x, convertArgument: convertArguments.x2)
self.addConvertArgument(command: &command3x, convertArgument: convertArguments.x3)
command1x.append(contentsOf: ["-o", output1x])
command2x.append(contentsOf: ["-o", output2x])
command3x.append(contentsOf: ["-o", output3x])
Shell.shell(command1x)
Shell.shell(command2x)
Shell.shell(command3x)
if parsedImage.imageExtensions.contains(.png) {
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -h 300 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -w 200 -o path/to/output.png
// /usr/local/bin/rsvg-convert path/to/image.png -h 300 -o path/to/output.png
var command1x = ["\(svgConverter)", "\(imageData.path)"]
var command2x = ["\(svgConverter)", "\(imageData.path)"]
var command3x = ["\(svgConverter)", "\(imageData.path)"]
self.addConvertArgument(command: &command1x, convertArgument: convertArguments.x1)
self.addConvertArgument(command: &command2x, convertArgument: convertArguments.x2)
self.addConvertArgument(command: &command3x, convertArgument: convertArguments.x3)
command1x.append(contentsOf: ["-o", output1x])
command2x.append(contentsOf: ["-o", output2x])
command3x.append(contentsOf: ["-o", output3x])
Shell.shell(command1x)
Shell.shell(command2x)
Shell.shell(command3x)
} else {
let output = "\(imagesetPath)/\(parsedImage.name).\(OutputImageExtension.svg.rawValue)"
let tempURL = URL(fileURLWithPath: output)
do {
if FileManager.default.fileExists(atPath: tempURL.path) {
try FileManager.default.removeItem(atPath: tempURL.path)
}
try FileManager.default.copyItem(atPath: imageData.path, toPath: tempURL.path)
} catch {
print(error.localizedDescription)
}
}
} else {
// convert path/to/image.png -resize 200x300 path/to/output.png
// convert path/to/image.png -resize 200x path/to/output.png
@ -119,7 +155,7 @@ class XcassetsGenerator {
}
// Write Content.json
let imagesetContentJson = parsedImage.contentJson
guard let imagesetContentJson = parsedImage.generateContentJson(isVector: imageData.ext == "svg") else { return }
let contentJsonFilePath = "\(imagesetPath)/Contents.json"
let contentJsonFilePathURL = URL(fileURLWithPath: contentJsonFilePath)
@ -161,8 +197,8 @@ class XcassetsGenerator {
// MARK: - Helpers: bypass generation
private func shouldGenerate(inputImagePath: String, xcassetImagePath: String) -> Bool {
if forceGeneration {
private func shouldGenerate(inputImagePath: String, xcassetImagePath: String, needToGenerateForSvg: Bool) -> Bool {
if forceGeneration || needToGenerateForSvg {
return true
}

View File

@ -21,7 +21,8 @@ struct Images: ParsableCommand {
// MARK: - Static
static let toolName = "Images"
static let defaultExtensionName = "UIImage"
static let defaultExtensionName = "Image"
static let defaultExtensionNameUIKit = "UIImage"
// MARK: - Command Options
@ -56,8 +57,15 @@ struct Images: ParsableCommand {
staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
extensionFilePath: options.extensionFilePath,
isSwiftUI: true)
ImageExtensionGenerator.generateExtensionFile(images: imagesToGenerate,
staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionNameUIKit,
extensionFilePath: options.extensionFilePathUIKit,
isSwiftUI: false)
print("[\(Self.toolName)] Images generated")
}
@ -74,13 +82,20 @@ struct Images: ParsableCommand {
// Input file
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = ImagesError.fileNotExists(options.inputFile)
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
// RSVG-Converter
_ = Images.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)
}
// Check if needed to regenerate
guard GeneratorChecker.shouldGenerate(force: options.forceExecution,
inputFilePath: options.inputFile,
@ -102,7 +117,7 @@ struct Images: ParsableCommand {
}
let error = ImagesError.rsvgConvertNotFound
print(error.localizedDescription)
print(error.description)
Images.exit(withError: error)
}
}

View File

@ -8,6 +8,7 @@
import Foundation
enum ImagesError: Error {
case extensionNamesCollision(String)
case inputFolderNotFound(String)
case fileNotExists(String)
case unknownImageExtension(String)
@ -17,31 +18,34 @@ enum ImagesError: Error {
case createAssetFolder(String)
case unknown(String)
var localizedDescription: String {
var description: String {
switch self {
case .extensionNamesCollision(let extensionName):
return "error: [\(Fonts.toolName)] Error on extension names, extension name and SwiftUI extension name should be different (\(extensionName) is used on both)"
case .inputFolderNotFound(let inputFolder):
return " error:[\(Images.toolName)] Input folder not found: \(inputFolder)"
return "error: [\(Images.toolName)] Input folder not found: \(inputFolder)"
case .fileNotExists(let filename):
return " error:[\(Images.toolName)] File \(filename) does not exists"
return "error: [\(Images.toolName)] File \(filename) does not exists"
case .unknownImageExtension(let filename):
return " error:[\(Images.toolName)] File \(filename) have an unhandled file extension. Cannot generate image."
return "error: [\(Images.toolName)] File \(filename) have an unhandled file extension. Cannot generate image."
case .getFileAttributed(let filename, let errorDescription):
return " error:[\(Images.toolName)] Getting file attributes of \(filename) failed with error: \(errorDescription)"
return "error: [\(Images.toolName)] Getting file attributes of \(filename) failed with error: \(errorDescription)"
case .rsvgConvertNotFound:
return " error:[\(Images.toolName)] Can't find rsvg-convert (can be installed with 'brew remove imagemagick && brew install imagemagick --with-librsvg')"
return "error: [\(Images.toolName)] Can't find rsvg-convert (can be installed with 'brew remove imagemagick && brew install librsvg')"
case .writeFile(let subErrorDescription, let filename):
return " error:[\(Images.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
return "error: [\(Images.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
case .createAssetFolder(let folder):
return "error:[\(Colors.toolName)] An error occured while creating folder `\(folder)`"
return "error: [\(Colors.toolName)] An error occured while creating folder `\(folder)`"
case .unknown(let errorDescription):
return " error:[\(Images.toolName)] Unknown error: \(errorDescription)"
return "error: [\(Images.toolName)] Unknown error: \(errorDescription)"
}
}
}

View File

@ -27,9 +27,12 @@ struct ImagesOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an UIImage extension. Using default extension name will generate static property.")
@Option(help: "Extension name. If not specified, it will generate an Image extension.")
var extensionName: String = Images.defaultExtensionName
@Option(help: "Extension name. If not specified, it will generate an UIImage extension.")
var extensionNameUIKit: String = Images.defaultExtensionNameUIKit
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+Image{extensionSuffix}.swift")
var extensionSuffix: String?
}
@ -37,6 +40,9 @@ struct ImagesOptions: ParsableArguments {
// MARK: - Computed var
extension ImagesOptions {
// MARK: - SwiftUI
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
@ -48,6 +54,21 @@ extension ImagesOptions {
"\(extensionOutputPath)/\(extensionFileName)"
}
// MARK: - UIKit
var extensionFileNameUIKit: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionNameUIKit)+\(extensionSuffix).swift"
}
return "\(extensionNameUIKit).swift"
}
var extensionFilePathUIKit: String {
"\(extensionOutputPath)/\(extensionFileNameUIKit)"
}
// MARK: -
var inputFilenameWithoutExt: String {
URL(fileURLWithPath: inputFile)
.deletingPathExtension()

View File

@ -0,0 +1,76 @@
//
// ImageContent.swift
//
//
// Created by Quentin Bandera on 19/04/2024.
//
import Foundation
enum TemplateRenderingIntent: String, Codable {
case template
case original
}
struct AssetContent: Codable, Equatable {
let images: [AssetImageDescription]
let info: AssetInfo
let properties: AssetProperties?
init(
images: [AssetImageDescription],
info: AssetInfo,
properties: AssetProperties? = nil
) {
self.images = images
self.info = info
self.properties = properties
}
static func == (lhs: AssetContent, rhs: AssetContent) -> 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 })
return lhsImages == rhsImages
}
}
struct AssetImageDescription: Codable, Equatable {
let idiom: String
let scale: String?
let filename: String
init(
idiom: String,
scale: String? = nil,
filename: String
) {
self.idiom = idiom
self.scale = scale
self.filename = filename
}
}
struct AssetInfo: Codable, Equatable {
let version: Int
let author: String
}
struct AssetProperties: Codable, Equatable {
let preservesVectorRepresentation: Bool
let templateRenderingIntent: TemplateRenderingIntent?
init(
preservesVectorRepresentation: Bool,
templateRenderingIntent: TemplateRenderingIntent? = nil
) {
self.preservesVectorRepresentation = preservesVectorRepresentation
self.templateRenderingIntent = templateRenderingIntent
}
enum CodingKeys: String, CodingKey {
case preservesVectorRepresentation = "preserves-vector-representation"
case templateRenderingIntent = "template-rendering-intent"
}
}

View File

@ -7,12 +7,31 @@
import Foundation
enum ImageExtension: String {
case png
}
struct ParsedImage {
let name: String
let tags: String
let width: Int
let height: Int
let imageExtensions: [ImageExtension]
init(
name: String,
tags: String,
width: Int,
height: Int,
imageExtensions: [ImageExtension] = []
) {
self.name = name
self.tags = tags
self.width = width
self.height = height
self.imageExtensions = imageExtensions
}
// MARK: - Convert
var convertArguments: (x1: ConvertArgument, x2: ConvertArgument, x3: ConvertArgument) {
@ -42,47 +61,81 @@ struct ParsedImage {
// MARK: - Assets
var contentJson: String {
"""
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x",
"filename" : "\(name).\(XcassetsGenerator.outputImageExtension)"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "\(name)@2x.\(XcassetsGenerator.outputImageExtension)"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "\(name)@3x.\(XcassetsGenerator.outputImageExtension)"
}
],
"info" : {
"version" : 1,
"author" : "ResgenSwift-Imagium"
}
func generateContentJson(isVector: Bool) -> String? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let imageContent = generateImageContent(isVector: isVector)
guard let data = try? encoder.encode(imageContent) else {
let error = ImagesError.writeFile("Contents.json", "Error encoding json file")
print(error.description)
Images.exit(withError: error)
}
"""
return String(decoding: data, as: UTF8.self)
}
func generateImageContent(isVector: Bool) -> AssetContent {
if !imageExtensions.contains(.png) && isVector {
//Generate svg
return AssetContent(
images: [
AssetImageDescription(
idiom: "universal",
filename: "\(name).\(OutputImageExtension.svg.rawValue)"
)
],
info: AssetInfo(
version: 1,
author: "ResgenSwift-Imagium"
),
properties: AssetProperties(
preservesVectorRepresentation: true,
templateRenderingIntent: .original
)
)
} else {
//Generate png
return AssetContent(
images: [
AssetImageDescription(
idiom: "universal",
scale: "1x",
filename: "\(name).\(OutputImageExtension.png.rawValue)"
),
AssetImageDescription(
idiom: "universal",
scale: "2x",
filename: "\(name)@2x.\(OutputImageExtension.png.rawValue)"
),
AssetImageDescription(
idiom: "universal",
scale: "3x",
filename: "\(name)@3x.\(OutputImageExtension.png.rawValue)"
)
],
info: AssetInfo(
version: 1,
author: "ResgenSwift-Imagium"
)
)
}
}
// MARK: - Extension property
func getImageProperty() -> String {
"""
var \(name): UIImage {
UIImage(named: "\(name)")!
}
"""
}
func getStaticImageProperty() -> String {
"""
static var \(name): UIImage {
func getImageProperty(isStatic: Bool, isSwiftUI: Bool) -> String {
if isSwiftUI {
return """
\(isStatic ? "static ": "")var \(name): Image {
Image("\(name)")
}
"""
}
return """
\(isStatic ? "static ": "")var \(name): UIImage {
UIImage(named: "\(name)")!
}
"""

View File

@ -38,11 +38,21 @@ class ImageFileParser {
}
return Int(splittedLine[3])!
}()
let image = ParsedImage(name: String(splittedLine[1]), tags: String(splittedLine[0]), width: width, height: height)
var imageExtensions: [ImageExtension] = []
splittedLine.forEach { stringExtension in
if let imageExtension = ImageExtension(rawValue: String(stringExtension)) {
imageExtensions.append(imageExtension)
}
}
let image = ParsedImage(name: String(splittedLine[1]), tags: String(splittedLine[0]), width: width, height: height, imageExtensions: imageExtensions)
imagesToGenerate.append(image)
}
print(imagesToGenerate)
return imagesToGenerate.filter {
$0.tags.contains(platform.rawValue)
}

View File

@ -1,6 +1,6 @@
//
// StringsFileGenerator.swift
//
//
//
// Created by Thibaut Schmitt on 04/01/2022.
//
@ -9,10 +9,16 @@ import Foundation
import ToolCore
class StringsFileGenerator {
// MARK: - Strings Files
static func writeStringsFiles(sections: [Section], langs: [String], defaultLang: String, tags: [String], outputPath: String, inputFilenameWithoutExt: String) {
static func writeStringsFiles(sections: [Section],
langs: [String],
defaultLang: String,
tags: [String],
outputPath: String,
inputFilenameWithoutExt: String) {
var stringsFilesContent = [String: String]()
for lang in langs {
stringsFilesContent[lang] = Self.generateStringsFileContent(lang: lang,
@ -20,24 +26,52 @@ class StringsFileGenerator {
tags: tags,
sections: sections)
}
// Write strings file content
langs.forEach { lang in
guard let fileContent = stringsFilesContent[lang] else { return }
let stringsFilePath = "\(outputPath)/\(lang).lproj/\(inputFilenameWithoutExt).strings"
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
do {
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
} catch let error {
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
}
}
static func generateStringsFileContent(lang: String, defaultLang: String, tags inputTags: [String], sections: [Section]) -> String {
static func writeXcStringsFiles(sections: [Section],
langs: [String],
defaultLang: String,
tags: [String],
outputPath: String,
inputFilenameWithoutExt: String) {
let fileContent: String = Self.generateXcStringsFileContent(
langs: langs,
defaultLang: defaultLang,
tags: tags,
sections: sections
)
let stringsFilePath = "\(outputPath)/\(inputFilenameWithoutExt).xcstrings"
let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
do {
try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
} catch let error {
let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
print(error.description)
Stringium.exit(withError: error)
}
}
static func generateStringsFileContent(lang: String,
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> String {
var stringsFileContent = """
/**
* Apple Strings File
@ -45,13 +79,13 @@ class StringsFileGenerator {
* Language: \(lang)
*/\n
"""
sections.forEach { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
return // Go to next section
}
stringsFileContent += "\n/********** \(section.name) **********/\n\n"
section.definitions.forEach { definition in
var skipDefinition = false // Set to true if not matching tag
@ -61,89 +95,257 @@ class StringsFileGenerator {
skipDefinition = true
return nil
}
// If tags contains `noTranslationTag` => get default lang
if definition.tags.contains(Stringium.noTranslationTag) {
return definition.translations[defaultLang]
}
// Else: get specific lang
return definition.translations[lang]
}()
if let translation = translationOpt {
stringsFileContent += "\"\(definition.name)\" = \"\(translation)\";\n\n"
} else if skipDefinition == false {
let error = StringiumError.langNotDefined(lang, definition.name, definition.reference != nil)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
}
}
return stringsFileContent
}
// MARK: - XcStrings Generation
static func generateXcStringsFileContent(langs: [String],
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> String {
let rootObject = generateRootObject(langs: langs, defaultLang: defaultLang, tags: inputTags, sections: sections)
let file = generateXcStringsFileContentFromRootObject(rootObject: rootObject)
return file
}
static func generateXcStringsFileContentFromRootObject(rootObject: Root) -> String {
do {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted]
let json = try encoder.encode(rootObject)
return String(decoding: json, as: UTF8.self)
} catch {
debugPrint("Failed to encode: \(error)")
}
return ""
}
static func generateRootObject(langs: [String],
defaultLang: String,
tags inputTags: [String],
sections: [Section]) -> Root {
var xcStringDefinitionTab: [XCStringDefinition] = []
sections.forEach { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
return // Go to next section
}
section.definitions.forEach { definition in
var skipDefinition = false
var isNoTranslation = false
var localizationTab: [XCStringLocalization] = []
if definition.hasOneOrMoreMatchingTags(inputTags: inputTags) == false {
skipDefinition = true
}
if definition.tags.contains(Stringium.noTranslationTag) {
isNoTranslation = true
}
if !skipDefinition {
if isNoTranslation {
// Search for langs in yaml
for lang in langs {
if let value = definition.translations[defaultLang], !value.isEmpty {
let localization = XCStringLocalization(
lang: lang,
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(state: "translated", value: value)
)
)
localizationTab.append(localization)
}
}
} else {
// Search for langs in twine
for (lang, value) in definition.translations where !value.isEmpty {
let localization = XCStringLocalization(
lang: lang,
content: XCStringLocalizationLangContent(
stringUnit: DefaultStringUnit(state: "translated", value: value)
)
)
localizationTab.append(localization)
}
}
let xcStringDefinition = XCStringDefinition(
title: definition.name,
content: XCStringDefinitionContent(
comment: definition.comment,
extractionState: "manual",
localizations: XCStringLocalizationContainer(
localizations: localizationTab
)
)
)
xcStringDefinitionTab.append(xcStringDefinition)
}
}
}
let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab)
return Root(
sourceLanguage: defaultLang,
strings: xcStringContainer,
version: "1.0"
)
}
// MARK: - Extension file
static func writeExtensionFiles(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool, inputFilename: String, extensionName: String, extensionFilePath: String) {
static func writeExtensionFiles(sections: [Section],
defaultLang lang: String,
tags: [String],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionFilePath: String,
extensionSuffix: String) {
// Get extension content
let extensionFileContent = Self.getExtensionContent(sections: sections,
defaultLang: lang,
tags: tags,
staticVar: staticVar,
inputFilename: inputFilename,
extensionName: extensionName)
extensionName: extensionName,
extensionSuffix: extensionSuffix)
// Write content
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
} catch let error {
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
}
// MARK: - Extension content
static func getExtensionContent(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool, inputFilename: String, extensionName: String) -> String {
static func getExtensionContent(sections: [Section],
defaultLang lang: String,
tags: [String],
staticVar: Bool,
inputFilename: String,
extensionName: String,
extensionSuffix: String) -> String {
[
Self.getHeader(stringsFilename: inputFilename, extensionClassname: extensionName),
Self.getEnumKey(sections: sections, tags: tags, extensionClassname: extensionName, extensionSuffix: extensionSuffix),
Self.getProperties(sections: sections, defaultLang: lang, tags: tags, staticVar: staticVar),
Self.getFooter()
]
.joined(separator: "\n")
}
// MARK: - Extension part
private static func getHeader(stringsFilename: String, extensionClassname: String) -> String {
"""
// Generated by ResgenSwift.Strings.\(Stringium.toolName) \(ResgenSwiftVersion)
import UIKit
fileprivate let kStringsFileName = "\(stringsFilename)"
extension \(extensionClassname) {
"""
}
private static func getEnumKey(sections: [Section], tags: [String], extensionClassname: String, extensionSuffix: String) -> String {
var enumDefinition = "\n enum Key\(extensionSuffix.uppercasedFirst()): String {\n"
// Enum
sections.forEach { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
return // Go to next section
}
section.definitions.forEach { definition in
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
return // Go to next definition
}
debugPrint("Found definition")
enumDefinition += " case \(definition.name) = \"\(definition.name)\"\n"
}
}
// KeyPath accessors
enumDefinition += "\n"
enumDefinition += " var keyPath: KeyPath<\(extensionClassname), String> {\n"
enumDefinition += " switch self {\n"
sections.forEach { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
return // Go to next section
}
section.definitions.forEach { definition in
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
return // Go to next definition
}
debugPrint("Found definition")
enumDefinition += " case .\(definition.name): return \\\(extensionClassname).\(definition.name)\n"
}
}
enumDefinition += " }\n" // Switch
enumDefinition += " }\n" // var keyPath
enumDefinition += " }" // Enum
return enumDefinition
}
private static func getProperties(sections: [Section], defaultLang lang: String, tags: [String], staticVar: Bool) -> String {
sections.compactMap { section in
// Check that at least one string will be generated
guard section.hasOneOrMoreMatchingTags(tags: tags) else {
return nil // Go to next section
}
var res = "\n // MARK: - \(section.name)\n"
res += section.definitions.compactMap { definition in
guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
return nil // Go to next definition
}
if staticVar {
return "\n\(definition.getNSLocalizedStringStaticProperty(forLang: lang))"
}
@ -154,10 +356,11 @@ class StringsFileGenerator {
}
.joined(separator: "\n")
}
private static func getFooter() -> String {
"""
}
"""
}
}

View File

@ -22,9 +22,9 @@ class TagsGenerator {
let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
do {
try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
} catch (let error) {
} catch let error {
let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
}
@ -80,6 +80,7 @@ class TagsGenerator {
private static func getFooter() -> String {
"""
}
"""
}
}

View File

@ -37,7 +37,7 @@ class Definition {
}
func hasOneOrMoreMatchingTags(inputTags: [String]) -> Bool {
if Set(inputTags).intersection(Set(self.tags)).isEmpty {
if Set(inputTags).isDisjoint(with: tags) {
return false
}
return true
@ -84,22 +84,29 @@ class Definition {
return (inputParameters: inputParameters, translationArguments: translationArguments)
}
private func getBaseProperty(lang: String, translation: String, isStatic: Bool) -> String {
private func getBaseProperty(lang: String, translation: String, isStatic: Bool, comment: String?) -> String {
"""
/// Translation in \(lang) :
/// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
\(isStatic ? "static ": "")var \(name): String {
NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "")
NSLocalizedString("\(name)", tableName: kStringsFileName, bundle: Bundle.main, value: "\(translation)", comment: "\(comment ?? "")")
}
"""
}
private func getBaseMethod(lang: String, translation: String, isStatic: Bool, inputParameters: [String], translationArguments: [String]) -> String {
private func getBaseMethod(lang: String, translation: String, isStatic: Bool, inputParameters: [String], translationArguments: [String], comment: String?) -> String {
"""
/// Translation in \(lang) :
/// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
\(isStatic ? "static ": "")func \(name)(\(inputParameters.joined(separator: ", "))) -> String {
String(format: \(isStatic ? "Self" : "self").\(name), \(translationArguments.joined(separator: ", ")))
}
@ -109,13 +116,18 @@ class Definition {
func getNSLocalizedStringProperty(forLang lang: String) -> String {
guard let translation = translations[lang] else {
let error = StringiumError.langNotDefined(lang, name, reference != nil)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
// Generate property
let property = getBaseProperty(lang: lang, translation: translation, isStatic: false)
let property = getBaseProperty(
lang: lang,
translation: translation,
isStatic: false,
comment: self.comment
)
// Generate method
var method = ""
if let parameters = self.getStringParameters(input: translation) {
@ -123,7 +135,8 @@ class Definition {
translation: translation,
isStatic: false,
inputParameters: parameters.inputParameters,
translationArguments: parameters.translationArguments)
translationArguments: parameters.translationArguments,
comment: self.comment)
}
return property + method
@ -132,12 +145,17 @@ class Definition {
func getNSLocalizedStringStaticProperty(forLang lang: String) -> String {
guard let translation = translations[lang] else {
let error = StringiumError.langNotDefined(lang, name, reference != nil)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
// Generate property
let property = getBaseProperty(lang: lang, translation: translation, isStatic: true)
let property = getBaseProperty(
lang: lang,
translation: translation,
isStatic: true,
comment: self.comment
)
// Generate method
var method = ""
@ -146,7 +164,8 @@ class Definition {
translation: translation,
isStatic: true,
inputParameters: parameters.inputParameters,
translationArguments: parameters.translationArguments)
translationArguments: parameters.translationArguments,
comment: self.comment)
}
return property + method
@ -157,13 +176,17 @@ class Definition {
func getProperty(forLang lang: String) -> String {
guard let translation = translations[lang] else {
let error = StringiumError.langNotDefined(lang, name, reference != nil)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
return """
/// Translation in \(lang) :
/// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
var \(name): String {
"\(translation)"
}
@ -173,13 +196,16 @@ class Definition {
func getStaticProperty(forLang lang: String) -> String {
guard let translation = translations[lang] else {
let error = StringiumError.langNotDefined(lang, name, reference != nil)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
return """
/// Translation in \(lang) :
/// \(translation)
///
/// Comment :
/// \(comment?.isEmpty == false ? comment! : "No comment")
static var \(name): String {
"\(translation)"
}

View File

@ -0,0 +1,109 @@
//
// XcString.swift
//
//
// Created by Quentin Bandera on 12/04/2024.
//
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 {
var container = encoder.container(keyedBy: DynamicKey.self)
for str in strings {
if let codingKey = DynamicKey(stringValue: str.title) {
try container.encode(str.content, forKey: codingKey)
}
}
}
static func == (lhs: XCStringDefinitionContainer, rhs: XCStringDefinitionContainer) -> Bool {
return lhs.strings.sorted(by: {
$0.title < $1.title
}) == rhs.strings.sorted(by: { $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
init(comment: String? = nil, extractionState: String, localizations: XCStringLocalizationContainer) {
self.comment = comment
self.extractionState = extractionState
self.localizations = localizations
}
}
struct XCStringLocalizationContainer: Codable, Equatable {
let localizations: [XCStringLocalization]
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DynamicKey.self)
for loca in localizations {
if let codingKey = DynamicKey(stringValue: loca.lang) {
try container.encode(loca.content, forKey: codingKey)
}
}
}
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 })
}
}
struct XCStringLocalization: Codable, Equatable {
let lang: String // json key -> custom encoding method
let content: XCStringLocalizationLangContent
}
struct XCStringLocalizationLangContent: Codable, Equatable {
let stringUnit: DefaultStringUnit
}
//enum VarationOrStringUnit: Encodable {
// case variations([Varation])
// case stringUnit: (DefaultStringUnit)
//
// func encode(to encoder: any Encoder) throws {
// if let varations {
//
// } else if let {
//
// }
// }
//}
struct DefaultStringUnit: Codable, Equatable {
let state: String
let value: String
}

View File

@ -84,7 +84,7 @@ class TwineFileParser {
}
}
if invalidDefinitionNames.count > 0 {
print(" warning:[\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))")
print("warning: [\(Stringium.toolName)] Found \(invalidDefinitionNames.count) definition (\(invalidDefinitionNames.joined(separator: ", "))")
}
return sections

View File

@ -43,13 +43,26 @@ struct Stringium: ParsableCommand {
let sections = TwineFileParser.parse(options.inputFile)
// Generate strings files
StringsFileGenerator.writeStringsFiles(sections: sections,
langs: options.langs,
defaultLang: options.defaultLang,
tags: options.tags,
outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
print(options.xcStrings)
if !options.xcStrings {
print("[\(Self.toolName)] Will generate strings")
StringsFileGenerator.writeStringsFiles(sections: sections,
langs: options.langs,
defaultLang: options.defaultLang,
tags: options.tags,
outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
} else {
print("[\(Self.toolName)] Will generate xcStrings")
StringsFileGenerator.writeXcStringsFiles(sections: sections,
langs: options.langs,
defaultLang: options.defaultLang,
tags: options.tags,
outputPath: options.stringsFileOutputPath,
inputFilenameWithoutExt: options.inputFilenameWithoutExt)
}
// Generate extension
StringsFileGenerator.writeExtensionFiles(sections: sections,
defaultLang: options.defaultLang,
@ -57,8 +70,9 @@ struct Stringium: ParsableCommand {
staticVar: options.staticMembers,
inputFilename: options.inputFilenameWithoutExt,
extensionName: options.extensionName,
extensionFilePath: options.extensionFilePath)
extensionFilePath: options.extensionFilePath,
extensionSuffix: options.extensionSuffix)
print("[\(Self.toolName)] Strings generated")
}
@ -70,20 +84,20 @@ struct Stringium: ParsableCommand {
// Input file
guard fileManager.fileExists(atPath: options.inputFile) else {
let error = StringiumError.fileNotExists(options.inputFile)
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
// Langs
guard options.langs.isEmpty == false else {
let error = StringiumError.langsListEmpty
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}
guard options.langs.contains(options.defaultLang) else {
let error = StringiumError.defaultLangsNotInLangs
print(error.localizedDescription)
print(error.description)
Stringium.exit(withError: error)
}

View File

@ -14,25 +14,25 @@ enum StringiumError: Error {
case writeFile(String, String)
case langNotDefined(String, String, Bool)
var localizedDescription: String {
var description: String {
switch self {
case .fileNotExists(let filename):
return " error:[\(Stringium.toolName)] File \(filename) does not exists "
return "error: [\(Stringium.toolName)] File \(filename) does not exists "
case .langsListEmpty:
return " error:[\(Stringium.toolName)] Langs list is empty"
return "error: [\(Stringium.toolName)] Langs list is empty"
case .defaultLangsNotInLangs:
return " error:[\(Stringium.toolName)] Langs list does not contains the default lang"
return "error: [\(Stringium.toolName)] Langs list does not contains the default lang"
case .writeFile(let subErrorDescription, let filename):
return " error:[\(Stringium.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
return "error: [\(Stringium.toolName)] An error occured while writing content to \(filename): \(subErrorDescription)"
case .langNotDefined(let lang, let definitionName, let isReference):
if isReference {
return " error:[\(Stringium.toolName)] Reference are handled only by TwineTool. Please use it or remove reference from you strings file."
return "error: [\(Stringium.toolName)] Reference are handled only by Twine. Please use it or remove reference from you strings file."
}
return " error:[\(Stringium.toolName)] Lang \"\(lang)\" not found for \"\(definitionName)\""
return "error: [\(Stringium.toolName)] Lang \"\(lang)\" not found for \"\(definitionName)\""
}
}
}

View File

@ -11,8 +11,8 @@ import ArgumentParser
struct StringiumOptions: ParsableArguments {
@Flag(name: [.customShort("f"), .customShort("F")], help: "Should force generation")
var forceGeneration = false
@Argument(help: "Input files where strings ared defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
@Argument(help: "Input files where strings are defined.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
var inputFile: String
@Option(name: .customLong("output-path"), help: "Path where to strings file.", transform: { $0.replaceTiltWithHomeDirectoryPath() })
@ -32,12 +32,15 @@ struct StringiumOptions: ParsableArguments {
@Option(help: "Tell if it will generate static properties or not")
var staticMembers: Bool = false
@Option(help: "Extension name. If not specified, it will generate an String extension. Using default extension name will generate static property.")
@Option(help: "Tell if it will generate xcStrings file or not")
var xcStrings: Bool = false
@Option(help: "Extension name. If not specified, it will generate an String extension.")
var extensionName: String = Stringium.defaultExtensionName
@Option(help: "Extension suffix. Ex: MyApp, it will generate {extensionName}+{extensionSuffix}.swift")
var extensionSuffix: String?
@Option(help: "Extension suffix: {extensionName}+{extensionSuffix}.swift")
var extensionSuffix: String
}
// MARK: - Private var getter
@ -68,10 +71,7 @@ extension StringiumOptions {
extension StringiumOptions {
var extensionFileName: String {
if let extensionSuffix = extensionSuffix {
return "\(extensionName)+\(extensionSuffix).swift"
}
return "\(extensionName).swift"
"\(extensionName)+\(extensionSuffix).swift"
}
var extensionFilePath: String {

View File

@ -27,4 +27,3 @@ struct Strings: ParsableCommand {
}
//Strings.main()

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