diff --git a/.gitignore b/.gitignore index c94db42..4d51e0f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .idea/vcs.xml .idea/workspace.xml .idea/xcode.xml +regen.xcodeproj/project.xcworkspace/xcuserdata/idomizrachi.xcuserdatad diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4330c2b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +os: osx +osx_image: xcode10.2 +language: swift +script: + - xcodebuild -scheme regen clean build test +after_success: + - bash <(curl -s https://codecov.io/bash) -J 'regen' diff --git a/README.md b/README.md index 8850061..f0cc4e5 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,5 @@ To: `Localization.loginButton` - ## Credits\Thanks https://github.com/icodeforlove/Colors for the ANSI color library diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..84be136 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +coverage: + ignore: + - "regen/Dependencies/**/*" + - "regen-tests/**/*" diff --git a/regen-tests/ArgumentsParserTests.swift b/regen-tests/ArgumentsParserTests.swift new file mode 100644 index 0000000..1c4657e --- /dev/null +++ b/regen-tests/ArgumentsParserTests.swift @@ -0,0 +1,79 @@ +// +// ArgumentsParserTests.swift +// regen-tests +// +// Created by Ido Mizrachi on 12/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import XCTest + +class ArgumentsParserTests: XCTestCase { + + func testEmptyArguments() { + let argumentsParser = ArgumentsParser(arguments: []) + switch argumentsParser.operationType { + case .usage: break + default: XCTFail("Bad operation") + } + } +} + +class ArgumentsParserLocalizationTests: XCTestCase { + + func testLocalizationOperationDefaultArguments() { + let argumentsParser = ArgumentsParser(arguments: ["localization"]) + switch argumentsParser.operationType { + case .usage: break + default: XCTFail("Bad operation") + } + + } + + func testLocalizationOperationWithMissingSearchPath() { + let argumentsParser = ArgumentsParser(arguments: ["localization", "--template", "hello", "--output", "Gen.m"]) + switch argumentsParser.operationType { + case .usage: break + default: XCTFail("Bad operation") + } + } + +// func testLocalizationOperationWithMissingOutput() { +// let argumentsParser = ArgumentsParser(arguments: ["localization", "--search-path", "hello", "--template", "hello"]) +// switch argumentsParser.operationType { +// case .usage: break +// default: XCTFail("Bad operation") +// } +// } + + func testLocalizationOperationWithMissingTemplate() { + let argumentsParser = ArgumentsParser(arguments: ["localization", "--output", "hello", "--search-path", "hello"]) + switch argumentsParser.operationType { + case .usage: break + default: XCTFail("Bad operation") + } + } + + func testImagesOperationWithDefaultArguments() { + let argumentsParser = ArgumentsParser(arguments: ["images"]) + switch argumentsParser.operationType { + case .usage: break + default: XCTFail("Bad operation") + } + } + + +// func testLocalizationOperationWithValidParameters() { +// let argumentsParser = ArgumentsParser(arguments: ["localization", "--template", "hello", "--output", "Gen.swift", "--search-path", "Search Path", "--base-language-code", "he"]) +// switch argumentsParser.operationType { +// case .localization(let parameters): +// XCTAssertEqual(parameters.templateFilepath, "hello") +// XCTAssertEqual(parameters.outputFilename, "Gen.swift") +// XCTAssertEqual(parameters.searchPath, "Search Path") +// XCTAssertEqual(parameters.baseLanguageCode, "he") +// default: XCTFail("Bad operation") +// } +// } +} + + diff --git a/regen-tests/Info.plist b/regen-tests/Info.plist new file mode 100644 index 0000000..6c40a6c --- /dev/null +++ b/regen-tests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/regen-tests/StringClassNameTests.swift b/regen-tests/StringClassNameTests.swift new file mode 100644 index 0000000..7022c9d --- /dev/null +++ b/regen-tests/StringClassNameTests.swift @@ -0,0 +1,28 @@ +// +// StringClassNameTests.swift +// regen-tests +// +// Created by Ido Mizrachi on 24/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import XCTest + +class StringClassNameTests: XCTestCase { + + func testConvertToClassName() { + let text = "helloThere" + XCTAssertEqual(text.className(), "HelloThere") + } + + func testEmptyString() { + let text = "" + XCTAssertEqual(text.className(), "") + } + + func testAlreadyCapitalizedString() { + let text = "HOLYSheep" + XCTAssertEqual(text.className(), "HOLYSheep") + } + +} diff --git a/regen-tests/regen_tests.swift b/regen-tests/regen_tests.swift new file mode 100644 index 0000000..ed4a149 --- /dev/null +++ b/regen-tests/regen_tests.swift @@ -0,0 +1,15 @@ +// +// regen_tests.swift +// regen-tests +// +// Created by Ido Mizrachi on 12/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import XCTest + + +class regen_tests: XCTestCase { + + +} diff --git a/regen.xcodeproj/project.pbxproj b/regen.xcodeproj/project.pbxproj index edba8df..5947c40 100644 --- a/regen.xcodeproj/project.pbxproj +++ b/regen.xcodeproj/project.pbxproj @@ -7,23 +7,102 @@ objects = { /* Begin PBXBuildFile section */ + 622B8F2D22D9D821001FC1E4 /* ImagesNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622B8F2C22D9D821001FC1E4 /* ImagesNamespace.swift */; }; + 622B8F2E22D9D821001FC1E4 /* ImagesNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622B8F2C22D9D821001FC1E4 /* ImagesNamespace.swift */; }; + 622B8F3122D9D919001FC1E4 /* ImagesetParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A01D40EC1B00738100 /* ImagesetParser.swift */; }; + 6236BBA622ECD7AC002615E9 /* _SwiftSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AE22D7BBD300D4585A /* _SwiftSupport.swift */; }; + 6236BBA722ECD7AC002615E9 /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5BB22D7BBD400D4585A /* Context.swift */; }; + 6236BBA822ECD7AC002615E9 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AA22D7BBD300D4585A /* Environment.swift */; }; + 6236BBA922ECD7AC002615E9 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B322D7BBD400D4585A /* Errors.swift */; }; + 6236BBAA22ECD7AC002615E9 /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B422D7BBD400D4585A /* Expression.swift */; }; + 6236BBAB22ECD7AD002615E9 /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AC22D7BBD300D4585A /* Extension.swift */; }; + 6236BBAC22ECD7AD002615E9 /* Filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B222D7BBD400D4585A /* Filters.swift */; }; + 6236BBAD22ECD7AD002615E9 /* FilterTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5BC22D7BBD400D4585A /* FilterTag.swift */; }; + 6236BBAE22ECD7AD002615E9 /* ForTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AB22D7BBD300D4585A /* ForTag.swift */; }; + 6236BBAF22ECD7AD002615E9 /* IfTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5A922D7BBD300D4585A /* IfTag.swift */; }; + 6236BBB022ECD7AD002615E9 /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B722D7BBD400D4585A /* Include.swift */; }; + 6236BBB122ECD7AD002615E9 /* Inheritence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B522D7BBD400D4585A /* Inheritence.swift */; }; + 6236BBB222ECD7AD002615E9 /* KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AD22D7BBD300D4585A /* KeyPath.swift */; }; + 6236BBB322ECD7AD002615E9 /* Lexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B822D7BBD400D4585A /* Lexer.swift */; }; + 6236BBB422ECD7AD002615E9 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AF22D7BBD300D4585A /* Loader.swift */; }; + 6236BBB522ECD7AD002615E9 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B022D7BBD300D4585A /* Node.swift */; }; + 6236BBB622ECD7AD002615E9 /* NowTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B922D7BBD400D4585A /* NowTag.swift */; }; + 6236BBB722ECD7AD002615E9 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B122D7BBD300D4585A /* Parser.swift */; }; + 6236BBB822ECD7AD002615E9 /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5A822D7BBD300D4585A /* Template.swift */; }; + 6236BBB922ECD7AD002615E9 /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B622D7BBD400D4585A /* Tokenizer.swift */; }; + 6236BBBA22ECD7AD002615E9 /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5BA22D7BBD400D4585A /* Variable.swift */; }; + 6236BBBB22ECE207002615E9 /* PathKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5D422D7BE6200D4585A /* PathKit.swift */; }; + 624516F222E632F500E86FB1 /* LocalizationParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6247EE3922DBB65D0096078D /* LocalizationParameters.swift */; }; + 624516F322E632FF00E86FB1 /* ImagesParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62492B5E22DE5E2700537757 /* ImagesParameters.swift */; }; + 624516F422E6334C00E86FB1 /* LocalizationParametersParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627F4E9C22DF59EF0090E24F /* LocalizationParametersParser.swift */; }; + 624516F522E6336200E86FB1 /* CommandLineParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627F4E9922DF551A0090E24F /* CommandLineParameter.swift */; }; + 624516F622E6337A00E86FB1 /* ImageesAssetsFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49E1D40EC1B00738100 /* ImageesAssetsFinder.swift */; }; + 624516F722E6339100E86FB1 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994BF8351F8275FF006A62C3 /* Image.swift */; }; + 624516F822E633D700E86FB1 /* Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994BF8331F8275DB006A62C3 /* Property.swift */; }; + 624516F922E633E100E86FB1 /* PropertyName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4AB1D40EC1B00738100 /* PropertyName.swift */; }; + 624516FA22E633F200E86FB1 /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6252986222D87AD200082EC3 /* Tree.swift */; }; + 624516FB22E633FF00E86FB1 /* ImageNodeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994BF8311F8275B9006A62C3 /* ImageNodeItem.swift */; }; + 624516FC22E6340D00E86FB1 /* ImagesFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A11D40EC1B00738100 /* ImagesFinder.swift */; }; + 624516FD22E6353200E86FB1 /* ImagesValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A41D40EC1B00738100 /* ImagesValidator.swift */; }; + 624516FE22E6353B00E86FB1 /* ClassName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 992D521A1F8418EB00258395 /* ClassName.swift */; }; + 6247EE3A22DBB65D0096078D /* LocalizationParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6247EE3922DBB65D0096078D /* LocalizationParameters.swift */; }; + 62492B5D22DE5D9300537757 /* whitelist.txt in Copy Files */ = {isa = PBXBuildFile; fileRef = 62492B5C22DE5D3E00537757 /* whitelist.txt */; }; + 62492B5F22DE5E2700537757 /* ImagesParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62492B5E22DE5E2700537757 /* ImagesParameters.swift */; }; + 6252986322D87AD200082EC3 /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6252986222D87AD200082EC3 /* Tree.swift */; }; + 6252987822D87DF900082EC3 /* regen_tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6252987722D87DF900082EC3 /* regen_tests.swift */; }; + 6268E5BD22D7BBD400D4585A /* Template.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5A822D7BBD300D4585A /* Template.swift */; }; + 6268E5BE22D7BBD400D4585A /* IfTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5A922D7BBD300D4585A /* IfTag.swift */; }; + 6268E5BF22D7BBD400D4585A /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AA22D7BBD300D4585A /* Environment.swift */; }; + 6268E5C022D7BBD400D4585A /* ForTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AB22D7BBD300D4585A /* ForTag.swift */; }; + 6268E5C122D7BBD400D4585A /* Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AC22D7BBD300D4585A /* Extension.swift */; }; + 6268E5C222D7BBD400D4585A /* KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AD22D7BBD300D4585A /* KeyPath.swift */; }; + 6268E5C322D7BBD400D4585A /* _SwiftSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AE22D7BBD300D4585A /* _SwiftSupport.swift */; }; + 6268E5C422D7BBD400D4585A /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5AF22D7BBD300D4585A /* Loader.swift */; }; + 6268E5C522D7BBD400D4585A /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B022D7BBD300D4585A /* Node.swift */; }; + 6268E5C622D7BBD400D4585A /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B122D7BBD300D4585A /* Parser.swift */; }; + 6268E5C722D7BBD400D4585A /* Filters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B222D7BBD400D4585A /* Filters.swift */; }; + 6268E5C822D7BBD400D4585A /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B322D7BBD400D4585A /* Errors.swift */; }; + 6268E5C922D7BBD400D4585A /* Expression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B422D7BBD400D4585A /* Expression.swift */; }; + 6268E5CA22D7BBD400D4585A /* Inheritence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B522D7BBD400D4585A /* Inheritence.swift */; }; + 6268E5CB22D7BBD400D4585A /* Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B622D7BBD400D4585A /* Tokenizer.swift */; }; + 6268E5CC22D7BBD400D4585A /* Include.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B722D7BBD400D4585A /* Include.swift */; }; + 6268E5CD22D7BBD400D4585A /* Lexer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B822D7BBD400D4585A /* Lexer.swift */; }; + 6268E5CE22D7BBD400D4585A /* NowTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5B922D7BBD400D4585A /* NowTag.swift */; }; + 6268E5CF22D7BBD400D4585A /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5BA22D7BBD400D4585A /* Variable.swift */; }; + 6268E5D022D7BBD400D4585A /* Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5BB22D7BBD400D4585A /* Context.swift */; }; + 6268E5D122D7BBD400D4585A /* FilterTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5BC22D7BBD400D4585A /* FilterTag.swift */; }; + 6268E5D522D7BE6200D4585A /* PathKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6268E5D422D7BE6200D4585A /* PathKit.swift */; }; + 62741CAA22DC5DC800B66956 /* ParametersLocalizationTemplate.swift in Copy Files */ = {isa = PBXBuildFile; fileRef = 62741CA922DC5DBF00B66956 /* ParametersLocalizationTemplate.swift */; }; + 627F4E9A22DF551A0090E24F /* CommandLineParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627F4E9922DF551A0090E24F /* CommandLineParameter.swift */; }; + 627F4E9D22DF59EF0090E24F /* LocalizationParametersParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627F4E9C22DF59EF0090E24F /* LocalizationParametersParser.swift */; }; + 62A625DC22E87AA1007AEA56 /* StringClassNameTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A625DB22E87AA1007AEA56 /* StringClassNameTests.swift */; }; + 62A625DE22E87E8B007AEA56 /* ImagesParametersParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A625DD22E87E8B007AEA56 /* ImagesParametersParser.swift */; }; + 62A625DF22E87E8B007AEA56 /* ImagesParametersParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62A625DD22E87E8B007AEA56 /* ImagesParametersParser.swift */; }; + 62A625E022E8966E007AEA56 /* ImagesTemplate.swift in Copy Files */ = {isa = PBXBuildFile; fileRef = 622B8F2F22D9D85B001FC1E4 /* ImagesTemplate.swift */; }; + 62CEA90F22DB10E200A20932 /* LocalizationTemplate.swift in Copy Files */ = {isa = PBXBuildFile; fileRef = 62793D3B222D6A140039D3F8 /* LocalizationTemplate.swift */; }; + 62F5CC0B22D87EB2002B28B5 /* Usage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4AC1D40EC1B00738100 /* Usage.swift */; }; + 62F5CC0C22D87ECB002B28B5 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49F1D40EC1B00738100 /* Colors.swift */; }; + 62F5CC0F22D8A99F002B28B5 /* ArgumentsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49D1D40EC1B00738100 /* ArgumentsParser.swift */; }; + 62F5CC1022D8AAA7002B28B5 /* OperationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4AA1D40EC1B00738100 /* OperationType.swift */; }; + 62F5CC1222D8AB41002B28B5 /* ArgumentsParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F5CC1122D8AB41002B28B5 /* ArgumentsParserTests.swift */; }; + 62F5CC1422D8C1AA002B28B5 /* LocalizationNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F5CC1322D8C1AA002B28B5 /* LocalizationNamespace.swift */; }; + 62F5CC1522D8D607002B28B5 /* LocalizationNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62F5CC1322D8C1AA002B28B5 /* LocalizationNamespace.swift */; }; + 62F5CC1622D8D621002B28B5 /* ImagesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A21D40EC1B00738100 /* ImagesOperation.swift */; }; 992D521B1F8418EB00258395 /* ClassName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 992D521A1F8418EB00258395 /* ClassName.swift */; }; 994BF8321F8275B9006A62C3 /* ImageNodeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994BF8311F8275B9006A62C3 /* ImageNodeItem.swift */; }; 994BF8341F8275DB006A62C3 /* Property.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994BF8331F8275DB006A62C3 /* Property.swift */; }; 994BF8361F8275FF006A62C3 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 994BF8351F8275FF006A62C3 /* Image.swift */; }; 99667CD11F8C064B00D4A853 /* FileUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99667CD01F8C064B00D4A853 /* FileUtils.swift */; }; - 9977F4681F834C7E00D77D2C /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9977F4671F834C7E00D77D2C /* Tree.swift */; }; 99ABD9F41EC7185D00032E1B /* OperationTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99ABD9F31EC7185D00032E1B /* OperationTimer.swift */; }; 99AF6AF21EC5EB9600C91BA8 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99AF6AF11EC5EB9600C91BA8 /* Logger.swift */; }; 99B9E4AE1D40EC1B00738100 /* ArgumentsParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49D1D40EC1B00738100 /* ArgumentsParser.swift */; }; - 99B9E4AF1D40EC1B00738100 /* AssetsFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49E1D40EC1B00738100 /* AssetsFinder.swift */; }; + 99B9E4AF1D40EC1B00738100 /* ImageesAssetsFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49E1D40EC1B00738100 /* ImageesAssetsFinder.swift */; }; 99B9E4B01D40EC1B00738100 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E49F1D40EC1B00738100 /* Colors.swift */; }; 99B9E4B11D40EC1B00738100 /* ImagesetParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A01D40EC1B00738100 /* ImagesetParser.swift */; }; - 99B9E4B21D40EC1B00738100 /* ImageFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A11D40EC1B00738100 /* ImageFinder.swift */; }; - 99B9E4B31D40EC1B00738100 /* ImageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A21D40EC1B00738100 /* ImageOperation.swift */; }; + 99B9E4B21D40EC1B00738100 /* ImagesFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A11D40EC1B00738100 /* ImagesFinder.swift */; }; + 99B9E4B31D40EC1B00738100 /* ImagesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A21D40EC1B00738100 /* ImagesOperation.swift */; }; 99B9E4B41D40EC1B00738100 /* ImagesClassGeneratorObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A31D40EC1B00738100 /* ImagesClassGeneratorObjC.swift */; }; 99B9E4B51D40EC1B00738100 /* ImagesValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A41D40EC1B00738100 /* ImagesValidator.swift */; }; - 99B9E4B61D40EC1B00738100 /* LocalizationClassGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A51D40EC1B00738100 /* LocalizationClassGenerator.swift */; }; 99B9E4B71D40EC1B00738100 /* LocalizationFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A61D40EC1B00738100 /* LocalizationFinder.swift */; }; 99B9E4B81D40EC1B00738100 /* LocalizationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A71D40EC1B00738100 /* LocalizationOperation.swift */; }; 99B9E4B91D40EC1B00738100 /* LocalizationParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4A81D40EC1B00738100 /* LocalizationParser.swift */; }; @@ -34,41 +113,81 @@ 99B9E4BE1D40EC1B00738100 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99B9E4AD1D40EC1B00738100 /* Version.swift */; }; 99D74E1B1EC3919D003BFACE /* ImagesClassGeneratorSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D74E1A1EC3919D003BFACE /* ImagesClassGeneratorSwift.swift */; }; 99D74E1D1EC391BE003BFACE /* ImagesClassGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D74E1C1EC391BE003BFACE /* ImagesClassGenerator.swift */; }; - 99D74E341EC39AF8003BFACE /* LocalizationClassGeneratorObjc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D74E331EC39AF8003BFACE /* LocalizationClassGeneratorObjc.swift */; }; - 99D74E361EC39B62003BFACE /* LocalizationClassGeneratorSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99D74E351EC39B62003BFACE /* LocalizationClassGeneratorSwift.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ - 99B9E4911D40EBF900738100 /* CopyFiles */ = { + 99B9E4911D40EBF900738100 /* Copy Files */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; + buildActionMask = 12; + dstPath = ""; + dstSubfolderSpec = 16; files = ( + 62A625E022E8966E007AEA56 /* ImagesTemplate.swift in Copy Files */, + 62492B5D22DE5D9300537757 /* whitelist.txt in Copy Files */, + 62741CAA22DC5DC800B66956 /* ParametersLocalizationTemplate.swift in Copy Files */, + 62CEA90F22DB10E200A20932 /* LocalizationTemplate.swift in Copy Files */, ); - runOnlyForDeploymentPostprocessing = 1; + name = "Copy Files"; + runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 622B8F2C22D9D821001FC1E4 /* ImagesNamespace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesNamespace.swift; sourceTree = ""; }; + 622B8F2F22D9D85B001FC1E4 /* ImagesTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesTemplate.swift; sourceTree = ""; }; + 6247EE3922DBB65D0096078D /* LocalizationParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationParameters.swift; sourceTree = ""; }; + 62492B5C22DE5D3E00537757 /* whitelist.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = whitelist.txt; sourceTree = ""; }; + 62492B5E22DE5E2700537757 /* ImagesParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesParameters.swift; sourceTree = ""; }; + 6252986222D87AD200082EC3 /* Tree.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tree.swift; sourceTree = ""; }; + 6252987522D87DF900082EC3 /* regen-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "regen-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6252987722D87DF900082EC3 /* regen_tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = regen_tests.swift; sourceTree = ""; }; + 6252987922D87DF900082EC3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6268E5A822D7BBD300D4585A /* Template.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Template.swift; sourceTree = ""; }; + 6268E5A922D7BBD300D4585A /* IfTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IfTag.swift; sourceTree = ""; }; + 6268E5AA22D7BBD300D4585A /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; + 6268E5AB22D7BBD300D4585A /* ForTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForTag.swift; sourceTree = ""; }; + 6268E5AC22D7BBD300D4585A /* Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; }; + 6268E5AD22D7BBD300D4585A /* KeyPath.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPath.swift; sourceTree = ""; }; + 6268E5AE22D7BBD300D4585A /* _SwiftSupport.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = _SwiftSupport.swift; sourceTree = ""; }; + 6268E5AF22D7BBD300D4585A /* Loader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; + 6268E5B022D7BBD300D4585A /* Node.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; + 6268E5B122D7BBD300D4585A /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; + 6268E5B222D7BBD400D4585A /* Filters.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Filters.swift; sourceTree = ""; }; + 6268E5B322D7BBD400D4585A /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; + 6268E5B422D7BBD400D4585A /* Expression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Expression.swift; sourceTree = ""; }; + 6268E5B522D7BBD400D4585A /* Inheritence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inheritence.swift; sourceTree = ""; }; + 6268E5B622D7BBD400D4585A /* Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tokenizer.swift; sourceTree = ""; }; + 6268E5B722D7BBD400D4585A /* Include.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Include.swift; sourceTree = ""; }; + 6268E5B822D7BBD400D4585A /* Lexer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lexer.swift; sourceTree = ""; }; + 6268E5B922D7BBD400D4585A /* NowTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NowTag.swift; sourceTree = ""; }; + 6268E5BA22D7BBD400D4585A /* Variable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Variable.swift; sourceTree = ""; }; + 6268E5BB22D7BBD400D4585A /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = ""; }; + 6268E5BC22D7BBD400D4585A /* FilterTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterTag.swift; sourceTree = ""; }; + 6268E5D422D7BE6200D4585A /* PathKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PathKit.swift; sourceTree = ""; }; + 62741CA922DC5DBF00B66956 /* ParametersLocalizationTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersLocalizationTemplate.swift; sourceTree = ""; }; + 62793D3B222D6A140039D3F8 /* LocalizationTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTemplate.swift; sourceTree = ""; }; + 627F4E9922DF551A0090E24F /* CommandLineParameter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandLineParameter.swift; sourceTree = ""; }; + 627F4E9C22DF59EF0090E24F /* LocalizationParametersParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationParametersParser.swift; sourceTree = ""; }; + 62A625DB22E87AA1007AEA56 /* StringClassNameTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringClassNameTests.swift; sourceTree = ""; }; + 62A625DD22E87E8B007AEA56 /* ImagesParametersParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesParametersParser.swift; sourceTree = ""; }; + 62F5CC1122D8AB41002B28B5 /* ArgumentsParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArgumentsParserTests.swift; sourceTree = ""; }; + 62F5CC1322D8C1AA002B28B5 /* LocalizationNamespace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationNamespace.swift; sourceTree = ""; }; 992D521A1F8418EB00258395 /* ClassName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassName.swift; sourceTree = ""; }; 994BF8311F8275B9006A62C3 /* ImageNodeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageNodeItem.swift; sourceTree = ""; }; 994BF8331F8275DB006A62C3 /* Property.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Property.swift; sourceTree = ""; }; 994BF8351F8275FF006A62C3 /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 99667CD01F8C064B00D4A853 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; - 9977F4671F834C7E00D77D2C /* Tree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tree.swift; sourceTree = ""; }; 99ABD9F31EC7185D00032E1B /* OperationTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OperationTimer.swift; sourceTree = ""; }; 99AF6AF11EC5EB9600C91BA8 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 99B9E4931D40EBF900738100 /* regen */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = regen; sourceTree = BUILT_PRODUCTS_DIR; }; 99B9E49D1D40EC1B00738100 /* ArgumentsParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArgumentsParser.swift; sourceTree = ""; }; - 99B9E49E1D40EC1B00738100 /* AssetsFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetsFinder.swift; sourceTree = ""; }; + 99B9E49E1D40EC1B00738100 /* ImageesAssetsFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageesAssetsFinder.swift; sourceTree = ""; }; 99B9E49F1D40EC1B00738100 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 99B9E4A01D40EC1B00738100 /* ImagesetParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesetParser.swift; sourceTree = ""; }; - 99B9E4A11D40EC1B00738100 /* ImageFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageFinder.swift; sourceTree = ""; }; - 99B9E4A21D40EC1B00738100 /* ImageOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageOperation.swift; sourceTree = ""; }; + 99B9E4A11D40EC1B00738100 /* ImagesFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesFinder.swift; sourceTree = ""; }; + 99B9E4A21D40EC1B00738100 /* ImagesOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesOperation.swift; sourceTree = ""; }; 99B9E4A31D40EC1B00738100 /* ImagesClassGeneratorObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesClassGeneratorObjC.swift; sourceTree = ""; }; 99B9E4A41D40EC1B00738100 /* ImagesValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesValidator.swift; sourceTree = ""; }; - 99B9E4A51D40EC1B00738100 /* LocalizationClassGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationClassGenerator.swift; sourceTree = ""; }; 99B9E4A61D40EC1B00738100 /* LocalizationFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationFinder.swift; sourceTree = ""; }; 99B9E4A71D40EC1B00738100 /* LocalizationOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationOperation.swift; sourceTree = ""; }; 99B9E4A81D40EC1B00738100 /* LocalizationParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationParser.swift; sourceTree = ""; }; @@ -79,11 +198,16 @@ 99B9E4AD1D40EC1B00738100 /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; 99D74E1A1EC3919D003BFACE /* ImagesClassGeneratorSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesClassGeneratorSwift.swift; sourceTree = ""; }; 99D74E1C1EC391BE003BFACE /* ImagesClassGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagesClassGenerator.swift; sourceTree = ""; }; - 99D74E331EC39AF8003BFACE /* LocalizationClassGeneratorObjc.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationClassGeneratorObjc.swift; sourceTree = ""; }; - 99D74E351EC39B62003BFACE /* LocalizationClassGeneratorSwift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationClassGeneratorSwift.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 6252987222D87DF900082EC3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 99B9E4901D40EBF900738100 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -94,23 +218,108 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 994BF8301F827590006A62C3 /* Model */ = { + 6252985F22D87AAA00082EC3 /* General Models */ = { isa = PBXGroup; children = ( - 994BF8311F8275B9006A62C3 /* ImageNodeItem.swift */, - 994BF8331F8275DB006A62C3 /* Property.swift */, - 994BF8351F8275FF006A62C3 /* Image.swift */, + 6252986222D87AD200082EC3 /* Tree.swift */, + 627F4E9922DF551A0090E24F /* CommandLineParameter.swift */, ); - path = Model; + path = "General Models"; sourceTree = ""; }; - 997179161F7D6F1500FF8AB2 /* Data Structures */ = { + 6252986422D87B6500082EC3 /* Colors */ = { isa = PBXGroup; children = ( - 9977F4671F834C7E00D77D2C /* Tree.swift */, + 99B9E49F1D40EC1B00738100 /* Colors.swift */, + ); + path = Colors; + sourceTree = ""; + }; + 6252987622D87DF900082EC3 /* regen-tests */ = { + isa = PBXGroup; + children = ( + 6252987722D87DF900082EC3 /* regen_tests.swift */, + 6252987922D87DF900082EC3 /* Info.plist */, + 62F5CC1122D8AB41002B28B5 /* ArgumentsParserTests.swift */, + 62A625DB22E87AA1007AEA56 /* StringClassNameTests.swift */, + ); + path = "regen-tests"; + sourceTree = ""; + }; + 6268E5A622D7BB9800D4585A /* Dependencies */ = { + isa = PBXGroup; + children = ( + 6252986422D87B6500082EC3 /* Colors */, + 6268E5D322D7BE5000D4585A /* PathKit */, + 6268E5A722D7BBCB00D4585A /* Stencil */, + ); + path = Dependencies; + sourceTree = ""; + }; + 6268E5A722D7BBCB00D4585A /* Stencil */ = { + isa = PBXGroup; + children = ( + 6268E5AE22D7BBD300D4585A /* _SwiftSupport.swift */, + 6268E5BB22D7BBD400D4585A /* Context.swift */, + 6268E5AA22D7BBD300D4585A /* Environment.swift */, + 6268E5B322D7BBD400D4585A /* Errors.swift */, + 6268E5B422D7BBD400D4585A /* Expression.swift */, + 6268E5AC22D7BBD300D4585A /* Extension.swift */, + 6268E5B222D7BBD400D4585A /* Filters.swift */, + 6268E5BC22D7BBD400D4585A /* FilterTag.swift */, + 6268E5AB22D7BBD300D4585A /* ForTag.swift */, + 6268E5A922D7BBD300D4585A /* IfTag.swift */, + 6268E5B722D7BBD400D4585A /* Include.swift */, + 6268E5B522D7BBD400D4585A /* Inheritence.swift */, + 6268E5AD22D7BBD300D4585A /* KeyPath.swift */, + 6268E5B822D7BBD400D4585A /* Lexer.swift */, + 6268E5AF22D7BBD300D4585A /* Loader.swift */, + 6268E5B022D7BBD300D4585A /* Node.swift */, + 6268E5B922D7BBD400D4585A /* NowTag.swift */, + 6268E5B122D7BBD300D4585A /* Parser.swift */, + 6268E5A822D7BBD300D4585A /* Template.swift */, + 6268E5B622D7BBD400D4585A /* Tokenizer.swift */, + 6268E5BA22D7BBD400D4585A /* Variable.swift */, + ); + path = Stencil; + sourceTree = ""; + }; + 6268E5D322D7BE5000D4585A /* PathKit */ = { + isa = PBXGroup; + children = ( + 6268E5D422D7BE6200D4585A /* PathKit.swift */, + ); + path = PathKit; + sourceTree = ""; + }; + 62793D3C222D6A200039D3F8 /* Resources */ = { + isa = PBXGroup; + children = ( + 62793D3B222D6A140039D3F8 /* LocalizationTemplate.swift */, + 62741CA922DC5DBF00B66956 /* ParametersLocalizationTemplate.swift */, + 622B8F2F22D9D85B001FC1E4 /* ImagesTemplate.swift */, + 62492B5C22DE5D3E00537757 /* whitelist.txt */, + ); + path = Resources; + sourceTree = ""; + }; + 627F4E9B22DF552F0090E24F /* Models */ = { + isa = PBXGroup; + children = ( + 6247EE3922DBB65D0096078D /* LocalizationParameters.swift */, + ); + path = Models; + sourceTree = ""; + }; + 994BF8301F827590006A62C3 /* Model */ = { + isa = PBXGroup; + children = ( + 62492B5E22DE5E2700537757 /* ImagesParameters.swift */, + 994BF8311F8275B9006A62C3 /* ImageNodeItem.swift */, + 994BF8331F8275DB006A62C3 /* Property.swift */, + 994BF8351F8275FF006A62C3 /* Image.swift */, ); - name = "Data Structures"; - path = "../RegenFramework/Data Structures"; + path = Model; sourceTree = ""; }; 997179181F7D6FDA00FF8AB2 /* Frameworks */ = { @@ -124,6 +333,7 @@ isa = PBXGroup; children = ( 99B9E4951D40EBF900738100 /* regen */, + 6252987622D87DF900082EC3 /* regen-tests */, 99B9E4941D40EBF900738100 /* Products */, 997179181F7D6FDA00FF8AB2 /* Frameworks */, ); @@ -133,6 +343,7 @@ isa = PBXGroup; children = ( 99B9E4931D40EBF900738100 /* regen */, + 6252987522D87DF900082EC3 /* regen-tests.xctest */, ); name = Products; sourceTree = ""; @@ -140,10 +351,12 @@ 99B9E4951D40EBF900738100 /* regen */ = { isa = PBXGroup; children = ( - 997179161F7D6F1500FF8AB2 /* Data Structures */, - 99BEB1FC1F78C36F002D9E05 /* Utilities */, + 6268E5A622D7BB9800D4585A /* Dependencies */, + 62793D3C222D6A200039D3F8 /* Resources */, + 6252985F22D87AAA00082EC3 /* General Models */, 99BEB1FA1F78C354002D9E05 /* Localization */, 99BEB1F91F78C342002D9E05 /* Images */, + 99BEB1FC1F78C36F002D9E05 /* Utilities */, 99B9E49D1D40EC1B00738100 /* ArgumentsParser.swift */, 99B9E4AA1D40EC1B00738100 /* OperationType.swift */, 99B9E4AC1D40EC1B00738100 /* Usage.swift */, @@ -157,14 +370,18 @@ isa = PBXGroup; children = ( 994BF8301F827590006A62C3 /* Model */, + 622B8F2C22D9D821001FC1E4 /* ImagesNamespace.swift */, 99D74E1C1EC391BE003BFACE /* ImagesClassGenerator.swift */, 99B9E4A31D40EC1B00738100 /* ImagesClassGeneratorObjC.swift */, 99D74E1A1EC3919D003BFACE /* ImagesClassGeneratorSwift.swift */, - 99B9E49E1D40EC1B00738100 /* AssetsFinder.swift */, + 99B9E49E1D40EC1B00738100 /* ImageesAssetsFinder.swift */, 99B9E4A01D40EC1B00738100 /* ImagesetParser.swift */, - 99B9E4A11D40EC1B00738100 /* ImageFinder.swift */, + 99B9E4A11D40EC1B00738100 /* ImagesFinder.swift */, 99B9E4A41D40EC1B00738100 /* ImagesValidator.swift */, - 99B9E4A21D40EC1B00738100 /* ImageOperation.swift */, + 99B9E4AB1D40EC1B00738100 /* PropertyName.swift */, + 992D521A1F8418EB00258395 /* ClassName.swift */, + 99B9E4A21D40EC1B00738100 /* ImagesOperation.swift */, + 62A625DD22E87E8B007AEA56 /* ImagesParametersParser.swift */, ); path = Images; sourceTree = ""; @@ -172,12 +389,12 @@ 99BEB1FA1F78C354002D9E05 /* Localization */ = { isa = PBXGroup; children = ( - 99B9E4A51D40EC1B00738100 /* LocalizationClassGenerator.swift */, - 99D74E331EC39AF8003BFACE /* LocalizationClassGeneratorObjc.swift */, - 99D74E351EC39B62003BFACE /* LocalizationClassGeneratorSwift.swift */, + 627F4E9B22DF552F0090E24F /* Models */, + 62F5CC1322D8C1AA002B28B5 /* LocalizationNamespace.swift */, 99B9E4A61D40EC1B00738100 /* LocalizationFinder.swift */, 99B9E4A81D40EC1B00738100 /* LocalizationParser.swift */, 99B9E4A71D40EC1B00738100 /* LocalizationOperation.swift */, + 627F4E9C22DF59EF0090E24F /* LocalizationParametersParser.swift */, ); path = Localization; sourceTree = ""; @@ -185,11 +402,8 @@ 99BEB1FC1F78C36F002D9E05 /* Utilities */ = { isa = PBXGroup; children = ( - 99B9E4AB1D40EC1B00738100 /* PropertyName.swift */, - 99B9E49F1D40EC1B00738100 /* Colors.swift */, 99AF6AF11EC5EB9600C91BA8 /* Logger.swift */, 99ABD9F31EC7185D00032E1B /* OperationTimer.swift */, - 992D521A1F8418EB00258395 /* ClassName.swift */, 99667CD01F8C064B00D4A853 /* FileUtils.swift */, ); path = Utilities; @@ -198,13 +412,30 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 6252987422D87DF900082EC3 /* regen-tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6252987A22D87DF900082EC3 /* Build configuration list for PBXNativeTarget "regen-tests" */; + buildPhases = ( + 6252987122D87DF900082EC3 /* Sources */, + 6252987222D87DF900082EC3 /* Frameworks */, + 6252987322D87DF900082EC3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "regen-tests"; + productName = "regen-tests"; + productReference = 6252987522D87DF900082EC3 /* regen-tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 99B9E4921D40EBF900738100 /* regen */ = { isa = PBXNativeTarget; buildConfigurationList = 99B9E49A1D40EBF900738100 /* Build configuration list for PBXNativeTarget "regen" */; buildPhases = ( 99B9E48F1D40EBF900738100 /* Sources */, 99B9E4901D40EBF900738100 /* Frameworks */, - 99B9E4911D40EBF900738100 /* CopyFiles */, + 99B9E4911D40EBF900738100 /* Copy Files */, ); buildRules = ( ); @@ -221,10 +452,13 @@ 99B9E48B1D40EBF900738100 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0900; + LastSwiftUpdateCheck = 1020; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Ido Mizrachi"; TargetAttributes = { + 6252987422D87DF900082EC3 = { + CreatedOnToolsVersion = 10.2.1; + }; 99B9E4921D40EBF900738100 = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 0900; @@ -236,6 +470,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 99B9E48A1D40EBF900738100; @@ -244,42 +479,133 @@ projectRoot = ""; targets = ( 99B9E4921D40EBF900738100 /* regen */, + 6252987422D87DF900082EC3 /* regen-tests */, ); }; /* End PBXProject section */ +/* Begin PBXResourcesBuildPhase section */ + 6252987322D87DF900082EC3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ + 6252987122D87DF900082EC3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6236BBA622ECD7AC002615E9 /* _SwiftSupport.swift in Sources */, + 6236BBA722ECD7AC002615E9 /* Context.swift in Sources */, + 6236BBA822ECD7AC002615E9 /* Environment.swift in Sources */, + 6236BBA922ECD7AC002615E9 /* Errors.swift in Sources */, + 6236BBAA22ECD7AC002615E9 /* Expression.swift in Sources */, + 6236BBAB22ECD7AD002615E9 /* Extension.swift in Sources */, + 6236BBAC22ECD7AD002615E9 /* Filters.swift in Sources */, + 6236BBAD22ECD7AD002615E9 /* FilterTag.swift in Sources */, + 6236BBAE22ECD7AD002615E9 /* ForTag.swift in Sources */, + 6236BBAF22ECD7AD002615E9 /* IfTag.swift in Sources */, + 6236BBB022ECD7AD002615E9 /* Include.swift in Sources */, + 6236BBB122ECD7AD002615E9 /* Inheritence.swift in Sources */, + 6236BBB222ECD7AD002615E9 /* KeyPath.swift in Sources */, + 6236BBB322ECD7AD002615E9 /* Lexer.swift in Sources */, + 6236BBB422ECD7AD002615E9 /* Loader.swift in Sources */, + 6236BBB522ECD7AD002615E9 /* Node.swift in Sources */, + 6236BBB622ECD7AD002615E9 /* NowTag.swift in Sources */, + 6236BBB722ECD7AD002615E9 /* Parser.swift in Sources */, + 6236BBB822ECD7AD002615E9 /* Template.swift in Sources */, + 6236BBB922ECD7AD002615E9 /* Tokenizer.swift in Sources */, + 6236BBBA22ECD7AD002615E9 /* Variable.swift in Sources */, + 622B8F2E22D9D821001FC1E4 /* ImagesNamespace.swift in Sources */, + 624516F522E6336200E86FB1 /* CommandLineParameter.swift in Sources */, + 62F5CC1522D8D607002B28B5 /* LocalizationNamespace.swift in Sources */, + 624516F822E633D700E86FB1 /* Property.swift in Sources */, + 6252987822D87DF900082EC3 /* regen_tests.swift in Sources */, + 62F5CC1022D8AAA7002B28B5 /* OperationType.swift in Sources */, + 6236BBBB22ECE207002615E9 /* PathKit.swift in Sources */, + 62F5CC0F22D8A99F002B28B5 /* ArgumentsParser.swift in Sources */, + 62F5CC1222D8AB41002B28B5 /* ArgumentsParserTests.swift in Sources */, + 624516FD22E6353200E86FB1 /* ImagesValidator.swift in Sources */, + 624516FE22E6353B00E86FB1 /* ClassName.swift in Sources */, + 62A625DF22E87E8B007AEA56 /* ImagesParametersParser.swift in Sources */, + 62F5CC0C22D87ECB002B28B5 /* Colors.swift in Sources */, + 624516F622E6337A00E86FB1 /* ImageesAssetsFinder.swift in Sources */, + 624516FB22E633FF00E86FB1 /* ImageNodeItem.swift in Sources */, + 624516FA22E633F200E86FB1 /* Tree.swift in Sources */, + 624516F722E6339100E86FB1 /* Image.swift in Sources */, + 622B8F3122D9D919001FC1E4 /* ImagesetParser.swift in Sources */, + 624516F222E632F500E86FB1 /* LocalizationParameters.swift in Sources */, + 62F5CC0B22D87EB2002B28B5 /* Usage.swift in Sources */, + 62F5CC1622D8D621002B28B5 /* ImagesOperation.swift in Sources */, + 624516F322E632FF00E86FB1 /* ImagesParameters.swift in Sources */, + 624516FC22E6340D00E86FB1 /* ImagesFinder.swift in Sources */, + 624516F422E6334C00E86FB1 /* LocalizationParametersParser.swift in Sources */, + 62A625DC22E87AA1007AEA56 /* StringClassNameTests.swift in Sources */, + 624516F922E633E100E86FB1 /* PropertyName.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 99B9E48F1D40EBF900738100 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 99B9E4B41D40EC1B00738100 /* ImagesClassGeneratorObjC.swift in Sources */, + 6268E5C722D7BBD400D4585A /* Filters.swift in Sources */, + 6268E5CE22D7BBD400D4585A /* NowTag.swift in Sources */, + 6268E5BF22D7BBD400D4585A /* Environment.swift in Sources */, 99B9E4BA1D40EC1B00738100 /* main.swift in Sources */, - 99B9E4AF1D40EC1B00738100 /* AssetsFinder.swift in Sources */, - 99B9E4B31D40EC1B00738100 /* ImageOperation.swift in Sources */, + 99B9E4AF1D40EC1B00738100 /* ImageesAssetsFinder.swift in Sources */, + 6268E5C922D7BBD400D4585A /* Expression.swift in Sources */, + 99B9E4B31D40EC1B00738100 /* ImagesOperation.swift in Sources */, 99D74E1D1EC391BE003BFACE /* ImagesClassGenerator.swift in Sources */, 994BF8341F8275DB006A62C3 /* Property.swift in Sources */, + 6268E5D522D7BE6200D4585A /* PathKit.swift in Sources */, + 6268E5C522D7BBD400D4585A /* Node.swift in Sources */, + 6268E5C322D7BBD400D4585A /* _SwiftSupport.swift in Sources */, 994BF8361F8275FF006A62C3 /* Image.swift in Sources */, 99B9E4B11D40EC1B00738100 /* ImagesetParser.swift in Sources */, + 627F4E9D22DF59EF0090E24F /* LocalizationParametersParser.swift in Sources */, 994BF8321F8275B9006A62C3 /* ImageNodeItem.swift in Sources */, - 99D74E341EC39AF8003BFACE /* LocalizationClassGeneratorObjc.swift in Sources */, 99D74E1B1EC3919D003BFACE /* ImagesClassGeneratorSwift.swift in Sources */, - 99B9E4B61D40EC1B00738100 /* LocalizationClassGenerator.swift in Sources */, + 6268E5D022D7BBD400D4585A /* Context.swift in Sources */, 99AF6AF21EC5EB9600C91BA8 /* Logger.swift in Sources */, 99B9E4B91D40EC1B00738100 /* LocalizationParser.swift in Sources */, + 6268E5C622D7BBD400D4585A /* Parser.swift in Sources */, + 6268E5CC22D7BBD400D4585A /* Include.swift in Sources */, 99B9E4B51D40EC1B00738100 /* ImagesValidator.swift in Sources */, + 6268E5C222D7BBD400D4585A /* KeyPath.swift in Sources */, + 6268E5BD22D7BBD400D4585A /* Template.swift in Sources */, 99B9E4BE1D40EC1B00738100 /* Version.swift in Sources */, + 6268E5CA22D7BBD400D4585A /* Inheritence.swift in Sources */, 99B9E4B01D40EC1B00738100 /* Colors.swift in Sources */, 99B9E4B81D40EC1B00738100 /* LocalizationOperation.swift in Sources */, - 99B9E4B21D40EC1B00738100 /* ImageFinder.swift in Sources */, + 6268E5C822D7BBD400D4585A /* Errors.swift in Sources */, + 99B9E4B21D40EC1B00738100 /* ImagesFinder.swift in Sources */, + 6268E5CF22D7BBD400D4585A /* Variable.swift in Sources */, + 6247EE3A22DBB65D0096078D /* LocalizationParameters.swift in Sources */, + 6268E5CB22D7BBD400D4585A /* Tokenizer.swift in Sources */, 99ABD9F41EC7185D00032E1B /* OperationTimer.swift in Sources */, - 9977F4681F834C7E00D77D2C /* Tree.swift in Sources */, + 6252986322D87AD200082EC3 /* Tree.swift in Sources */, + 627F4E9A22DF551A0090E24F /* CommandLineParameter.swift in Sources */, + 6268E5C022D7BBD400D4585A /* ForTag.swift in Sources */, 99B9E4BD1D40EC1B00738100 /* Usage.swift in Sources */, + 622B8F2D22D9D821001FC1E4 /* ImagesNamespace.swift in Sources */, 99B9E4BC1D40EC1B00738100 /* PropertyName.swift in Sources */, - 99D74E361EC39B62003BFACE /* LocalizationClassGeneratorSwift.swift in Sources */, + 6268E5D122D7BBD400D4585A /* FilterTag.swift in Sources */, + 6268E5C122D7BBD400D4585A /* Extension.swift in Sources */, 99667CD11F8C064B00D4A853 /* FileUtils.swift in Sources */, + 62A625DE22E87E8B007AEA56 /* ImagesParametersParser.swift in Sources */, 99B9E4BB1D40EC1B00738100 /* OperationType.swift in Sources */, + 6268E5CD22D7BBD400D4585A /* Lexer.swift in Sources */, 99B9E4B71D40EC1B00738100 /* LocalizationFinder.swift in Sources */, + 6268E5BE22D7BBD400D4585A /* IfTag.swift in Sources */, + 62492B5F22DE5E2700537757 /* ImagesParameters.swift in Sources */, + 62F5CC1422D8C1AA002B28B5 /* LocalizationNamespace.swift in Sources */, + 6268E5C422D7BBD400D4585A /* Loader.swift in Sources */, 99B9E4AE1D40EC1B00738100 /* ArgumentsParser.swift in Sources */, 992D521B1F8418EB00258395 /* ClassName.swift in Sources */, ); @@ -288,6 +614,54 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ + 6252987B22D87DF900082EC3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "regen-tests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.idomizrachi.regen-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + }; + name = Debug; + }; + 6252987C22D87DF900082EC3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = "regen-tests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = "com.idomizrachi.regen-tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + }; + name = Release; + }; 99B9E4981D40EBF900738100 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -314,7 +688,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -339,7 +712,7 @@ SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -369,7 +742,6 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -387,7 +759,7 @@ SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -395,10 +767,11 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = ""; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -406,15 +779,25 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = ""; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + PROVISIONING_PROFILE_SPECIFIER = ""; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 6252987A22D87DF900082EC3 /* Build configuration list for PBXNativeTarget "regen-tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6252987B22D87DF900082EC3 /* Debug */, + 6252987C22D87DF900082EC3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 99B9E48E1D40EBF900738100 /* Build configuration list for PBXProject "regen" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/regen.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/regen.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/regen.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/regen.xcodeproj/xcshareddata/xcschemes/regen.xcscheme b/regen.xcodeproj/xcshareddata/xcschemes/regen.xcscheme new file mode 100644 index 0000000..78219d9 --- /dev/null +++ b/regen.xcodeproj/xcshareddata/xcschemes/regen.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/regen.xcodeproj/xcuserdata/idomizrachi.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/regen.xcodeproj/xcuserdata/idomizrachi.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..fe2b454 --- /dev/null +++ b/regen.xcodeproj/xcuserdata/idomizrachi.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,5 @@ + + + diff --git a/regen.xcodeproj/xcuserdata/idomizrachi.xcuserdatad/xcschemes/xcschememanagement.plist b/regen.xcodeproj/xcuserdata/idomizrachi.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..dd6e997 --- /dev/null +++ b/regen.xcodeproj/xcuserdata/idomizrachi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + regen.xcscheme + + orderHint + 0 + + regen.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + 6252986822D87DA800082EC3 + + primary + + + 99B9E4921D40EBF900738100 + + primary + + + + + diff --git a/regen/ArgumentsParser.swift b/regen/ArgumentsParser.swift index 1ec9cf5..59ab079 100644 --- a/regen/ArgumentsParser.swift +++ b/regen/ArgumentsParser.swift @@ -7,123 +7,70 @@ import Foundation -enum Language { - case ObjC - case Swift +protocol CanBeInitializedWithString { + init?(_ description: String) } +extension Int: CanBeInitializedWithString {} +extension String: CanBeInitializedWithString {} + class ArgumentsParser { - - static let imagesType = "images" - static let localizationType = "localization" - let arguments : [String] - - var output : String? - var language: Language = .ObjC - var verbose:Bool = false - var color:Bool = true - + let operationType: OperationType + init(arguments : [String]) { self.arguments = arguments - parseOutput() - parseLanguage() - parseVerbose() - parseColor() - } - - func operationType() -> OperationType { - if isVersionOperationType() { - return .version - } - if isImagesOperationType() { - return .images - } - if isLocalizationOperationType() { - return .localization - } - return .usage + self.operationType = ArgumentsParser.parseOperationType(arguments: arguments) } - - func isVersionOperationType() -> Bool { - //The first argument contains the executable file path - if arguments.contains("--version") && arguments.count == 2 { - return true - } - return false - } - - func scanType() -> String? { - guard let indexOfScanType = arguments.index(of: "--scanType") else { - return nil - } - if indexOfScanType+1 < arguments.count { - let scanType = arguments[indexOfScanType+1] - return scanType.lowercased() - } - return nil - } - - - func isImagesOperationType() -> Bool { - if let scanType = scanType() { - if scanType == "images" { - return true + + private static func parseOperationType(arguments: [String]) -> OperationType { + let operationTypeKey = parseOperationTypeKey(arguments) + switch operationTypeKey { + case .localization: + guard let parameters = parseLocalizationParameters(arguments) else { + return .usage } - } - return false - } - - func isLocalizationOperationType() -> Bool { - if let scanType = scanType() { - if scanType == "localization" { - return true + return .localization(parameters: parameters) + case .images: + guard let parameters = parseImagesParameters(arguments) else { + return .usage } + return .images(parameters: parameters) + case .version: + return .version + case .usage: + return .usage } - return false } - - func parseOutput() { - guard let indexOfOutput = arguments.index(of: "--output") else { - return - } - if indexOfOutput+1 < arguments.count { - self.output = arguments[indexOfOutput+1] + + private static func parseOperationTypeKey(_ arguments: [String]) -> OperationType.Keys { + guard let firstArgument = arguments.first else { + return .usage } + return OperationType.Keys(rawValue: firstArgument) ?? .usage } - - func parseLanguage() { - guard let indexOfLanguage = arguments.index(of: "--language") else { - return - } - if indexOfLanguage+1 < arguments.count { - let language = arguments[indexOfLanguage+1].lowercased() - if (language == "swift") { - self.language = .Swift - } else { - self.language = .ObjC - } - } + + private static func parseLocalizationParameters(_ arguments: [String]) -> Localization.Parameters? { + let parser = LocalizationParametersParser(arguments: arguments) + return parser.parse() } - - func parseVerbose() { - if arguments.index(of: "--verbose") != nil { - self.verbose = true - } else if arguments.index(of: "-v") != nil { - self.verbose = true - } else { - self.verbose = false - } + + private static func parseImagesParameters(_ arguments: [String]) -> Images.Parameters? { + let parser = ImagesParametersParser(arguments: arguments) + return parser.parse() } - - func parseColor() { - if arguments.index(of: "--nocolor") != nil { - self.color = false - } else { - self.color = true + +// private static func parseAssetsFile(arguments: [String]) -> String? { +// let assetsFile: String? = tryParse("--assets-file", from: arguments) +// return assetsFile +// } + + private static func isVersionOperation(_ arguments: [String]) -> Bool { + guard let firstArgument = arguments.first else { + return false } + return firstArgument.lowercased() == OperationType.Keys.version.rawValue } - } diff --git a/regen/Utilities/Colors.swift b/regen/Dependencies/Colors/Colors.swift similarity index 98% rename from regen/Utilities/Colors.swift rename to regen/Dependencies/Colors/Colors.swift index 07827c8..ed20aac 100644 --- a/regen/Utilities/Colors.swift +++ b/regen/Dependencies/Colors/Colors.swift @@ -4,6 +4,7 @@ // // Created by Chad Scira on 3/3/15. // +// https://github.com/icodeforlove/Colors import Foundation @@ -114,7 +115,7 @@ func parseExistingANSI(_ string: String) -> [ANSIGroup] { for match in matches { var parts = matchesForRegexInText("\\u001B\\[([^m]*)m(.+?)\\u001B\\[0m", text: match), - codes = parts[1].characters.split {$0 == ";"}.map { String($0) }, + codes = parts[1].split {$0 == ";"}.map { String($0) }, string = parts[2] results.append(ANSIGroup(codes: codes.filter { Int($0) != nil }.map { Int($0)! }, string: string)) diff --git a/regen/Dependencies/PathKit/PathKit.swift b/regen/Dependencies/PathKit/PathKit.swift new file mode 100755 index 0000000..7fe7612 --- /dev/null +++ b/regen/Dependencies/PathKit/PathKit.swift @@ -0,0 +1,795 @@ +// PathKit - Effortless path operations + +#if os(Linux) +import Glibc + +let system_glob = Glibc.glob +#else +import Darwin + +let system_glob = Darwin.glob +#endif + +import Foundation + + +/// Represents a filesystem path. +public struct Path { + /// The character used by the OS to separate two path elements + public static let separator = "/" + + /// The underlying string representation + internal var path: String + + internal static var fileManager = FileManager.default + + internal var fileSystemInfo: FileSystemInfo = DefaultFileSystemInfo() + + // MARK: Init + + public init() { + self.path = "" + } + + /// Create a Path from a given String + public init(_ path: String) { + self.path = path + } + + /// Create a Path by joining multiple path components together + public init(components: S) where S.Iterator.Element == String { + if components.isEmpty { + path = "." + } else if components.first == Path.separator && components.count > 1 { + let p = components.joined(separator: Path.separator) + path = p.substring(from: p.index(after: p.startIndex)) + } else { + path = components.joined(separator: Path.separator) + } + } +} + + +// MARK: StringLiteralConvertible + +extension Path : ExpressibleByStringLiteral { + public typealias ExtendedGraphemeClusterLiteralType = StringLiteralType + public typealias UnicodeScalarLiteralType = StringLiteralType + + public init(extendedGraphemeClusterLiteral path: StringLiteralType) { + self.init(stringLiteral: path) + } + + public init(unicodeScalarLiteral path: StringLiteralType) { + self.init(stringLiteral: path) + } + + public init(stringLiteral value: StringLiteralType) { + self.path = value + } +} + + +// MARK: CustomStringConvertible + +extension Path : CustomStringConvertible { + public var description: String { + return self.path + } +} + + +// MARK: Conversion + +extension Path { + public var string: String { + return self.path + } + + public var url: URL { + return URL(fileURLWithPath: path) + } +} + + +// MARK: Hashable + +extension Path : Hashable { + public var hashValue: Int { + return path.hashValue + } +} + + +// MARK: Path Info + +extension Path { + /// Test whether a path is absolute. + /// + /// - Returns: `true` iff the path begings with a slash + /// + public var isAbsolute: Bool { + return path.hasPrefix(Path.separator) + } + + /// Test whether a path is relative. + /// + /// - Returns: `true` iff a path is relative (not absolute) + /// + public var isRelative: Bool { + return !isAbsolute + } + + /// Concatenates relative paths to the current directory and derives the normalized path + /// + /// - Returns: the absolute path in the actual filesystem + /// + public func absolute() -> Path { + if isAbsolute { + return normalize() + } + + let expandedPath = Path(NSString(string: self.path).expandingTildeInPath) + if expandedPath.isAbsolute { + return expandedPath.normalize() + } + + return (Path.current + self).normalize() + } + + /// Normalizes the path, this cleans up redundant ".." and ".", double slashes + /// and resolves "~". + /// + /// - Returns: a new path made by removing extraneous path components from the underlying String + /// representation. + /// + public func normalize() -> Path { + return Path(NSString(string: self.path).standardizingPath) + } + + /// De-normalizes the path, by replacing the current user home directory with "~". + /// + /// - Returns: a new path made by removing extraneous path components from the underlying String + /// representation. + /// + public func abbreviate() -> Path { + let rangeOptions: String.CompareOptions = fileSystemInfo.isFSCaseSensitiveAt(path: self) ? + [.anchored] : [.anchored, .caseInsensitive] + let home = Path.home.string + guard let homeRange = self.path.range(of: home, options: rangeOptions) else { return self } + let withoutHome = Path(self.path.replacingCharacters(in: homeRange, with: "")) + + if withoutHome.path.isEmpty || withoutHome.path == Path.separator { + return Path("~") + } else if withoutHome.isAbsolute { + return Path("~" + withoutHome.path) + } else { + return Path("~") + withoutHome.path + } + } + + /// Returns the path of the item pointed to by a symbolic link. + /// + /// - Returns: the path of directory or file to which the symbolic link refers + /// + public func symlinkDestination() throws -> Path { + let symlinkDestination = try Path.fileManager.destinationOfSymbolicLink(atPath: path) + let symlinkPath = Path(symlinkDestination) + if symlinkPath.isRelative { + return self + ".." + symlinkPath + } else { + return symlinkPath + } + } +} + +internal protocol FileSystemInfo { + func isFSCaseSensitiveAt(path: Path) -> Bool +} + +internal struct DefaultFileSystemInfo: FileSystemInfo { + func isFSCaseSensitiveAt(path: Path) -> Bool { + #if os(Linux) + // URL resourceValues(forKeys:) is not supported on non-darwin platforms... + // But we can (fairly?) safely assume for now that the Linux FS is case sensitive. + // TODO: refactor when/if resourceValues is available, or look into using something + // like stat or pathconf to determine if the mountpoint is case sensitive. + return true + #else + var isCaseSensitive = false + // Calling resourceValues will fail if the path does not exist on the filesystem, which + // makes sense, but means we can only guarantee the return value is correct if the + // path actually exists. + if let resourceValues = try? path.url.resourceValues(forKeys: [.volumeSupportsCaseSensitiveNamesKey]) { + isCaseSensitive = resourceValues.volumeSupportsCaseSensitiveNames ?? isCaseSensitive + } + return isCaseSensitive + #endif + } +} + +// MARK: Path Components + +extension Path { + /// The last path component + /// + /// - Returns: the last path component + /// + public var lastComponent: String { + return NSString(string: path).lastPathComponent + } + + /// The last path component without file extension + /// + /// - Note: This returns "." for ".." on Linux, and ".." on Apple platforms. + /// + /// - Returns: the last path component without file extension + /// + public var lastComponentWithoutExtension: String { + return NSString(string: lastComponent).deletingPathExtension + } + + /// Splits the string representation on the directory separator. + /// Absolute paths remain the leading slash as first component. + /// + /// - Returns: all path components + /// + public var components: [String] { + return NSString(string: path).pathComponents + } + + /// The file extension behind the last dot of the last component. + /// + /// - Returns: the file extension + /// + public var `extension`: String? { + let pathExtension = NSString(string: path).pathExtension + if pathExtension.isEmpty { + return nil + } + + return pathExtension + } +} + + +// MARK: File Info + +extension Path { + /// Test whether a file or directory exists at a specified path + /// + /// - Returns: `false` iff the path doesn't exist on disk or its existence could not be + /// determined + /// + public var exists: Bool { + return Path.fileManager.fileExists(atPath: self.path) + } + + /// Test whether a path is a directory. + /// + /// - Returns: `true` if the path is a directory or a symbolic link that points to a directory; + /// `false` if the path is not a directory or the path doesn't exist on disk or its existence + /// could not be determined + /// + public var isDirectory: Bool { + var directory = ObjCBool(false) + guard Path.fileManager.fileExists(atPath: normalize().path, isDirectory: &directory) else { + return false + } +#if os(Linux) + return directory +#else + return directory.boolValue +#endif + } + + /// Test whether a path is a regular file. + /// + /// - Returns: `true` if the path is neither a directory nor a symbolic link that points to a + /// directory; `false` if the path is a directory or a symbolic link that points to a + /// directory or the path doesn't exist on disk or its existence + /// could not be determined + /// + public var isFile: Bool { + var directory = ObjCBool(false) + guard Path.fileManager.fileExists(atPath: normalize().path, isDirectory: &directory) else { + return false + } +#if os(Linux) + return !directory +#else + return !directory.boolValue +#endif + } + + /// Test whether a path is a symbolic link. + /// + /// - Returns: `true` if the path is a symbolic link; `false` if the path doesn't exist on disk + /// or its existence could not be determined + /// + public var isSymlink: Bool { + do { + let _ = try Path.fileManager.destinationOfSymbolicLink(atPath: path) + return true + } catch { + return false + } + } + + /// Test whether a path is readable + /// + /// - Returns: `true` if the current process has read privileges for the file at path; + /// otherwise `false` if the process does not have read privileges or the existence of the + /// file could not be determined. + /// + public var isReadable: Bool { + return Path.fileManager.isReadableFile(atPath: self.path) + } + + /// Test whether a path is writeable + /// + /// - Returns: `true` if the current process has write privileges for the file at path; + /// otherwise `false` if the process does not have write privileges or the existence of the + /// file could not be determined. + /// + public var isWritable: Bool { + return Path.fileManager.isWritableFile(atPath: self.path) + } + + /// Test whether a path is executable + /// + /// - Returns: `true` if the current process has execute privileges for the file at path; + /// otherwise `false` if the process does not have execute privileges or the existence of the + /// file could not be determined. + /// + public var isExecutable: Bool { + return Path.fileManager.isExecutableFile(atPath: self.path) + } + + /// Test whether a path is deletable + /// + /// - Returns: `true` if the current process has delete privileges for the file at path; + /// otherwise `false` if the process does not have delete privileges or the existence of the + /// file could not be determined. + /// + public var isDeletable: Bool { + return Path.fileManager.isDeletableFile(atPath: self.path) + } +} + + +// MARK: File Manipulation + +extension Path { + /// Create the directory. + /// + /// - Note: This method fails if any of the intermediate parent directories does not exist. + /// This method also fails if any of the intermediate path elements corresponds to a file and + /// not a directory. + /// + public func mkdir() throws -> () { + try Path.fileManager.createDirectory(atPath: self.path, withIntermediateDirectories: false, attributes: nil) + } + + /// Create the directory and any intermediate parent directories that do not exist. + /// + /// - Note: This method fails if any of the intermediate path elements corresponds to a file and + /// not a directory. + /// + public func mkpath() throws -> () { + try Path.fileManager.createDirectory(atPath: self.path, withIntermediateDirectories: true, attributes: nil) + } + + /// Delete the file or directory. + /// + /// - Note: If the path specifies a directory, the contents of that directory are recursively + /// removed. + /// + public func delete() throws -> () { + try Path.fileManager.removeItem(atPath: self.path) + } + + /// Move the file or directory to a new location synchronously. + /// + /// - Parameter destination: The new path. This path must include the name of the file or + /// directory in its new location. + /// + public func move(_ destination: Path) throws -> () { + try Path.fileManager.moveItem(atPath: self.path, toPath: destination.path) + } + + /// Copy the file or directory to a new location synchronously. + /// + /// - Parameter destination: The new path. This path must include the name of the file or + /// directory in its new location. + /// + public func copy(_ destination: Path) throws -> () { + try Path.fileManager.copyItem(atPath: self.path, toPath: destination.path) + } + + /// Creates a hard link at a new destination. + /// + /// - Parameter destination: The location where the link will be created. + /// + public func link(_ destination: Path) throws -> () { + try Path.fileManager.linkItem(atPath: self.path, toPath: destination.path) + } + + /// Creates a symbolic link at a new destination. + /// + /// - Parameter destintation: The location where the link will be created. + /// + public func symlink(_ destination: Path) throws -> () { + try Path.fileManager.createSymbolicLink(atPath: self.path, withDestinationPath: destination.path) + } +} + + +// MARK: Current Directory + +extension Path { + /// The current working directory of the process + /// + /// - Returns: the current working directory of the process + /// + public static var current: Path { + get { + return self.init(Path.fileManager.currentDirectoryPath) + } + set { + _ = Path.fileManager.changeCurrentDirectoryPath(newValue.description) + } + } + + /// Changes the current working directory of the process to the path during the execution of the + /// given block. + /// + /// - Note: The original working directory is restored when the block returns or throws. + /// - Parameter closure: A closure to be executed while the current directory is configured to + /// the path. + /// + public func chdir(closure: () throws -> ()) rethrows { + let previous = Path.current + Path.current = self + defer { Path.current = previous } + try closure() + } +} + + +// MARK: Temporary + +extension Path { + /// - Returns: the path to either the user’s or application’s home directory, + /// depending on the platform. + /// + public static var home: Path { + return Path(NSHomeDirectory()) + } + + /// - Returns: the path of the temporary directory for the current user. + /// + public static var temporary: Path { + return Path(NSTemporaryDirectory()) + } + + /// - Returns: the path of a temporary directory unique for the process. + /// - Note: Based on `NSProcessInfo.globallyUniqueString`. + /// + public static func processUniqueTemporary() throws -> Path { + let path = temporary + ProcessInfo.processInfo.globallyUniqueString + if !path.exists { + try path.mkdir() + } + return path + } + + /// - Returns: the path of a temporary directory unique for each call. + /// - Note: Based on `NSUUID`. + /// + public static func uniqueTemporary() throws -> Path { + let path = try processUniqueTemporary() + UUID().uuidString + try path.mkdir() + return path + } +} + + +// MARK: Contents + +extension Path { + /// Reads the file. + /// + /// - Returns: the contents of the file at the specified path. + /// + public func read() throws -> Data { + return try Data(contentsOf: self.url, options: NSData.ReadingOptions(rawValue: 0)) + } + + /// Reads the file contents and encoded its bytes to string applying the given encoding. + /// + /// - Parameter encoding: the encoding which should be used to decode the data. + /// (by default: `NSUTF8StringEncoding`) + /// + /// - Returns: the contents of the file at the specified path as string. + /// + public func read(_ encoding: String.Encoding = String.Encoding.utf8) throws -> String { + return try NSString(contentsOfFile: path, encoding: encoding.rawValue).substring(from: 0) as String + } + + /// Write a file. + /// + /// - Note: Works atomically: the data is written to a backup file, and then — assuming no + /// errors occur — the backup file is renamed to the name specified by path. + /// + /// - Parameter data: the contents to write to file. + /// + public func write(_ data: Data) throws { + try data.write(to: normalize().url, options: .atomic) + } + + /// Reads the file. + /// + /// - Note: Works atomically: the data is written to a backup file, and then — assuming no + /// errors occur — the backup file is renamed to the name specified by path. + /// + /// - Parameter string: the string to write to file. + /// + /// - Parameter encoding: the encoding which should be used to represent the string as bytes. + /// (by default: `NSUTF8StringEncoding`) + /// + /// - Returns: the contents of the file at the specified path as string. + /// + public func write(_ string: String, encoding: String.Encoding = String.Encoding.utf8) throws { + try string.write(toFile: normalize().path, atomically: true, encoding: encoding) + } +} + + +// MARK: Traversing + +extension Path { + /// Get the parent directory + /// + /// - Returns: the normalized path of the parent directory + /// + public func parent() -> Path { + return self + ".." + } + + /// Performs a shallow enumeration in a directory + /// + /// - Returns: paths to all files, directories and symbolic links contained in the directory + /// + public func children() throws -> [Path] { + return try Path.fileManager.contentsOfDirectory(atPath: path).map { + self + Path($0) + } + } + + /// Performs a deep enumeration in a directory + /// + /// - Returns: paths to all files, directories and symbolic links contained in the directory or + /// any subdirectory. + /// + public func recursiveChildren() throws -> [Path] { + return try Path.fileManager.subpathsOfDirectory(atPath: path).map { + self + Path($0) + } + } +} + + +// MARK: Globbing + +extension Path { + public static func glob(_ pattern: String) -> [Path] { + var gt = glob_t() + let cPattern = strdup(pattern) + defer { + globfree(>) + free(cPattern) + } + + let flags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK + if system_glob(cPattern, flags, nil, >) == 0 { +#if os(Linux) + let matchc = gt.gl_pathc +#else + let matchc = gt.gl_matchc +#endif + return (0.. [Path] { + return Path.glob((self + pattern).description) + } +} + + +// MARK: SequenceType + +extension Path : Sequence { + public struct DirectoryEnumerationOptions : OptionSet { + public let rawValue: UInt + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + public static var skipsSubdirectoryDescendants = DirectoryEnumerationOptions(rawValue: FileManager.DirectoryEnumerationOptions.skipsSubdirectoryDescendants.rawValue) + public static var skipsPackageDescendants = DirectoryEnumerationOptions(rawValue: FileManager.DirectoryEnumerationOptions.skipsPackageDescendants.rawValue) + public static var skipsHiddenFiles = DirectoryEnumerationOptions(rawValue: FileManager.DirectoryEnumerationOptions.skipsHiddenFiles.rawValue) + } + + /// Represents a path sequence with specific enumeration options + public struct PathSequence : Sequence { + private var path: Path + private var options: DirectoryEnumerationOptions + init(path: Path, options: DirectoryEnumerationOptions) { + self.path = path + self.options = options + } + + public func makeIterator() -> DirectoryEnumerator { + return DirectoryEnumerator(path: path, options: options) + } + } + + /// Enumerates the contents of a directory, returning the paths of all files and directories + /// contained within that directory. These paths are relative to the directory. + public struct DirectoryEnumerator : IteratorProtocol { + public typealias Element = Path + + let path: Path + let directoryEnumerator: FileManager.DirectoryEnumerator? + + init(path: Path, options mask: DirectoryEnumerationOptions = []) { + let options = FileManager.DirectoryEnumerationOptions(rawValue: mask.rawValue) + self.path = path + self.directoryEnumerator = Path.fileManager.enumerator(at: path.url, includingPropertiesForKeys: nil, options: options) + } + + public func next() -> Path? { + let next = directoryEnumerator?.nextObject() + + if let next = next as? URL { + return Path(next.path) + } + return nil + } + + /// Skip recursion into the most recently obtained subdirectory. + public func skipDescendants() { + directoryEnumerator?.skipDescendants() + } + } + + /// Perform a deep enumeration of a directory. + /// + /// - Returns: a directory enumerator that can be used to perform a deep enumeration of the + /// directory. + /// + public func makeIterator() -> DirectoryEnumerator { + return DirectoryEnumerator(path: self) + } + + /// Perform a deep enumeration of a directory. + /// + /// - Parameter options: FileManager directory enumerator options. + /// + /// - Returns: a path sequence that can be used to perform a deep enumeration of the + /// directory. + /// + public func iterateChildren(options: DirectoryEnumerationOptions = []) -> PathSequence { + return PathSequence(path: self, options: options) + } +} + + +// MARK: Equatable + +extension Path : Equatable {} + +/// Determines if two paths are identical +/// +/// - Note: The comparison is string-based. Be aware that two different paths (foo.txt and +/// ./foo.txt) can refer to the same file. +/// +public func ==(lhs: Path, rhs: Path) -> Bool { + return lhs.path == rhs.path +} + + +// MARK: Pattern Matching + +/// Implements pattern-matching for paths. +/// +/// - Returns: `true` iff one of the following conditions is true: +/// - the paths are equal (based on `Path`'s `Equatable` implementation) +/// - the paths can be normalized to equal Paths. +/// +public func ~=(lhs: Path, rhs: Path) -> Bool { + return lhs == rhs + || lhs.normalize() == rhs.normalize() +} + + +// MARK: Comparable + +extension Path : Comparable {} + +/// Defines a strict total order over Paths based on their underlying string representation. +public func <(lhs: Path, rhs: Path) -> Bool { + return lhs.path < rhs.path +} + + +// MARK: Operators + +/// Appends a Path fragment to another Path to produce a new Path +public func +(lhs: Path, rhs: Path) -> Path { + return lhs.path + rhs.path +} + +/// Appends a String fragment to another Path to produce a new Path +public func +(lhs: Path, rhs: String) -> Path { + return lhs.path + rhs +} + +/// Appends a String fragment to another String to produce a new Path +private func +(lhs: String, rhs: String) -> Path { + if rhs.hasPrefix(Path.separator) { + // Absolute paths replace relative paths + return Path(rhs) + } else { + var lSlice = NSString(string: lhs).pathComponents.fullSlice + var rSlice = NSString(string: rhs).pathComponents.fullSlice + + // Get rid of trailing "/" at the left side + if lSlice.count > 1 && lSlice.last == Path.separator { + lSlice.removeLast() + } + + // Advance after the first relevant "." + lSlice = lSlice.filter { $0 != "." }.fullSlice + rSlice = rSlice.filter { $0 != "." }.fullSlice + + // Eats up trailing components of the left and leading ".." of the right side + while lSlice.last != ".." && !lSlice.isEmpty && rSlice.first == ".." { + if lSlice.count > 1 || lSlice.first != Path.separator { + // A leading "/" is never popped + lSlice.removeLast() + } + if !rSlice.isEmpty { + rSlice.removeFirst() + } + + switch (lSlice.isEmpty, rSlice.isEmpty) { + case (true, _): + break + case (_, true): + break + default: + continue + } + } + + return Path(components: lSlice + rSlice) + } +} + +extension Array { + var fullSlice: ArraySlice { + return self[self.indices.suffix(from: 0)] + } +} diff --git a/regen/Dependencies/Stencil/Context.swift b/regen/Dependencies/Stencil/Context.swift new file mode 100755 index 0000000..007cf68 --- /dev/null +++ b/regen/Dependencies/Stencil/Context.swift @@ -0,0 +1,68 @@ +/// A container for template variables. +public class Context { + var dictionaries: [[String: Any?]] + + public let environment: Environment + + init(dictionary: [String: Any] = [:], environment: Environment? = nil) { + if !dictionary.isEmpty { + dictionaries = [dictionary] + } else { + dictionaries = [] + } + + self.environment = environment ?? Environment() + } + + public subscript(key: String) -> Any? { + /// Retrieves a variable's value, starting at the current context and going upwards + get { + for dictionary in Array(dictionaries.reversed()) { + if let value = dictionary[key] { + return value + } + } + + return nil + } + + /// Set a variable in the current context, deleting the variable if it's nil + set(value) { + if var dictionary = dictionaries.popLast() { + dictionary[key] = value + dictionaries.append(dictionary) + } + } + } + + /// Push a new level into the Context + fileprivate func push(_ dictionary: [String: Any] = [:]) { + dictionaries.append(dictionary) + } + + /// Pop the last level off of the Context + fileprivate func pop() -> [String: Any?]? { + return dictionaries.popLast() + } + + /// Push a new level onto the context for the duration of the execution of the given closure + public func push(dictionary: [String: Any] = [:], closure: (() throws -> Result)) rethrows -> Result { + push(dictionary) + defer { _ = pop() } + return try closure() + } + + public func flatten() -> [String: Any] { + var accumulator: [String: Any] = [:] + + for dictionary in dictionaries { + for (key, value) in dictionary { + if let value = value { + accumulator.updateValue(value, forKey: key) + } + } + } + + return accumulator + } +} diff --git a/regen/Dependencies/Stencil/Environment.swift b/regen/Dependencies/Stencil/Environment.swift new file mode 100755 index 0000000..0c2c72e --- /dev/null +++ b/regen/Dependencies/Stencil/Environment.swift @@ -0,0 +1,48 @@ +public struct Environment { + public let templateClass: Template.Type + public var extensions: [Extension] + + public var loader: Loader? + + public init(loader: Loader? = nil, + extensions: [Extension] = [], + templateClass: Template.Type = Template.self) { + + self.templateClass = templateClass + self.loader = loader + self.extensions = extensions + [DefaultExtension()] + } + + public func loadTemplate(name: String) throws -> Template { + if let loader = loader { + return try loader.loadTemplate(name: name, environment: self) + } else { + throw TemplateDoesNotExist(templateNames: [name], loader: nil) + } + } + + public func loadTemplate(names: [String]) throws -> Template { + if let loader = loader { + return try loader.loadTemplate(names: names, environment: self) + } else { + throw TemplateDoesNotExist(templateNames: names, loader: nil) + } + } + + public func renderTemplate(name: String, context: [String: Any] = [:]) throws -> String { + let template = try loadTemplate(name: name) + return try render(template: template, context: context) + } + + public func renderTemplate(string: String, context: [String: Any] = [:]) throws -> String { + let template = templateClass.init(templateString: string, environment: self) + return try render(template: template, context: context) + } + + func render(template: Template, context: [String: Any]) throws -> String { + // update template environment as it can be created from string literal with default environment + template.environment = self + return try template.render(context) + } + +} diff --git a/regen/Dependencies/Stencil/Errors.swift b/regen/Dependencies/Stencil/Errors.swift new file mode 100755 index 0000000..9c1b584 --- /dev/null +++ b/regen/Dependencies/Stencil/Errors.swift @@ -0,0 +1,83 @@ +public class TemplateDoesNotExist: Error, CustomStringConvertible { + let templateNames: [String] + let loader: Loader? + + public init(templateNames: [String], loader: Loader? = nil) { + self.templateNames = templateNames + self.loader = loader + } + + public var description: String { + let templates = templateNames.joined(separator: ", ") + + if let loader = loader { + return "Template named `\(templates)` does not exist in loader \(loader)" + } + + return "Template named `\(templates)` does not exist. No loaders found" + } +} + +public struct TemplateSyntaxError: Error, Equatable, CustomStringConvertible { + public let reason: String + public var description: String { return reason } + public internal(set) var token: Token? + public internal(set) var stackTrace: [Token] + public var templateName: String? { return token?.sourceMap.filename } + var allTokens: [Token] { + return stackTrace + (token.map { [$0] } ?? []) + } + + public init(reason: String, token: Token? = nil, stackTrace: [Token] = []) { + self.reason = reason + self.stackTrace = stackTrace + self.token = token + } + + public init(_ description: String) { + self.init(reason: description) + } +} + +extension Error { + func withToken(_ token: Token?) -> Error { + if var error = self as? TemplateSyntaxError { + error.token = error.token ?? token + return error + } else { + return TemplateSyntaxError(reason: "\(self)", token: token) + } + } +} + +public protocol ErrorReporter: AnyObject { + func renderError(_ error: Error) -> String +} + +open class SimpleErrorReporter: ErrorReporter { + + open func renderError(_ error: Error) -> String { + guard let templateError = error as? TemplateSyntaxError else { return error.localizedDescription } + + func describe(token: Token) -> String { + let templateName = token.sourceMap.filename ?? "" + let location = token.sourceMap.location + let highlight = """ + \(String(Array(repeating: " ", count: location.lineOffset)))\ + ^\(String(Array(repeating: "~", count: max(token.contents.count - 1, 0)))) + """ + + return """ + \(templateName)\(location.lineNumber):\(location.lineOffset): error: \(templateError.reason) + \(location.content) + \(highlight) + """ + } + + var descriptions = templateError.stackTrace.reduce([]) { $0 + [describe(token: $1)] } + let description = templateError.token.map(describe(token:)) ?? templateError.reason + descriptions.append(description) + return descriptions.joined(separator: "\n") + } + +} diff --git a/regen/Dependencies/Stencil/Expression.swift b/regen/Dependencies/Stencil/Expression.swift new file mode 100755 index 0000000..045b34c --- /dev/null +++ b/regen/Dependencies/Stencil/Expression.swift @@ -0,0 +1,322 @@ +public protocol Expression: CustomStringConvertible { + func evaluate(context: Context) throws -> Bool +} + +protocol InfixOperator: Expression { + init(lhs: Expression, rhs: Expression) +} + +protocol PrefixOperator: Expression { + init(expression: Expression) +} + +final class StaticExpression: Expression, CustomStringConvertible { + let value: Bool + + init(value: Bool) { + self.value = value + } + + func evaluate(context: Context) throws -> Bool { + return value + } + + var description: String { + return "\(value)" + } +} + +final class VariableExpression: Expression, CustomStringConvertible { + let variable: Resolvable + + init(variable: Resolvable) { + self.variable = variable + } + + var description: String { + return "(variable: \(variable))" + } + + /// Resolves a variable in the given context as boolean + func resolve(context: Context, variable: Resolvable) throws -> Bool { + let result = try variable.resolve(context) + var truthy = false + + if let result = result as? [Any] { + truthy = !result.isEmpty + } else if let result = result as? [String: Any] { + truthy = !result.isEmpty + } else if let result = result as? Bool { + truthy = result + } else if let result = result as? String { + truthy = !result.isEmpty + } else if let value = result, let result = toNumber(value: value) { + truthy = result > 0 + } else if result != nil { + truthy = true + } + + return truthy + } + + func evaluate(context: Context) throws -> Bool { + return try resolve(context: context, variable: variable) + } +} + +final class NotExpression: Expression, PrefixOperator, CustomStringConvertible { + let expression: Expression + + init(expression: Expression) { + self.expression = expression + } + + var description: String { + return "not \(expression)" + } + + func evaluate(context: Context) throws -> Bool { + return try !expression.evaluate(context: context) + } +} + +final class InExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) in \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + let rhsValue = try rhs.variable.resolve(context) + + if let lhs = lhsValue as? AnyHashable, let rhs = rhsValue as? [AnyHashable] { + return rhs.contains(lhs) + } else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableClosedRange { + return rhs.contains(lhs) + } else if let lhs = lhsValue as? Int, let rhs = rhsValue as? CountableRange { + return rhs.contains(lhs) + } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { + return rhs.contains(lhs) + } else if lhsValue == nil && rhsValue == nil { + return true + } + } + + return false + } + +} + +final class OrExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) or \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + let lhs = try self.lhs.evaluate(context: context) + if lhs { + return lhs + } + + return try rhs.evaluate(context: context) + } +} + +final class AndExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) and \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + let lhs = try self.lhs.evaluate(context: context) + if !lhs { + return lhs + } + + return try rhs.evaluate(context: context) + } +} + +class EqualityExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + required init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) == \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + let rhsValue = try rhs.variable.resolve(context) + + if let lhs = lhsValue, let rhs = rhsValue { + if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { + return lhs == rhs + } else if let lhs = lhsValue as? String, let rhs = rhsValue as? String { + return lhs == rhs + } else if let lhs = lhsValue as? Bool, let rhs = rhsValue as? Bool { + return lhs == rhs + } + } else if lhsValue == nil && rhsValue == nil { + return true + } + } + + return false + } +} + +class NumericExpression: Expression, InfixOperator, CustomStringConvertible { + let lhs: Expression + let rhs: Expression + + required init(lhs: Expression, rhs: Expression) { + self.lhs = lhs + self.rhs = rhs + } + + var description: String { + return "(\(lhs) \(symbol) \(rhs))" + } + + func evaluate(context: Context) throws -> Bool { + if let lhs = lhs as? VariableExpression, let rhs = rhs as? VariableExpression { + let lhsValue = try lhs.variable.resolve(context) + let rhsValue = try rhs.variable.resolve(context) + + if let lhs = lhsValue, let rhs = rhsValue { + if let lhs = toNumber(value: lhs), let rhs = toNumber(value: rhs) { + return compare(lhs: lhs, rhs: rhs) + } + } + } + + return false + } + + var symbol: String { + return "" + } + + func compare(lhs: Number, rhs: Number) -> Bool { + return false + } +} + +class MoreThanExpression: NumericExpression { + override var symbol: String { + return ">" + } + + override func compare(lhs: Number, rhs: Number) -> Bool { + return lhs > rhs + } +} + +class MoreThanEqualExpression: NumericExpression { + override var symbol: String { + return ">=" + } + + override func compare(lhs: Number, rhs: Number) -> Bool { + return lhs >= rhs + } +} + +class LessThanExpression: NumericExpression { + override var symbol: String { + return "<" + } + + override func compare(lhs: Number, rhs: Number) -> Bool { + return lhs < rhs + } +} + +class LessThanEqualExpression: NumericExpression { + override var symbol: String { + return "<=" + } + + override func compare(lhs: Number, rhs: Number) -> Bool { + return lhs <= rhs + } +} + +class InequalityExpression: EqualityExpression { + override var description: String { + return "(\(lhs) != \(rhs))" + } + + override func evaluate(context: Context) throws -> Bool { + return try !super.evaluate(context: context) + } +} + +// swiftlint:disable:next cyclomatic_complexity +func toNumber(value: Any) -> Number? { + if let value = value as? Float { + return Number(value) + } else if let value = value as? Double { + return Number(value) + } else if let value = value as? UInt { + return Number(value) + } else if let value = value as? Int { + return Number(value) + } else if let value = value as? Int8 { + return Number(value) + } else if let value = value as? Int16 { + return Number(value) + } else if let value = value as? Int32 { + return Number(value) + } else if let value = value as? Int64 { + return Number(value) + } else if let value = value as? UInt8 { + return Number(value) + } else if let value = value as? UInt16 { + return Number(value) + } else if let value = value as? UInt32 { + return Number(value) + } else if let value = value as? UInt64 { + return Number(value) + } else if let value = value as? Number { + return value + } else if let value = value as? Float64 { + return Number(value) + } else if let value = value as? Float32 { + return Number(value) + } + + return nil +} diff --git a/regen/Dependencies/Stencil/Extension.swift b/regen/Dependencies/Stencil/Extension.swift new file mode 100755 index 0000000..a91b4ab --- /dev/null +++ b/regen/Dependencies/Stencil/Extension.swift @@ -0,0 +1,99 @@ +open class Extension { + typealias TagParser = (TokenParser, Token) throws -> NodeType + var tags = [String: TagParser]() + + var filters = [String: Filter]() + + public init() { + } + + /// Registers a new template tag + public func registerTag(_ name: String, parser: @escaping (TokenParser, Token) throws -> NodeType) { + tags[name] = parser + } + + /// Registers a simple template tag with a name and a handler + public func registerSimpleTag(_ name: String, handler: @escaping (Context) throws -> String) { + registerTag(name) { _, token in + SimpleNode(token: token, handler: handler) + } + } + + /// Registers boolean filter with it's negative counterpart + // swiftlint:disable:next discouraged_optional_boolean + public func registerFilter(name: String, negativeFilterName: String, filter: @escaping (Any?) throws -> Bool?) { + filters[name] = .simple(filter) + filters[negativeFilterName] = .simple { + guard let result = try filter($0) else { return nil } + return !result + } + } + + /// Registers a template filter with the given name + public func registerFilter(_ name: String, filter: @escaping (Any?) throws -> Any?) { + filters[name] = .simple(filter) + } + + /// Registers a template filter with the given name + public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?]) throws -> Any?) { + filters[name] = .arguments({ value, args, _ in try filter(value, args) }) + } + + /// Registers a template filter with the given name + public func registerFilter(_ name: String, filter: @escaping (Any?, [Any?], Context) throws -> Any?) { + filters[name] = .arguments(filter) + } +} + +class DefaultExtension: Extension { + override init() { + super.init() + registerDefaultTags() + registerDefaultFilters() + } + + fileprivate func registerDefaultTags() { + registerTag("for", parser: ForNode.parse) + registerTag("if", parser: IfNode.parse) + registerTag("ifnot", parser: IfNode.parse_ifnot) + #if !os(Linux) + registerTag("now", parser: NowNode.parse) + #endif + registerTag("include", parser: IncludeNode.parse) + registerTag("extends", parser: ExtendsNode.parse) + registerTag("block", parser: BlockNode.parse) + registerTag("filter", parser: FilterNode.parse) + } + + fileprivate func registerDefaultFilters() { + registerFilter("default", filter: defaultFilter) + registerFilter("capitalize", filter: capitalise) + registerFilter("uppercase", filter: uppercase) + registerFilter("lowercase", filter: lowercase) + registerFilter("join", filter: joinFilter) + registerFilter("split", filter: splitFilter) + registerFilter("indent", filter: indentFilter) + registerFilter("filter", filter: filterFilter) + } +} + +protocol FilterType { + func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? +} + +enum Filter: FilterType { + case simple(((Any?) throws -> Any?)) + case arguments(((Any?, [Any?], Context) throws -> Any?)) + + func invoke(value: Any?, arguments: [Any?], context: Context) throws -> Any? { + switch self { + case let .simple(filter): + if !arguments.isEmpty { + throw TemplateSyntaxError("Can't invoke filter with an argument") + } + return try filter(value) + case let .arguments(filter): + return try filter(value, arguments, context) + } + } +} diff --git a/regen/Dependencies/Stencil/FilterTag.swift b/regen/Dependencies/Stencil/FilterTag.swift new file mode 100755 index 0000000..e623b53 --- /dev/null +++ b/regen/Dependencies/Stencil/FilterTag.swift @@ -0,0 +1,36 @@ +class FilterNode: NodeType { + let resolvable: Resolvable + let nodes: [NodeType] + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components + + guard bits.count == 2 else { + throw TemplateSyntaxError("'filter' tag takes one argument, the filter expression") + } + + let blocks = try parser.parse(until(["endfilter"])) + + guard parser.nextToken() != nil else { + throw TemplateSyntaxError("`endfilter` was not found.") + } + + let resolvable = try parser.compileFilter("filter_value|\(bits[1])", containedIn: token) + return FilterNode(nodes: blocks, resolvable: resolvable, token: token) + } + + init(nodes: [NodeType], resolvable: Resolvable, token: Token) { + self.nodes = nodes + self.resolvable = resolvable + self.token = token + } + + func render(_ context: Context) throws -> String { + let value = try renderNodes(nodes, context) + + return try context.push(dictionary: ["filter_value": value]) { + try VariableNode(variable: resolvable, token: token).render(context) + } + } +} diff --git a/regen/Dependencies/Stencil/Filters.swift b/regen/Dependencies/Stencil/Filters.swift new file mode 100755 index 0000000..a456299 --- /dev/null +++ b/regen/Dependencies/Stencil/Filters.swift @@ -0,0 +1,129 @@ +func capitalise(_ value: Any?) -> Any? { + if let array = value as? [Any?] { + return array.map { stringify($0).capitalized } + } else { + return stringify(value).capitalized + } +} + +func uppercase(_ value: Any?) -> Any? { + if let array = value as? [Any?] { + return array.map { stringify($0).uppercased() } + } else { + return stringify(value).uppercased() + } +} + +func lowercase(_ value: Any?) -> Any? { + if let array = value as? [Any?] { + return array.map { stringify($0).lowercased() } + } else { + return stringify(value).lowercased() + } +} + +func defaultFilter(value: Any?, arguments: [Any?]) -> Any? { + // value can be optional wrapping nil, so this way we check for underlying value + if let value = value, String(describing: value) != "nil" { + return value + } + + for argument in arguments { + if let argument = argument { + return argument + } + } + + return nil +} + +func joinFilter(value: Any?, arguments: [Any?]) throws -> Any? { + guard arguments.count < 2 else { + throw TemplateSyntaxError("'join' filter takes at most one argument") + } + + let separator = stringify(arguments.first ?? "") + + if let value = value as? [Any] { + return value + .map(stringify) + .joined(separator: separator) + } + + return value +} + +func splitFilter(value: Any?, arguments: [Any?]) throws -> Any? { + guard arguments.count < 2 else { + throw TemplateSyntaxError("'split' filter takes at most one argument") + } + + let separator = stringify(arguments.first ?? " ") + if let value = value as? String { + return value.components(separatedBy: separator) + } + + return value +} + +func indentFilter(value: Any?, arguments: [Any?]) throws -> Any? { + guard arguments.count <= 3 else { + throw TemplateSyntaxError("'indent' filter can take at most 3 arguments") + } + + var indentWidth = 4 + if !arguments.isEmpty { + guard let value = arguments[0] as? Int else { + throw TemplateSyntaxError(""" + 'indent' filter width argument must be an Integer (\(String(describing: arguments[0]))) + """) + } + indentWidth = value + } + + var indentationChar = " " + if arguments.count > 1 { + guard let value = arguments[1] as? String else { + throw TemplateSyntaxError(""" + 'indent' filter indentation argument must be a String (\(String(describing: arguments[1])) + """) + } + indentationChar = value + } + + var indentFirst = false + if arguments.count > 2 { + guard let value = arguments[2] as? Bool else { + throw TemplateSyntaxError("'indent' filter indentFirst argument must be a Bool") + } + indentFirst = value + } + + let indentation = [String](repeating: indentationChar, count: indentWidth).joined() + return indent(stringify(value), indentation: indentation, indentFirst: indentFirst) +} + +func indent(_ content: String, indentation: String, indentFirst: Bool) -> String { + guard !indentation.isEmpty else { return content } + + var lines = content.components(separatedBy: .newlines) + let firstLine = (indentFirst ? indentation : "") + lines.removeFirst() + let result = lines.reduce([firstLine]) { result, line in + result + [(line.isEmpty ? "" : "\(indentation)\(line)")] + } + return result.joined(separator: "\n") +} + +func filterFilter(value: Any?, arguments: [Any?], context: Context) throws -> Any? { + guard let value = value else { return nil } + guard arguments.count == 1 else { + throw TemplateSyntaxError("'filter' filter takes one argument") + } + + let attribute = stringify(arguments[0]) + + let expr = try context.environment.compileFilter("$0|\(attribute)") + return try context.push(dictionary: ["$0": value]) { + try expr.resolve(context) + } +} diff --git a/regen/Dependencies/Stencil/ForTag.swift b/regen/Dependencies/Stencil/ForTag.swift new file mode 100755 index 0000000..f727324 --- /dev/null +++ b/regen/Dependencies/Stencil/ForTag.swift @@ -0,0 +1,176 @@ +import Foundation + +class ForNode: NodeType { + let resolvable: Resolvable + let loopVariables: [String] + let nodes: [NodeType] + let emptyNodes: [NodeType] + let `where`: Expression? + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let components = token.components + + func hasToken(_ token: String, at index: Int) -> Bool { + return components.count > (index + 1) && components[index] == token + } + + func endsOrHasToken(_ token: String, at index: Int) -> Bool { + return components.count == index || hasToken(token, at: index) + } + + guard hasToken("in", at: 2) && endsOrHasToken("where", at: 4) else { + throw TemplateSyntaxError("'for' statements should use the syntax: `for in [where ]`.") + } + + let loopVariables = components[1] + .split(separator: ",") + .map(String.init) + .map { $0.trim(character: " ") } + + let resolvable = try parser.compileResolvable(components[3], containedIn: token) + + let `where` = hasToken("where", at: 4) + ? try parser.compileExpression(components: Array(components.suffix(from: 5)), token: token) + : nil + + let forNodes = try parser.parse(until(["endfor", "empty"])) + + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endfor` was not found.") + } + + var emptyNodes = [NodeType]() + if token.contents == "empty" { + emptyNodes = try parser.parse(until(["endfor"])) + _ = parser.nextToken() + } + + return ForNode( + resolvable: resolvable, + loopVariables: loopVariables, + nodes: forNodes, + emptyNodes: emptyNodes, + where: `where`, + token: token + ) + } + + init( + resolvable: Resolvable, + loopVariables: [String], + nodes: [NodeType], + emptyNodes: [NodeType], + where: Expression? = nil, + token: Token? = nil + ) { + self.resolvable = resolvable + self.loopVariables = loopVariables + self.nodes = nodes + self.emptyNodes = emptyNodes + self.where = `where` + self.token = token + } + + func render(_ context: Context) throws -> String { + var values = try resolve(context) + + if let `where` = self.where { + values = try values.filter { item -> Bool in + try push(value: item, context: context) { + try `where`.evaluate(context: context) + } + } + } + + if !values.isEmpty { + let count = values.count + + return try zip(0..., values) + .map { index, item in + let forContext: [String: Any] = [ + "first": index == 0, + "last": index == (count - 1), + "counter": index + 1, + "counter0": index, + "length": count + ] + + return try context.push(dictionary: ["forloop": forContext]) { + try push(value: item, context: context) { + try renderNodes(nodes, context) + } + } + } + .joined() + } + + return try context.push { + try renderNodes(emptyNodes, context) + } + } + + private func push(value: Any, context: Context, closure: () throws -> (Result)) throws -> Result { + if loopVariables.isEmpty { + return try context.push { + try closure() + } + } + + let valueMirror = Mirror(reflecting: value) + if case .tuple? = valueMirror.displayStyle { + if loopVariables.count > Int(valueMirror.children.count) { + throw TemplateSyntaxError("Tuple '\(value)' has less values than loop variables") + } + var variablesContext = [String: Any]() + valueMirror.children.prefix(loopVariables.count).enumerated().forEach { offset, element in + if loopVariables[offset] != "_" { + variablesContext[loopVariables[offset]] = element.value + } + } + + return try context.push(dictionary: variablesContext) { + try closure() + } + } + + return try context.push(dictionary: [loopVariables.first ?? "": value]) { + try closure() + } + } + + private func resolve(_ context: Context) throws -> [Any] { + let resolved = try resolvable.resolve(context) + + var values: [Any] + if let dictionary = resolved as? [String: Any], !dictionary.isEmpty { + values = dictionary.sorted { $0.key < $1.key } + } else if let array = resolved as? [Any] { + values = array + } else if let range = resolved as? CountableClosedRange { + values = Array(range) + } else if let range = resolved as? CountableRange { + values = Array(range) + } else if let resolved = resolved { + let mirror = Mirror(reflecting: resolved) + switch mirror.displayStyle { + case .struct?, .tuple?: + values = Array(mirror.children) + case .class?: + var children = Array(mirror.children) + var currentMirror: Mirror? = mirror + while let superclassMirror = currentMirror?.superclassMirror { + children.append(contentsOf: superclassMirror.children) + currentMirror = superclassMirror + } + values = Array(children) + default: + values = [] + } + } else { + values = [] + } + + return values + } +} diff --git a/regen/Dependencies/Stencil/IfTag.swift b/regen/Dependencies/Stencil/IfTag.swift new file mode 100755 index 0000000..061914a --- /dev/null +++ b/regen/Dependencies/Stencil/IfTag.swift @@ -0,0 +1,313 @@ +enum Operator { + case infix(String, Int, InfixOperator.Type) + case prefix(String, Int, PrefixOperator.Type) + + var name: String { + switch self { + case .infix(let name, _, _): + return name + case .prefix(let name, _, _): + return name + } + } +} + +let operators: [Operator] = [ + .infix("in", 5, InExpression.self), + .infix("or", 6, OrExpression.self), + .infix("and", 7, AndExpression.self), + .prefix("not", 8, NotExpression.self), + .infix("==", 10, EqualityExpression.self), + .infix("!=", 10, InequalityExpression.self), + .infix(">", 10, MoreThanExpression.self), + .infix(">=", 10, MoreThanEqualExpression.self), + .infix("<", 10, LessThanExpression.self), + .infix("<=", 10, LessThanEqualExpression.self) +] + +func findOperator(name: String) -> Operator? { + for `operator` in operators where `operator`.name == name { + return `operator` + } + + return nil +} + +indirect enum IfToken { + case infix(name: String, bindingPower: Int, operatorType: InfixOperator.Type) + case prefix(name: String, bindingPower: Int, operatorType: PrefixOperator.Type) + case variable(Resolvable) + case subExpression(Expression) + case end + + var bindingPower: Int { + switch self { + case .infix(_, let bindingPower, _): + return bindingPower + case .prefix(_, let bindingPower, _): + return bindingPower + case .variable: + return 0 + case .subExpression: + return 0 + case .end: + return 0 + } + } + + func nullDenotation(parser: IfExpressionParser) throws -> Expression { + switch self { + case .infix(let name, _, _): + throw TemplateSyntaxError("'if' expression error: infix operator '\(name)' doesn't have a left hand side") + case .prefix(_, let bindingPower, let operatorType): + let expression = try parser.expression(bindingPower: bindingPower) + return operatorType.init(expression: expression) + case .variable(let variable): + return VariableExpression(variable: variable) + case .subExpression(let expression): + return expression + case .end: + throw TemplateSyntaxError("'if' expression error: end") + } + } + + func leftDenotation(left: Expression, parser: IfExpressionParser) throws -> Expression { + switch self { + case .infix(_, let bindingPower, let operatorType): + let right = try parser.expression(bindingPower: bindingPower) + return operatorType.init(lhs: left, rhs: right) + case .prefix(let name, _, _): + throw TemplateSyntaxError("'if' expression error: prefix operator '\(name)' was called with a left hand side") + case .variable(let variable): + throw TemplateSyntaxError("'if' expression error: variable '\(variable)' was called with a left hand side") + case .subExpression: + throw TemplateSyntaxError("'if' expression error: sub expression was called with a left hand side") + case .end: + throw TemplateSyntaxError("'if' expression error: end") + } + } + + var isEnd: Bool { + switch self { + case .end: + return true + default: + return false + } + } +} + +final class IfExpressionParser { + let tokens: [IfToken] + var position: Int = 0 + + private init(tokens: [IfToken]) { + self.tokens = tokens + } + + static func parser(components: [String], environment: Environment, token: Token) throws -> IfExpressionParser { + return try IfExpressionParser(components: ArraySlice(components), environment: environment, token: token) + } + + private init(components: ArraySlice, environment: Environment, token: Token) throws { + var parsedComponents = Set() + var bracketsBalance = 0 + self.tokens = try zip(components.indices, components).compactMap { index, component in + guard !parsedComponents.contains(index) else { return nil } + + if component == "(" { + bracketsBalance += 1 + let (expression, parsedCount) = try IfExpressionParser.subExpression( + from: components.suffix(from: index + 1), + environment: environment, + token: token + ) + parsedComponents.formUnion(Set(index...(index + parsedCount))) + return .subExpression(expression) + } else if component == ")" { + bracketsBalance -= 1 + if bracketsBalance < 0 { + throw TemplateSyntaxError("'if' expression error: missing opening bracket") + } + parsedComponents.insert(index) + return nil + } else { + parsedComponents.insert(index) + if let `operator` = findOperator(name: component) { + switch `operator` { + case .infix(let name, let bindingPower, let operatorType): + return .infix(name: name, bindingPower: bindingPower, operatorType: operatorType) + case .prefix(let name, let bindingPower, let operatorType): + return .prefix(name: name, bindingPower: bindingPower, operatorType: operatorType) + } + } + return .variable(try environment.compileResolvable(component, containedIn: token)) + } + } + } + + private static func subExpression( + from components: ArraySlice, + environment: Environment, + token: Token + ) throws -> (Expression, Int) { + var bracketsBalance = 1 + let subComponents = components.prefix { + if $0 == "(" { + bracketsBalance += 1 + } else if $0 == ")" { + bracketsBalance -= 1 + } + return bracketsBalance != 0 + } + if bracketsBalance > 0 { + throw TemplateSyntaxError("'if' expression error: missing closing bracket") + } + + let expressionParser = try IfExpressionParser(components: subComponents, environment: environment, token: token) + let expression = try expressionParser.parse() + return (expression, subComponents.count) + } + + var currentToken: IfToken { + if tokens.count > position { + return tokens[position] + } + + return .end + } + + var nextToken: IfToken { + position += 1 + return currentToken + } + + func parse() throws -> Expression { + let expression = try self.expression() + + if !currentToken.isEnd { + throw TemplateSyntaxError("'if' expression error: dangling token") + } + + return expression + } + + func expression(bindingPower: Int = 0) throws -> Expression { + var token = currentToken + position += 1 + + var left = try token.nullDenotation(parser: self) + + while bindingPower < currentToken.bindingPower { + token = currentToken + position += 1 + left = try token.leftDenotation(left: left, parser: self) + } + + return left + } +} + +/// Represents an if condition and the associated nodes when the condition +/// evaluates +final class IfCondition { + let expression: Expression? + let nodes: [NodeType] + + init(expression: Expression?, nodes: [NodeType]) { + self.expression = expression + self.nodes = nodes + } + + func render(_ context: Context) throws -> String { + return try context.push { + try renderNodes(nodes, context) + } + } +} + +class IfNode: NodeType { + let conditions: [IfCondition] + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + var components = token.components + components.removeFirst() + + let expression = try parser.compileExpression(components: components, token: token) + let nodes = try parser.parse(until(["endif", "elif", "else"])) + var conditions: [IfCondition] = [ + IfCondition(expression: expression, nodes: nodes) + ] + + var nextToken = parser.nextToken() + while let current = nextToken, current.contents.hasPrefix("elif") { + var components = current.components + components.removeFirst() + let expression = try parser.compileExpression(components: components, token: current) + + let nodes = try parser.parse(until(["endif", "elif", "else"])) + nextToken = parser.nextToken() + conditions.append(IfCondition(expression: expression, nodes: nodes)) + } + + if let current = nextToken, current.contents == "else" { + conditions.append(IfCondition(expression: nil, nodes: try parser.parse(until(["endif"])))) + nextToken = parser.nextToken() + } + + guard let current = nextToken, current.contents == "endif" else { + throw TemplateSyntaxError("`endif` was not found.") + } + + return IfNode(conditions: conditions, token: token) + } + + class func parse_ifnot(_ parser: TokenParser, token: Token) throws -> NodeType { + var components = token.components + guard components.count == 2 else { + throw TemplateSyntaxError("'ifnot' statements should use the following syntax 'ifnot condition'.") + } + components.removeFirst() + var trueNodes = [NodeType]() + var falseNodes = [NodeType]() + + let expression = try parser.compileExpression(components: components, token: token) + falseNodes = try parser.parse(until(["endif", "else"])) + + guard let token = parser.nextToken() else { + throw TemplateSyntaxError("`endif` was not found.") + } + + if token.contents == "else" { + trueNodes = try parser.parse(until(["endif"])) + _ = parser.nextToken() + } + + return IfNode(conditions: [ + IfCondition(expression: expression, nodes: trueNodes), + IfCondition(expression: nil, nodes: falseNodes) + ], token: token) + } + + init(conditions: [IfCondition], token: Token? = nil) { + self.conditions = conditions + self.token = token + } + + func render(_ context: Context) throws -> String { + for condition in conditions { + if let expression = condition.expression { + let truthy = try expression.evaluate(context: context) + + if truthy { + return try condition.render(context) + } + } else { + return try condition.render(context) + } + } + + return "" + } +} diff --git a/regen/Dependencies/Stencil/Include.swift b/regen/Dependencies/Stencil/Include.swift new file mode 100755 index 0000000..5f5224e --- /dev/null +++ b/regen/Dependencies/Stencil/Include.swift @@ -0,0 +1,46 @@ +class IncludeNode: NodeType { + let templateName: Variable + let includeContext: String? + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components + + guard bits.count == 2 || bits.count == 3 else { + throw TemplateSyntaxError(""" + 'include' tag requires one argument, the template file to be included. \ + A second optional argument can be used to specify the context that will \ + be passed to the included file + """) + } + + return IncludeNode(templateName: Variable(bits[1]), includeContext: bits.count == 3 ? bits[2] : nil, token: token) + } + + init(templateName: Variable, includeContext: String? = nil, token: Token) { + self.templateName = templateName + self.includeContext = includeContext + self.token = token + } + + func render(_ context: Context) throws -> String { + guard let templateName = try self.templateName.resolve(context) as? String else { + throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") + } + + let template = try context.environment.loadTemplate(name: templateName) + + do { + let subContext = includeContext.flatMap { context[$0] as? [String: Any] } ?? [:] + return try context.push(dictionary: subContext) { + try template.render(context) + } + } catch { + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) + } else { + throw error + } + } + } +} diff --git a/regen/Dependencies/Stencil/Inheritence.swift b/regen/Dependencies/Stencil/Inheritence.swift new file mode 100755 index 0000000..611d28c --- /dev/null +++ b/regen/Dependencies/Stencil/Inheritence.swift @@ -0,0 +1,189 @@ +class BlockContext { + class var contextKey: String { return "block_context" } + + // contains mapping of block names to their nodes and templates where they are defined + var blocks: [String: [BlockNode]] + + init(blocks: [String: BlockNode]) { + self.blocks = [:] + blocks.forEach { self.blocks[$0.key] = [$0.value] } + } + + func push(_ block: BlockNode, forKey blockName: String) { + if var blocks = blocks[blockName] { + blocks.append(block) + self.blocks[blockName] = blocks + } else { + self.blocks[blockName] = [block] + } + } + + func pop(_ blockName: String) -> BlockNode? { + if var blocks = blocks[blockName] { + let block = blocks.removeFirst() + if blocks.isEmpty { + self.blocks.removeValue(forKey: blockName) + } else { + self.blocks[blockName] = blocks + } + return block + } else { + return nil + } + } +} + +extension Collection { + func any(_ closure: (Iterator.Element) -> Bool) -> Iterator.Element? { + for element in self { + if closure(element) { + return element + } + } + + return nil + } +} + +class ExtendsNode: NodeType { + let templateName: Variable + let blocks: [String: BlockNode] + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components + + guard bits.count == 2 else { + throw TemplateSyntaxError("'extends' takes one argument, the template file to be extended") + } + + let parsedNodes = try parser.parse() + guard (parsedNodes.any { $0 is ExtendsNode }) == nil else { + throw TemplateSyntaxError("'extends' cannot appear more than once in the same template") + } + + let blockNodes = parsedNodes.compactMap { $0 as? BlockNode } + + let nodes = blockNodes.reduce([String: BlockNode]()) { accumulator, node -> [String: BlockNode] in + var dict = accumulator + dict[node.name] = node + return dict + } + + return ExtendsNode(templateName: Variable(bits[1]), blocks: nodes, token: token) + } + + init(templateName: Variable, blocks: [String: BlockNode], token: Token) { + self.templateName = templateName + self.blocks = blocks + self.token = token + } + + func render(_ context: Context) throws -> String { + guard let templateName = try self.templateName.resolve(context) as? String else { + throw TemplateSyntaxError("'\(self.templateName)' could not be resolved as a string") + } + + let baseTemplate = try context.environment.loadTemplate(name: templateName) + + let blockContext: BlockContext + if let currentBlockContext = context[BlockContext.contextKey] as? BlockContext { + blockContext = currentBlockContext + for (name, block) in blocks { + blockContext.push(block, forKey: name) + } + } else { + blockContext = BlockContext(blocks: blocks) + } + + do { + // pushes base template and renders it's content + // block_context contains all blocks from child templates + return try context.push(dictionary: [BlockContext.contextKey: blockContext]) { + try baseTemplate.render(context) + } + } catch { + // if error template is already set (see catch in BlockNode) + // and it happend in the same template as current template + // there is no need to wrap it in another error + if let error = error as? TemplateSyntaxError, error.templateName != token?.sourceMap.filename { + throw TemplateSyntaxError(reason: error.reason, stackTrace: error.allTokens) + } else { + throw error + } + } + } +} + +class BlockNode: NodeType { + let name: String + let nodes: [NodeType] + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + let bits = token.components + + guard bits.count == 2 else { + throw TemplateSyntaxError("'block' tag takes one argument, the block name") + } + + let blockName = bits[1] + let nodes = try parser.parse(until(["endblock"])) + _ = parser.nextToken() + return BlockNode(name: blockName, nodes: nodes, token: token) + } + + init(name: String, nodes: [NodeType], token: Token) { + self.name = name + self.nodes = nodes + self.token = token + } + + func render(_ context: Context) throws -> String { + if let blockContext = context[BlockContext.contextKey] as? BlockContext, let child = blockContext.pop(name) { + let childContext = try self.childContext(child, blockContext: blockContext, context: context) + // render extension node + do { + return try context.push(dictionary: childContext) { + try child.render(context) + } + } catch { + throw error.withToken(child.token) + } + } + + return try renderNodes(nodes, context) + } + + // child node is a block node from child template that extends this node (has the same name) + func childContext(_ child: BlockNode, blockContext: BlockContext, context: Context) throws -> [String: Any] { + var childContext: [String: Any] = [BlockContext.contextKey: blockContext] + + if let blockSuperNode = child.nodes.first(where: { + if let token = $0.token, case .variable = token.kind, token.contents == "block.super" { + return true + } else { + return false + } + }) { + do { + // render base node so that its content can be used as part of child node that extends it + childContext["block"] = ["super": try self.render(context)] + } catch { + if let error = error as? TemplateSyntaxError { + throw TemplateSyntaxError( + reason: error.reason, + token: blockSuperNode.token, + stackTrace: error.allTokens) + } else { + throw TemplateSyntaxError( + reason: "\(error)", + token: blockSuperNode.token, + stackTrace: []) + } + } + } + return childContext + } + +} diff --git a/regen/Dependencies/Stencil/KeyPath.swift b/regen/Dependencies/Stencil/KeyPath.swift new file mode 100755 index 0000000..98767b7 --- /dev/null +++ b/regen/Dependencies/Stencil/KeyPath.swift @@ -0,0 +1,112 @@ +import Foundation + +/// A structure used to represent a template variable, and to resolve it in a given context. +final class KeyPath { + private var components = [String]() + private var current = "" + private var partialComponents = [String]() + private var subscriptLevel = 0 + + let variable: String + let context: Context + + // Split the keypath string and resolve references if possible + init(_ variable: String, in context: Context) { + self.variable = variable + self.context = context + } + + func parse() throws -> [String] { + defer { + components = [] + current = "" + partialComponents = [] + subscriptLevel = 0 + } + + for character in variable { + switch character { + case "." where subscriptLevel == 0: + try foundSeparator() + case "[": + try openBracket() + case "]": + try closeBracket() + default: + try addCharacter(character) + } + } + try finish() + + return components + } + + private func foundSeparator() throws { + if !current.isEmpty { + partialComponents.append(current) + } + + guard !partialComponents.isEmpty else { + throw TemplateSyntaxError("Unexpected '.' in variable '\(variable)'") + } + + components += partialComponents + current = "" + partialComponents = [] + } + + // when opening the first bracket, we must have a partial component + private func openBracket() throws { + guard !partialComponents.isEmpty || !current.isEmpty else { + throw TemplateSyntaxError("Unexpected '[' in variable '\(variable)'") + } + + if subscriptLevel > 0 { + current.append("[") + } else if !current.isEmpty { + partialComponents.append(current) + current = "" + } + + subscriptLevel += 1 + } + + // for a closing bracket at root level, try to resolve the reference + private func closeBracket() throws { + guard subscriptLevel > 0 else { + throw TemplateSyntaxError("Unbalanced ']' in variable '\(variable)'") + } + + if subscriptLevel > 1 { + current.append("]") + } else if !current.isEmpty, + let value = try Variable(current).resolve(context) { + partialComponents.append("\(value)") + current = "" + } else { + throw TemplateSyntaxError("Unable to resolve subscript '\(current)' in variable '\(variable)'") + } + + subscriptLevel -= 1 + } + + private func addCharacter(_ character: Character) throws { + guard partialComponents.isEmpty || subscriptLevel > 0 else { + throw TemplateSyntaxError("Unexpected character '\(character)' in variable '\(variable)'") + } + + current.append(character) + } + + private func finish() throws { + // check if we have a last piece + if !current.isEmpty { + partialComponents.append(current) + } + components += partialComponents + + guard subscriptLevel == 0 else { + throw TemplateSyntaxError("Unbalanced subscript brackets in variable '\(variable)'") + } + } +} diff --git a/regen/Dependencies/Stencil/Lexer.swift b/regen/Dependencies/Stencil/Lexer.swift new file mode 100755 index 0000000..47465f5 --- /dev/null +++ b/regen/Dependencies/Stencil/Lexer.swift @@ -0,0 +1,231 @@ +import Foundation + +typealias Line = (content: String, number: UInt, range: Range) + +struct Lexer { + let templateName: String? + let templateString: String + let lines: [Line] + + /// The potential token start characters. In a template these appear after a + /// `{` character, for example `{{`, `{%`, `{#`, ... + private static let tokenChars: [Unicode.Scalar] = ["{", "%", "#"] + + /// The token end characters, corresponding to their token start characters. + /// For example, a variable token starts with `{{` and ends with `}}` + private static let tokenCharMap: [Unicode.Scalar: Unicode.Scalar] = [ + "{": "}", + "%": "%", + "#": "#" + ] + + init(templateName: String? = nil, templateString: String) { + self.templateName = templateName + self.templateString = templateString + + self.lines = templateString.components(separatedBy: .newlines).enumerated().compactMap { + guard !$0.element.isEmpty, + let range = templateString.range(of: $0.element) else { return nil } + return (content: $0.element, number: UInt($0.offset + 1), range) + } + } + + /// Create a token that will be passed on to the parser, with the given + /// content and a range. The content will be tested to see if it's a + /// `variable`, a `block` or a `comment`, otherwise it'll default to a simple + /// `text` token. + /// + /// - Parameters: + /// - string: The content string of the token + /// - range: The range within the template content, used for smart + /// error reporting + func createToken(string: String, at range: Range) -> Token { + func strip() -> String { + guard string.count > 4 else { return "" } + let trimmed = String(string.dropFirst(2).dropLast(2)) + .components(separatedBy: "\n") + .filter { !$0.isEmpty } + .map { $0.trim(character: " ") } + .joined(separator: " ") + return trimmed + } + + if string.hasPrefix("{{") || string.hasPrefix("{%") || string.hasPrefix("{#") { + let value = strip() + let range = templateString.range(of: value, range: range) ?? range + let location = rangeLocation(range) + let sourceMap = SourceMap(filename: templateName, location: location) + + if string.hasPrefix("{{") { + return .variable(value: value, at: sourceMap) + } else if string.hasPrefix("{%") { + return .block(value: value, at: sourceMap) + } else if string.hasPrefix("{#") { + return .comment(value: value, at: sourceMap) + } + } + + let location = rangeLocation(range) + let sourceMap = SourceMap(filename: templateName, location: location) + return .text(value: string, at: sourceMap) + } + + /// Transforms the template into a list of tokens, that will eventually be + /// passed on to the parser. + /// + /// - Returns: The list of tokens (see `createToken(string: at:)`). + func tokenize() -> [Token] { + var tokens: [Token] = [] + + let scanner = Scanner(templateString) + while !scanner.isEmpty { + if let (char, text) = scanner.scanForTokenStart(Lexer.tokenChars) { + if !text.isEmpty { + tokens.append(createToken(string: text, at: scanner.range)) + } + + guard let end = Lexer.tokenCharMap[char] else { continue } + let result = scanner.scanForTokenEnd(end) + tokens.append(createToken(string: result, at: scanner.range)) + } else { + tokens.append(createToken(string: scanner.content, at: scanner.range)) + scanner.content = "" + } + } + + return tokens + } + + /// Finds the line matching the given range (for a token) + /// + /// - Parameter range: The range to search for. + /// - Returns: The content for that line, the line number and offset within + /// the line. + func rangeLocation(_ range: Range) -> ContentLocation { + guard let line = self.lines.first(where: { $0.range.contains(range.lowerBound) }) else { + return ("", 0, 0) + } + let offset = templateString.distance(from: line.range.lowerBound, to: range.lowerBound) + return (line.content, line.number, offset) + } + +} + +class Scanner { + let originalContent: String + var content: String + var range: Range + + /// The start delimiter for a token. + private static let tokenStartDelimiter: Unicode.Scalar = "{" + /// And the corresponding end delimiter for a token. + private static let tokenEndDelimiter: Unicode.Scalar = "}" + + init(_ content: String) { + self.originalContent = content + self.content = content + range = content.startIndex.. String { + var foundChar = false + + for (index, char) in content.unicodeScalars.enumerated() { + if foundChar && char == Scanner.tokenEndDelimiter { + let result = String(content.prefix(index + 1)) + content = String(content.dropFirst(index + 1)) + range = range.upperBound.. (Unicode.Scalar, String)? { + var foundBrace = false + + range = range.upperBound.. String.Index? { + var index = startIndex + + while index != endIndex { + if character != self[index] { + return index + } + index = self.index(after: index) + } + + return nil + } + + func findLastNot(character: Character) -> String.Index? { + var index = self.index(before: endIndex) + + while index != startIndex { + if character != self[index] { + return self.index(after: index) + } + index = self.index(before: index) + } + + return nil + } + + func trim(character: Character) -> String { + let first = findFirstNot(character: character) ?? startIndex + let last = findLastNot(character: character) ?? endIndex + return String(self[first.. Template + func loadTemplate(names: [String], environment: Environment) throws -> Template +} + +extension Loader { + public func loadTemplate(names: [String], environment: Environment) throws -> Template { + for name in names { + do { + return try loadTemplate(name: name, environment: environment) + } catch is TemplateDoesNotExist { + continue + } catch { + throw error + } + } + + throw TemplateDoesNotExist(templateNames: names, loader: self) + } +} + +// A class for loading a template from disk +public class FileSystemLoader: Loader, CustomStringConvertible { + public let paths: [Path] + + public init(paths: [Path]) { + self.paths = paths + } + + public init(bundle: [Bundle]) { + self.paths = bundle.map { + Path($0.bundlePath) + } + } + + public var description: String { + return "FileSystemLoader(\(paths))" + } + + public func loadTemplate(name: String, environment: Environment) throws -> Template { + for path in paths { + let templatePath = try path.safeJoin(path: Path(name)) + + if !templatePath.exists { + continue + } + + let content: String = try templatePath.read() + return environment.templateClass.init(templateString: content, environment: environment, name: name) + } + + throw TemplateDoesNotExist(templateNames: [name], loader: self) + } + + public func loadTemplate(names: [String], environment: Environment) throws -> Template { + for path in paths { + for templateName in names { + let templatePath = try path.safeJoin(path: Path(templateName)) + + if templatePath.exists { + let content: String = try templatePath.read() + return environment.templateClass.init(templateString: content, environment: environment, name: templateName) + } + } + } + + throw TemplateDoesNotExist(templateNames: names, loader: self) + } +} + +public class DictionaryLoader: Loader { + public let templates: [String: String] + + public init(templates: [String: String]) { + self.templates = templates + } + + public func loadTemplate(name: String, environment: Environment) throws -> Template { + if let content = templates[name] { + return environment.templateClass.init(templateString: content, environment: environment, name: name) + } + + throw TemplateDoesNotExist(templateNames: [name], loader: self) + } + + public func loadTemplate(names: [String], environment: Environment) throws -> Template { + for name in names { + if let content = templates[name] { + return environment.templateClass.init(templateString: content, environment: environment, name: name) + } + } + + throw TemplateDoesNotExist(templateNames: names, loader: self) + } +} + +extension Path { + func safeJoin(path: Path) throws -> Path { + let newPath = self + path + + if !newPath.absolute().description.hasPrefix(absolute().description) { + throw SuspiciousFileOperation(basePath: self, path: newPath) + } + + return newPath + } +} + +class SuspiciousFileOperation: Error { + let basePath: Path + let path: Path + + init(basePath: Path, path: Path) { + self.basePath = basePath + self.path = path + } + + var description: String { + return "Path `\(path)` is located outside of base path `\(basePath)`" + } +} diff --git a/regen/Dependencies/Stencil/Node.swift b/regen/Dependencies/Stencil/Node.swift new file mode 100755 index 0000000..3386211 --- /dev/null +++ b/regen/Dependencies/Stencil/Node.swift @@ -0,0 +1,149 @@ +import Foundation + +public protocol NodeType { + /// Render the node in the given context + func render(_ context: Context) throws -> String + + /// Reference to this node's token + var token: Token? { get } +} + +/// Render the collection of nodes in the given context +public func renderNodes(_ nodes: [NodeType], _ context: Context) throws -> String { + return try nodes + .map { + do { + return try $0.render(context) + } catch { + throw error.withToken($0.token) + } + } + .joined() +} + +public class SimpleNode: NodeType { + public let handler: (Context) throws -> String + public let token: Token? + + public init(token: Token, handler: @escaping (Context) throws -> String) { + self.token = token + self.handler = handler + } + + public func render(_ context: Context) throws -> String { + return try handler(context) + } +} + +public class TextNode: NodeType { + public let text: String + public let token: Token? + + public init(text: String) { + self.text = text + self.token = nil + } + + public func render(_ context: Context) throws -> String { + return self.text + } +} + +public protocol Resolvable { + func resolve(_ context: Context) throws -> Any? +} + +public class VariableNode: NodeType { + public let variable: Resolvable + public var token: Token? + let condition: Expression? + let elseExpression: Resolvable? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + var components = token.components + + func hasToken(_ token: String, at index: Int) -> Bool { + return components.count > (index + 1) && components[index] == token + } + + let condition: Expression? + let elseExpression: Resolvable? + + if hasToken("if", at: 1) { + let components = components.suffix(from: 2) + if let elseIndex = components.firstIndex(of: "else") { + condition = try parser.compileExpression(components: Array(components.prefix(upTo: elseIndex)), token: token) + let elseToken = components.suffix(from: elseIndex.advanced(by: 1)).joined(separator: " ") + elseExpression = try parser.compileResolvable(elseToken, containedIn: token) + } else { + condition = try parser.compileExpression(components: Array(components), token: token) + elseExpression = nil + } + } else { + condition = nil + elseExpression = nil + } + + guard let resolvable = components.first else { + throw TemplateSyntaxError(reason: "Missing variable name", token: token) + } + let filter = try parser.compileResolvable(resolvable, containedIn: token) + return VariableNode(variable: filter, token: token, condition: condition, elseExpression: elseExpression) + } + + public init(variable: Resolvable, token: Token? = nil) { + self.variable = variable + self.token = token + self.condition = nil + self.elseExpression = nil + } + + init(variable: Resolvable, token: Token? = nil, condition: Expression?, elseExpression: Resolvable?) { + self.variable = variable + self.token = token + self.condition = condition + self.elseExpression = elseExpression + } + + public init(variable: String, token: Token? = nil) { + self.variable = Variable(variable) + self.token = token + self.condition = nil + self.elseExpression = nil + } + + public func render(_ context: Context) throws -> String { + if let condition = self.condition, try condition.evaluate(context: context) == false { + return try elseExpression?.resolve(context).map(stringify) ?? "" + } + + let result = try variable.resolve(context) + return stringify(result) + } +} + +func stringify(_ result: Any?) -> String { + if let result = result as? String { + return result + } else if let array = result as? [Any?] { + return unwrap(array).description + } else if let result = result as? CustomStringConvertible { + return result.description + } else if let result = result as? NSObject { + return result.description + } + + return "" +} + +func unwrap(_ array: [Any?]) -> [Any] { + return array.map { (item: Any?) -> Any in + if let item = item { + if let items = item as? [Any?] { + return unwrap(items) + } else { + return item + } + } else { return item as Any } + } +} diff --git a/regen/Dependencies/Stencil/NowTag.swift b/regen/Dependencies/Stencil/NowTag.swift new file mode 100755 index 0000000..bad6627 --- /dev/null +++ b/regen/Dependencies/Stencil/NowTag.swift @@ -0,0 +1,44 @@ +#if !os(Linux) +import Foundation + +class NowNode: NodeType { + let format: Variable + let token: Token? + + class func parse(_ parser: TokenParser, token: Token) throws -> NodeType { + var format: Variable? + + let components = token.components + guard components.count <= 2 else { + throw TemplateSyntaxError("'now' tags may only have one argument: the format string.") + } + if components.count == 2 { + format = Variable(components[1]) + } + + return NowNode(format: format, token: token) + } + + init(format: Variable?, token: Token? = nil) { + self.format = format ?? Variable("\"yyyy-MM-dd 'at' HH:mm\"") + self.token = token + } + + func render(_ context: Context) throws -> String { + let date = Date() + let format = try self.format.resolve(context) + + var formatter: DateFormatter + if let format = format as? DateFormatter { + formatter = format + } else if let format = format as? String { + formatter = DateFormatter() + formatter.dateFormat = format + } else { + return "" + } + + return formatter.string(from: date) + } +} +#endif diff --git a/regen/Dependencies/Stencil/Parser.swift b/regen/Dependencies/Stencil/Parser.swift new file mode 100755 index 0000000..404b8e2 --- /dev/null +++ b/regen/Dependencies/Stencil/Parser.swift @@ -0,0 +1,224 @@ +public func until(_ tags: [String]) -> ((TokenParser, Token) -> Bool) { + return { parser, token in + if let name = token.components.first { + for tag in tags where name == tag { + return true + } + } + + return false + } +} + +/// A class for parsing an array of tokens and converts them into a collection of Node's +public class TokenParser { + public typealias TagParser = (TokenParser, Token) throws -> NodeType + + fileprivate var tokens: [Token] + fileprivate let environment: Environment + + public init(tokens: [Token], environment: Environment) { + self.tokens = tokens + self.environment = environment + } + + /// Parse the given tokens into nodes + public func parse() throws -> [NodeType] { + return try parse(nil) + } + + public func parse(_ parseUntil: ((_ parser: TokenParser, _ token: Token) -> (Bool))?) throws -> [NodeType] { + var nodes = [NodeType]() + + while !tokens.isEmpty { + guard let token = nextToken() else { break } + + switch token.kind { + case .text: + nodes.append(TextNode(text: token.contents)) + case .variable: + try nodes.append(VariableNode.parse(self, token: token)) + case .block: + if let parseUntil = parseUntil, parseUntil(self, token) { + prependToken(token) + return nodes + } + + if let tag = token.components.first { + do { + let parser = try environment.findTag(name: tag) + let node = try parser(self, token) + nodes.append(node) + } catch { + throw error.withToken(token) + } + } + case .comment: + continue + } + } + + return nodes + } + + public func nextToken() -> Token? { + if !tokens.isEmpty { + return tokens.remove(at: 0) + } + + return nil + } + + public func prependToken(_ token: Token) { + tokens.insert(token, at: 0) + } + + /// Create filter expression from a string contained in provided token + public func compileFilter(_ filterToken: String, containedIn token: Token) throws -> Resolvable { + return try environment.compileFilter(filterToken, containedIn: token) + } + + /// Create boolean expression from components contained in provided token + public func compileExpression(components: [String], token: Token) throws -> Expression { + return try environment.compileExpression(components: components, containedIn: token) + } + + /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token + public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { + return try environment.compileResolvable(token, containedIn: containingToken) + } + +} + +extension Environment { + func findTag(name: String) throws -> Extension.TagParser { + for ext in extensions { + if let filter = ext.tags[name] { + return filter + } + } + + throw TemplateSyntaxError("Unknown template tag '\(name)'") + } + + func findFilter(_ name: String) throws -> FilterType { + for ext in extensions { + if let filter = ext.filters[name] { + return filter + } + } + + let suggestedFilters = self.suggestedFilters(for: name) + if suggestedFilters.isEmpty { + throw TemplateSyntaxError("Unknown filter '\(name)'.") + } else { + throw TemplateSyntaxError(""" + Unknown filter '\(name)'. \ + Found similar filters: \(suggestedFilters.map { "'\($0)'" }.joined(separator: ", ")). + """) + } + } + + private func suggestedFilters(for name: String) -> [String] { + let allFilters = extensions.flatMap { $0.filters.keys } + + let filtersWithDistance = allFilters + .map { (filterName: $0, distance: $0.levenshteinDistance(name)) } + // do not suggest filters which names are shorter than the distance + .filter { $0.filterName.count > $0.distance } + guard let minDistance = filtersWithDistance.min(by: { $0.distance < $1.distance })?.distance else { + return [] + } + // suggest all filters with the same distance + return filtersWithDistance.filter { $0.distance == minDistance }.map { $0.filterName } + } + + /// Create filter expression from a string + public func compileFilter(_ token: String) throws -> Resolvable { + return try FilterExpression(token: token, environment: self) + } + + /// Create filter expression from a string contained in provided token + public func compileFilter(_ filterToken: String, containedIn containingToken: Token) throws -> Resolvable { + do { + return try FilterExpression(token: filterToken, environment: self) + } catch { + guard var syntaxError = error as? TemplateSyntaxError, syntaxError.token == nil else { + throw error + } + // find offset of filter in the containing token so that only filter is highligted, not the whole token + if let filterTokenRange = containingToken.contents.range(of: filterToken) { + var location = containingToken.sourceMap.location + location.lineOffset += containingToken.contents.distance( + from: containingToken.contents.startIndex, + to: filterTokenRange.lowerBound + ) + syntaxError.token = .variable( + value: filterToken, + at: SourceMap(filename: containingToken.sourceMap.filename, location: location) + ) + } else { + syntaxError.token = containingToken + } + throw syntaxError + } + } + + /// Create resolvable (i.e. range variable or filter expression) from a string + public func compileResolvable(_ token: String) throws -> Resolvable { + return try RangeVariable(token, environment: self) + ?? compileFilter(token) + } + + /// Create resolvable (i.e. range variable or filter expression) from a string contained in provided token + public func compileResolvable(_ token: String, containedIn containingToken: Token) throws -> Resolvable { + return try RangeVariable(token, environment: self, containedIn: containingToken) + ?? compileFilter(token, containedIn: containingToken) + } + + /// Create boolean expression from components contained in provided token + public func compileExpression(components: [String], containedIn token: Token) throws -> Expression { + return try IfExpressionParser.parser(components: components, environment: self, token: token).parse() + } + +} + +// https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows +extension String { + subscript(_ index: Int) -> Character { + return self[self.index(self.startIndex, offsetBy: index)] + } + + func levenshteinDistance(_ target: String) -> Int { + // create two work vectors of integer distances + var last, current: [Int] + + // initialize v0 (the previous row of distances) + // this row is A[0][i]: edit distance for an empty s + // the distance is just the number of characters to delete from t + last = [Int](0...target.count) + current = [Int](repeating: 0, count: target.count + 1) + + for selfIndex in 0.. String { + let context = context + let parser = TokenParser(tokens: tokens, environment: context.environment) + let nodes = try parser.parse() + return try renderNodes(nodes, context) + } + + // swiftlint:disable discouraged_optional_collection + /// Render the given template + open func render(_ dictionary: [String: Any]? = nil) throws -> String { + return try render(Context(dictionary: dictionary ?? [:], environment: environment)) + } +} diff --git a/regen/Dependencies/Stencil/Tokenizer.swift b/regen/Dependencies/Stencil/Tokenizer.swift new file mode 100755 index 0000000..30f3117 --- /dev/null +++ b/regen/Dependencies/Stencil/Tokenizer.swift @@ -0,0 +1,130 @@ +import Foundation + +extension String { + /// Split a string by a separator leaving quoted phrases together + func smartSplit(separator: Character = " ") -> [String] { + var word = "" + var components: [String] = [] + var separate: Character = separator + var singleQuoteCount = 0 + var doubleQuoteCount = 0 + + for character in self { + if character == "'" { + singleQuoteCount += 1 + } else if character == "\"" { + doubleQuoteCount += 1 + } + + if character == separate { + if separate != separator { + word.append(separate) + } else if (singleQuoteCount % 2 == 0 || doubleQuoteCount % 2 == 0) && !word.isEmpty { + appendWord(word, to: &components) + word = "" + } + + separate = separator + } else { + if separate == separator && (character == "'" || character == "\"") { + separate = character + } + word.append(character) + } + } + + if !word.isEmpty { + appendWord(word, to: &components) + } + + return components + } + + private func appendWord(_ word: String, to components: inout [String]) { + let specialCharacters = ",|:" + + if !components.isEmpty { + if let precedingChar = components.last?.last, specialCharacters.contains(precedingChar) { + components[components.count - 1] += word + } else if specialCharacters.contains(word) { + components[components.count - 1] += word + } else if word != "(" && word.first == "(" || word != ")" && word.first == ")" { + components.append(String(word.prefix(1))) + appendWord(String(word.dropFirst()), to: &components) + } else if word != "(" && word.last == "(" || word != ")" && word.last == ")" { + appendWord(String(word.dropLast()), to: &components) + components.append(String(word.suffix(1))) + } else { + components.append(word) + } + } else { + components.append(word) + } + } +} + +public struct SourceMap: Equatable { + public let filename: String? + public let location: ContentLocation + + init(filename: String? = nil, location: ContentLocation = ("", 0, 0)) { + self.filename = filename + self.location = location + } + + static let unknown = SourceMap() + + public static func == (lhs: SourceMap, rhs: SourceMap) -> Bool { + return lhs.filename == rhs.filename && lhs.location == rhs.location + } +} + +public class Token: Equatable { + public enum Kind: Equatable { + /// A token representing a piece of text. + case text + /// A token representing a variable. + case variable + /// A token representing a comment. + case comment + /// A token representing a template block. + case block + } + + public let contents: String + public let kind: Kind + public let sourceMap: SourceMap + + /// Returns the underlying value as an array seperated by spaces + public private(set) lazy var components: [String] = self.contents.smartSplit() + + init(contents: String, kind: Kind, sourceMap: SourceMap) { + self.contents = contents + self.kind = kind + self.sourceMap = sourceMap + } + + /// A token representing a piece of text. + public static func text(value: String, at sourceMap: SourceMap) -> Token { + return Token(contents: value, kind: .text, sourceMap: sourceMap) + } + + /// A token representing a variable. + public static func variable(value: String, at sourceMap: SourceMap) -> Token { + return Token(contents: value, kind: .variable, sourceMap: sourceMap) + } + + /// A token representing a comment. + public static func comment(value: String, at sourceMap: SourceMap) -> Token { + return Token(contents: value, kind: .comment, sourceMap: sourceMap) + } + + /// A token representing a template block. + public static func block(value: String, at sourceMap: SourceMap) -> Token { + return Token(contents: value, kind: .block, sourceMap: sourceMap) + } + + public static func == (lhs: Token, rhs: Token) -> Bool { + return lhs.contents == rhs.contents && lhs.kind == rhs.kind && lhs.sourceMap == rhs.sourceMap + } +} diff --git a/regen/Dependencies/Stencil/Variable.swift b/regen/Dependencies/Stencil/Variable.swift new file mode 100755 index 0000000..ee03028 --- /dev/null +++ b/regen/Dependencies/Stencil/Variable.swift @@ -0,0 +1,280 @@ +import Foundation + +typealias Number = Float + +class FilterExpression: Resolvable { + let filters: [(FilterType, [Variable])] + let variable: Variable + + init(token: String, environment: Environment) throws { + let bits = token.smartSplit(separator: "|").map { String($0).trim(character: " ") } + if bits.isEmpty { + throw TemplateSyntaxError("Variable tags must include at least 1 argument") + } + + variable = Variable(bits[0]) + let filterBits = bits[bits.indices.suffix(from: 1)] + + do { + filters = try filterBits.map { + let (name, arguments) = parseFilterComponents(token: $0) + let filter = try environment.findFilter(name) + return (filter, arguments) + } + } catch { + filters = [] + throw error + } + } + + func resolve(_ context: Context) throws -> Any? { + let result = try variable.resolve(context) + + return try filters.reduce(result) { value, filter in + let arguments = try filter.1.map { try $0.resolve(context) } + return try filter.0.invoke(value: value, arguments: arguments, context: context) + } + } +} + +/// A structure used to represent a template variable, and to resolve it in a given context. +public struct Variable: Equatable, Resolvable { + public let variable: String + + /// Create a variable with a string representing the variable + public init(_ variable: String) { + self.variable = variable + } + + /// Resolve the variable in the given context + public func resolve(_ context: Context) throws -> Any? { + if (variable.hasPrefix("'") && variable.hasSuffix("'")) || (variable.hasPrefix("\"") && variable.hasSuffix("\"")) { + // String literal + return String(variable[variable.index(after: variable.startIndex) ..< variable.index(before: variable.endIndex)]) + } + + // Number literal + if let int = Int(variable) { + return int + } + if let number = Number(variable) { + return number + } + // Boolean literal + if let bool = Bool(variable) { + return bool + } + + var current: Any? = context + for bit in try lookup(context) { + current = resolve(bit: bit, context: current) + + if current == nil { + return nil + } + } + + if let resolvable = current as? Resolvable { + current = try resolvable.resolve(context) + } else if let node = current as? NodeType { + current = try node.render(context) + } + + return Variable.normalize(current) + } + + // Split the lookup string and resolve references if possible + private func lookup(_ context: Context) throws -> [String] { + let keyPath = KeyPath(variable, in: context) + return try keyPath.parse() + } + + // Try to resolve a partial keypath for the given context + private func resolve(bit: String, context: Any?) -> Any? { + let context = Variable.normalize(context) + + if let context = context as? Context { + return context[bit] + } else if let dictionary = context as? [String: Any] { + return resolve(bit: bit, dictionary: dictionary) + } else if let array = context as? [Any] { + return resolve(bit: bit, collection: array) + } else if let string = context as? String { + return resolve(bit: bit, collection: string) + } else if let object = context as? NSObject { // NSKeyValueCoding + #if os(Linux) + return nil + #else + if object.responds(to: Selector(bit)) { + return object.value(forKey: bit) + } + #endif + } else if let value = context { + return Mirror(reflecting: value).getValue(for: bit) + } + + return nil + } + + // Try to resolve a partial keypath for the given dictionary + private func resolve(bit: String, dictionary: [String: Any]) -> Any? { + if bit == "count" { + return dictionary.count + } else { + return dictionary[bit] + } + } + + // Try to resolve a partial keypath for the given collection + private func resolve(bit: String, collection: T) -> Any? { + if let index = Int(bit) { + if index >= 0 && index < collection.count { + return collection[collection.index(collection.startIndex, offsetBy: index)] + } else { + return nil + } + } else if bit == "first" { + return collection.first + } else if bit == "last" { + return collection[collection.index(collection.endIndex, offsetBy: -1)] + } else if bit == "count" { + return collection.count + } else { + return nil + } + } +} + +/// A structure used to represet range of two integer values expressed as `from...to`. +/// Values should be numbers (they will be converted to integers). +/// Rendering this variable produces array from range `from...to`. +/// If `from` is more than `to` array will contain values of reversed range. +public struct RangeVariable: Resolvable { + public let from: Resolvable + // swiftlint:disable:next identifier_name + public let to: Resolvable + + public init?(_ token: String, environment: Environment) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } + + self.from = try environment.compileFilter(components[0]) + self.to = try environment.compileFilter(components[1]) + } + + public init?(_ token: String, environment: Environment, containedIn containingToken: Token) throws { + let components = token.components(separatedBy: "...") + guard components.count == 2 else { + return nil + } + + self.from = try environment.compileFilter(components[0], containedIn: containingToken) + self.to = try environment.compileFilter(components[1], containedIn: containingToken) + } + + public func resolve(_ context: Context) throws -> Any? { + let lowerResolved = try from.resolve(context) + let upperResolved = try to.resolve(context) + + guard let lower = lowerResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'from' value is not an Integer (\(lowerResolved ?? "nil"))") + } + + guard let upper = upperResolved.flatMap(toNumber(value:)).flatMap(Int.init) else { + throw TemplateSyntaxError("'to' value is not an Integer (\(upperResolved ?? "nil") )") + } + + let range = min(lower, upper)...max(lower, upper) + return lower > upper ? Array(range.reversed()) : Array(range) + } + +} + +extension Variable { + static func normalize(_ current: Any?) -> Any? { + if let current = current as? Normalizable { + return current.normalize() + } + + return current + } +} + +protocol Normalizable { + func normalize() -> Any? +} + +extension Array: Normalizable { + func normalize() -> Any? { + return map { $0 as Any } + } +} + +extension NSArray: Normalizable { + func normalize() -> Any? { + return map { $0 as Any } + } +} + +extension Dictionary: Normalizable { + func normalize() -> Any? { + var dictionary: [String: Any] = [:] + + for (key, value) in self { + if let key = key as? String { + dictionary[key] = Variable.normalize(value) + } else if let key = key as? CustomStringConvertible { + dictionary[key.description] = Variable.normalize(value) + } + } + + return dictionary + } +} + +func parseFilterComponents(token: String) -> (String, [Variable]) { + var components = token.smartSplit(separator: ":") + let name = components.removeFirst().trim(character: " ") + let variables = components + .joined(separator: ":") + .smartSplit(separator: ",") + .map { Variable($0.trim(character: " ")) } + return (name, variables) +} + +extension Mirror { + func getValue(for key: String) -> Any? { + let result = descendant(key) ?? Int(key).flatMap { descendant($0) } + if result == nil { + // go through inheritance chain to reach superclass properties + return superclassMirror?.getValue(for: key) + } else if let result = result { + guard String(describing: result) != "nil" else { + // mirror returns non-nil value even for nil-containing properties + // so we have to check if its value is actually nil or not + return nil + } + if let result = (result as? AnyOptional)?.wrapped { + return result + } else { + return result + } + } + return result + } +} + +protocol AnyOptional { + var wrapped: Any? { get } +} + +extension Optional: AnyOptional { + var wrapped: Any? { + switch self { + case let .some(value): return value + case .none: return nil + } + } +} diff --git a/regen/Dependencies/Stencil/_SwiftSupport.swift b/regen/Dependencies/Stencil/_SwiftSupport.swift new file mode 100755 index 0000000..4519fbd --- /dev/null +++ b/regen/Dependencies/Stencil/_SwiftSupport.swift @@ -0,0 +1,46 @@ +import Foundation + +#if !swift(>=4.1) + public extension Sequence { + func compactMap(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { + return try flatMap(transform) + } + } +#endif + +#if !swift(>=4.1) + public extension Collection { + func index(_ index: Self.Index, offsetBy offset: Int) -> Self.Index { + let indexDistance = Self.IndexDistance(offset) + return self.index(index, offsetBy: indexDistance) + } + } +#endif + +#if !swift(>=4.1) +public extension TemplateSyntaxError { + public static func == (lhs: TemplateSyntaxError, rhs: TemplateSyntaxError) -> Bool { + return lhs.reason == rhs.reason && + lhs.description == rhs.description && + lhs.token == rhs.token && + lhs.stackTrace == rhs.stackTrace && + lhs.templateName == rhs.templateName + } +} +#endif + +#if !swift(>=4.1) +public extension Variable { + public static func == (lhs: Variable, rhs: Variable) -> Bool { + return lhs.variable == rhs.variable + } +} +#endif + +#if !swift(>=4.2) +extension ArraySlice where Element: Equatable { + func firstIndex(of element: Element) -> Int? { + return index(of: element) + } +} +#endif diff --git a/regen/General Models/CommandLineParameter.swift b/regen/General Models/CommandLineParameter.swift new file mode 100644 index 0000000..6c60629 --- /dev/null +++ b/regen/General Models/CommandLineParameter.swift @@ -0,0 +1,31 @@ +// +// CommandLineParameter.swift +// regen +// +// Created by Ido Mizrachi on 17/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +enum CommandLineParameter: String { + case searchPath = "--search-path" + case template = "--template" + case outputFilename = "--output-filename" + case outputClassName = "--output-class-name" + case baseLanguageCode = "--base-language-code" + case whitelist = "--whitelist-filename" + case parameterStartRegex = "--parameter-start-regex" + case parameterEndRegex = "--parameter-end-regex" + case parameterStartOffset = "--parameter-start-offset" + case parameterEndOffset = "--parameter-end-offset" + case assetsFile = "--assets" +} + +func tryParse(_ parameter: CommandLineParameter, from arguments: [String]) -> T? { + if let index = arguments.firstIndex(of: parameter.rawValue), index+1 < arguments.count { + return T(arguments[index+1]) + } else { + return nil + } +} diff --git a/RegenFramework/Data Structures/Tree.swift b/regen/General Models/Tree.swift similarity index 100% rename from RegenFramework/Data Structures/Tree.swift rename to regen/General Models/Tree.swift diff --git a/regen/Images/AssetsFinder.swift b/regen/Images/AssetsFinder.swift deleted file mode 100644 index a1ebdfd..0000000 --- a/regen/Images/AssetsFinder.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AssetsFinder.swift -// Regen -// -// Created by Ido Mizrachi on 7/8/16. -// - -import Cocoa - -class AssetsFinder { - public static let assetsSuffix = ".xcassets" - - let fileManager : FileManager - - init(fileManager: FileManager) { - self.fileManager = fileManager - } - - func findAssetsFiles(in path : String) -> [String] { - var assets : [String] = [] - Logger.debug("\tSearching image assets files: started") - let enumerator = fileManager.enumerator(atPath: path) - while let element = enumerator?.nextObject() as? String { - if isAsset(element) { - assets.append(path + "/" + element) - } - } - Logger.debug("\tSearching image assets files: finished (\(assets.count) asset\\s found)") - return assets - } - - func isAsset(_ element : String) -> Bool { - return element.hasSuffix(AssetsFinder.assetsSuffix) - } - -} diff --git a/regen/Utilities/ClassName.swift b/regen/Images/ClassName.swift similarity index 85% rename from regen/Utilities/ClassName.swift rename to regen/Images/ClassName.swift index 1d48c32..22e3ed7 100644 --- a/regen/Utilities/ClassName.swift +++ b/regen/Images/ClassName.swift @@ -9,7 +9,7 @@ import Cocoa extension String { - func className() -> String { + func className() -> String { return prefix(1).uppercased() + String(dropFirst()) } } diff --git a/regen/Images/ImageFinder.swift b/regen/Images/ImageFinder.swift deleted file mode 100644 index f8ac0f8..0000000 --- a/regen/Images/ImageFinder.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// ImageFinder.swift -// Regen -// -// Created by Ido Mizrachi on 7/8/16. -// - -import Cocoa - -class ImageFinder { - static let imageSuffix = ".imageset" - - let fileManager : FileManager - - init(fileManager: FileManager) { - self.fileManager = fileManager - } - - func findImages(in assets : String) -> [String] { - var images : [String] = [] - var searchPath = assets - if !assets.hasSuffix("/") { - searchPath = searchPath + "/" - } - Logger.debug("\tSearching for images: started") - let enumaretor = fileManager.enumerator(atPath: searchPath) - while let element = enumaretor?.nextObject() as? String { - if isImage(element) { - images.append(searchPath + element) - } - } - Logger.debug("\tSearching for images: finished (\(images.count) image\\s found)") - return images - } - - func isImage(_ element : String) -> Bool { - return element.hasSuffix(ImageFinder.imageSuffix) - } -} diff --git a/regen/Images/ImageOperation.swift b/regen/Images/ImageOperation.swift deleted file mode 100644 index 11350db..0000000 --- a/regen/Images/ImageOperation.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// ImageOperation.swift -// Regen -// -// Created by Ido Mizrachi on 7/15/16. -// - -import Foundation - -class ImageOperation { - let fileManager : FileManager - let language: Language - - init(fileManager : FileManager, language: Language) { - self.fileManager = fileManager - self.language = language - } - - func run(_ searchPath : String, output : String) { - Logger.info("Images scan: started") - let assetsFinder = AssetsFinder(fileManager: fileManager) - let imageFinder = ImageFinder(fileManager: fileManager) - let imagesetParser = ImagesetParser() - var images : [Image] = [] - - let xcassetsFiles = assetsFinder.findAssetsFiles(in: searchPath) - for assetsFile in xcassetsFiles { - let imageFiles = imageFinder.findImages(in: assetsFile) - for imageFile in imageFiles { - if let image = imagesetParser.parseImage(imageFile) { - images.append(image) - } - } - } - - let imagesTree = foldersTree(images: images) - - let validator = ImagesValidator() - let validationIssues = validator.validate(images) - if validationIssues.count == 0 { - let generator: ImagesClassGenerator - if self.language == .ObjC { - generator = ImagesClassGeneratorObjC() - } else { - generator = ImagesClassGeneratorSwift() - } - generator.generateClass(fromImagesTree: imagesTree, generatedFile: output) - } else { - Logger.error("Issues found:") - for validationIssue in validationIssues { - let firstImage = validationIssue.firstImage - let secondImage = validationIssue.secondImage - Logger.error("\timage \(firstImage) conflicts with \(secondImage) as property \(validationIssue.property)") - } - exit(EXIT_FAILURE) - } - Logger.info("Images scan: Finished") - } - - func foldersTree(images: [Image]) -> Tree { - let root = ImageNodeItem(folder: "", folderClass: "") - let tree: Tree = Tree(item: root) - for image in images { - if image.folders.count == 0 { - tree.item.images.append(image.file) - } else { - var node: TreeNode = tree - var nextNode: TreeNode? = nil - for folder in image.folders { - var found = false - for child in node.children { - if child.item.folder == folder.propertyName { - found = true - nextNode = child - break - } - } - if found == false { - let uuid = UUID().uuidString - let folderClass = folder.propertyName.className() + "_" + String(uuid[.. = TreeNode(item: ImageNodeItem(folder: folder.propertyName, folderClass: folderClass)) - node.addChild(folderNode) - node = folderNode - } else { - node = nextNode! - } - } - node.item.images.append(image.file) - } - } - - return tree - } -} diff --git a/regen/Images/ImageesAssetsFinder.swift b/regen/Images/ImageesAssetsFinder.swift new file mode 100644 index 0000000..5b3e840 --- /dev/null +++ b/regen/Images/ImageesAssetsFinder.swift @@ -0,0 +1,36 @@ +// +// AssetsFinder.swift +// Regen +// +// Created by Ido Mizrachi on 7/8/16. +// + +import Cocoa + +extension Images { + class AssetsFinder { + public static let assetsSuffix = ".xcassets" + + init() { + } + + func findAssetsFiles(in path : String) -> [String] { + var assets : [String] = [] +// Logger.debug("\tSearching image assets files: started") + let enumerator = FileManager.default.enumerator(atPath: path) + while let element = enumerator?.nextObject() as? String { + if isAsset(element) { + assets.append(path + "/" + element) + } + } +// Logger.debug("\tSearching image assets files: finished (\(assets.count) asset\\s found)") + return assets + } + + func isAsset(_ element : String) -> Bool { + return element.hasSuffix(AssetsFinder.assetsSuffix) + } + + } + +} diff --git a/regen/Images/ImagesFinder.swift b/regen/Images/ImagesFinder.swift new file mode 100644 index 0000000..724e97b --- /dev/null +++ b/regen/Images/ImagesFinder.swift @@ -0,0 +1,37 @@ +// +// ImageFinder.swift +// Regen +// +// Created by Ido Mizrachi on 7/8/16. +// + +import Cocoa + +extension Images { + class ImagesetsFinder { + + init() { + } + + func find(in assets : String) -> [String] { + var images : [String] = [] + var searchPath = assets + if !assets.hasSuffix("/") { + searchPath = searchPath + "/" + } +// Logger.debug("\tSearching for images: started") + let enumaretor = FileManager.default.enumerator(atPath: searchPath) + while let element = enumaretor?.nextObject() as? String { + if isImageset(element) { + images.append(searchPath + element) + } + } +// Logger.debug("\tSearching for images: finished (\(images.count) image\\s found)") + return images + } + + func isImageset(_ element : String) -> Bool { + return element.hasSuffix(Images.imagesetSuffix) + } + } +} diff --git a/regen/Images/ImagesNamespace.swift b/regen/Images/ImagesNamespace.swift new file mode 100644 index 0000000..9168c9c --- /dev/null +++ b/regen/Images/ImagesNamespace.swift @@ -0,0 +1,16 @@ +// +// ImagesNamespace.swift +// regen +// +// Created by Ido Mizrachi on 13/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +struct Images { +} + +extension Images { + static let imagesetSuffix = ".imageset" +} diff --git a/regen/Images/ImagesOperation.swift b/regen/Images/ImagesOperation.swift new file mode 100644 index 0000000..503bf27 --- /dev/null +++ b/regen/Images/ImagesOperation.swift @@ -0,0 +1,184 @@ +// +// ImagesOperation.swift +// Regen +// +// Created by Ido Mizrachi on 7/15/16. +// + +import Foundation + +extension Images { + class Operation { + + let parameters: Parameters + + init(parameters: Parameters) { + self.parameters = parameters + } + + func run() { + let imagesetsFinder = ImagesetsFinder() + let imagesetParser = ImagesetParser() + var images : [Image] = [] + let imagesetFiles = imagesetsFinder.find(in: parameters.assetsFile) + for imagesetFile in imagesetFiles { + if let image = imagesetParser.parse(imagesetFile) { + images.append(image) + } + } + let validator = Validator() + let issues = validator.validate(images) + guard issues.isEmpty else { + return + } + let imagesTree = foldersTree(images: images) + + let result = generate(tree: imagesTree) + try! result.data(using: .utf8)!.write(to: URL(fileURLWithPath: parameters.outputFilename)) +// generate() +//// if validationIssues.count == 0 { +//// let generator: ImagesClassGenerator +//// switch self.language { +//// case .objc: +//// generator = ImagesClassGeneratorObjC() +//// case .swift: +//// generator = ImagesClassGeneratorSwift() +//// } +//// generator.generateClass(fromImagesTree: imagesTree, generatedFile: output) +//// } else { +//// Logger.error("Issues found:") +//// for validationIssue in validationIssues { +//// let firstImage = validationIssue.firstImage +//// let secondImage = validationIssue.secondImage +//// Logger.error("\timage \(firstImage) conflicts with \(secondImage) as property \(validationIssue.property)") +//// } +//// exit(EXIT_FAILURE) +//// } +//// Logger.info("Images scan: Finished") + } + + private func generate(root: Tree) { +// let data = try! JSONEncoder().encode(imagesTree) +// let array = try! JSONSerialization.jsonObject(with: data, options: []) as! [AnyHashable] +// +// let context: [String: AnyHashable] = ["className": parameters.outputClassName, "properties": array] +// +// let environment = Environment(loader: FileSystemLoader(paths: [""])) +// let render = try! environment.renderTemplate(name: parameters.templateFile, context: context) +// try! render.data(using: .utf8)!.write(to: URL(fileURLWithPath: parameters.outputFilename)) + } + + /** + public struct {{ className }} { + {% for folder in folders %} let {{ folder.propertyName }}: {{folder.className }} = {{folder.className }}() + + {% for image in images %} let {{ image.propertyName }}: String = "{{ image.imageSetName }}" + } + + */ + private func generate(tree: Tree) -> String { + //flatten the tree + //generate code per section + //construct a single file + let folders: [[String: String]] = tree.children.map { + let folder: [String: String] = [ + "propertyName": $0.item.folder.propertyName(), + "className": $0.item.folderClass + ] + return folder + } + let images: [[String: String]] = tree.item.images.map { + let image: [String: String] = [ + "propertyName": $0.propertyName, + "imageSetName": $0.name + + ] + return image + } + let context: [String: AnyHashable] = [ + "className": parameters.outputClassName, + "folders": folders, + "images": images + ] + let environment = Environment(loader: FileSystemLoader(paths: [""])) + let render = try! environment.renderTemplate(name: parameters.templateFile, context: context) + let result = String(data: render.data(using: .utf8)!, encoding: .utf8)! + var childrenGenerated: String = "" + tree.children.forEach { + childrenGenerated += generateChild(tree: $0, parent: parameters.outputClassName) + "\n" + } + + return childrenGenerated + "\n" + result + } + + private func generateChild(tree: TreeNode, parent: String) -> String { + let folders: [[String: String]] = tree.children.map { + let folder: [String: String] = [ + "propertyName": $0.item.folder.propertyName(), + "className": $0.item.folderClass + ] + return folder + } + let images: [[String: String]] = tree.item.images.map { + let image: [String: String] = [ + "propertyName": $0.propertyName, + "imageSetName": $0.name + + ] + return image + } + let context: [String: AnyHashable] = [ + "parent": parent, + "className": tree.item.folderClass, + "folders": folders, + "images": images + ] + let environment = Environment(loader: FileSystemLoader(paths: [""])) + let render = try! environment.renderTemplate(name: parameters.templateFile, context: context) + let result = String(data: render.data(using: .utf8)!, encoding: .utf8)! + + var childrenGenerated: String = "" + tree.children.forEach { + childrenGenerated += generateChild(tree: $0, parent: tree.item.folderClass) +// childrenGenerated += "\n" + } + return childrenGenerated + /*"\n\n" +*/ result + } + + func foldersTree(images: [Image]) -> Tree { + let root = ImageNodeItem(folder: "", folderClass: "") + let tree: Tree = Tree(item: root) + for image in images { + if image.folders.count == 0 { + tree.item.images.append(image.file) + } else { + var node: TreeNode = tree + var nextNode: TreeNode? = nil + for folder in image.folders { + var found = false + for child in node.children { + if child.item.folder == folder.propertyName { + found = true + nextNode = child + break + } + } + if found == false { + let folderClass = folder.propertyName.className() + let folderNode: TreeNode = TreeNode(item: ImageNodeItem(folder: folder.propertyName, folderClass: folderClass)) + node.addChild(folderNode) + node = folderNode + } else { + node = nextNode! + } + } + node.item.images.append(image.file) + } + } + + return tree + } + } +} + + diff --git a/regen/Images/ImagesParametersParser.swift b/regen/Images/ImagesParametersParser.swift new file mode 100644 index 0000000..7fe9ea6 --- /dev/null +++ b/regen/Images/ImagesParametersParser.swift @@ -0,0 +1,29 @@ +// +// ImagesParametersParser.swift +// regen +// +// Created by Ido Mizrachi on 24/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +class ImagesParametersParser { + let arguments: [String] + + init(arguments: [String]) { + self.arguments = arguments + } + + func parse() -> Images.Parameters? { + guard let assetsFile: String = tryParse(.assetsFile, from: arguments) else { + return nil + } + guard let templateFile: String = tryParse(.template, from: arguments) else { + return nil + } + let outputFilename: String = tryParse(.outputFilename, from: arguments) ?? "Localization.swift" + let outputClassName: String = tryParse(.outputClassName, from: arguments) ?? "Localization" + return Images.Parameters(assetsFile: assetsFile, templateFile: templateFile, outputFilename: outputFilename, outputClassName: outputClassName) + } +} diff --git a/regen/Images/ImagesValidator.swift b/regen/Images/ImagesValidator.swift index 9dbb0d6..6374966 100644 --- a/regen/Images/ImagesValidator.swift +++ b/regen/Images/ImagesValidator.swift @@ -13,24 +13,27 @@ struct ValidationIssue { var property : String } -class ImagesValidator { - func validate(_ images : [Image]) -> [ValidationIssue] { - Logger.debug("\tImages validation: started") - var issues : [ValidationIssue] = [] - guard images.count > 1 else { - Logger.debug("\tImages validation: finished") - return issues - } - for i in 0...images.count-2 { - for j in i+1...images.count-1 { - if images[i].file.propertyName == images[j].file.propertyName { - if images[i].folders.last?.propertyName == images[j].folders.last?.propertyName { - issues.append(ValidationIssue(firstImage: images[i].file.name, secondImage: images[j].file.name, property: images[i].file.name)) +extension Images { + class Validator { + func validate(_ images : [Image]) -> [ValidationIssue] { + // Logger.debug("\tImages validation: started") + var issues : [ValidationIssue] = [] + guard images.count > 1 else { + // Logger.debug("\tImages validation: finished") + return issues + } + for i in 0...images.count-2 { + for j in i+1...images.count-1 { + if images[i].file.propertyName == images[j].file.propertyName { + if images[i].folders.last?.propertyName == images[j].folders.last?.propertyName { + issues.append(ValidationIssue(firstImage: images[i].file.name, secondImage: images[j].file.name, property: images[i].file.name)) + } } } } + // Logger.debug("\tImages validation: finished (\(issues.count) issue\\s found)") + return issues } - Logger.debug("\tImages validation: finished (\(issues.count) issue\\s found)") - return issues } + } diff --git a/regen/Images/ImagesetParser.swift b/regen/Images/ImagesetParser.swift index d2aa91f..2c1e7e0 100644 --- a/regen/Images/ImagesetParser.swift +++ b/regen/Images/ImagesetParser.swift @@ -7,96 +7,98 @@ import Cocoa -class ImagesetParser { - private static func removeAssetsPath(_ imageset: String) -> String? { - guard let assetsPathRange = imageset.range(of: AssetsFinder.assetsSuffix) else { - return nil +extension Images { + class ImagesetParser { + private static func removeAssetsPath(_ imageset: String) -> String? { + guard let assetsPathRange = imageset.range(of: AssetsFinder.assetsSuffix) else { + return nil + } + return String(imageset[imageset.index(assetsPathRange.upperBound, offsetBy: 1)...]) } - return String(imageset[imageset.index(assetsPathRange.upperBound, offsetBy: 1)...]) - } - - private static func removeImagesetSuffix(_ imageset: String) -> String? { - if imageset.hasSuffix(ImageFinder.imageSuffix) { - var mutableImageSet = imageset - mutableImageSet.removeLast(ImageFinder.imageSuffix.count) - return mutableImageSet - } else { - return nil - } - } - - func parseImage(_ imageset: String) -> Image? { - guard let relativeImagename = ImagesetParser.removeAssetsPath(imageset) else { - return nil - } - guard let relativeImagenameWithoutSuffix = ImagesetParser.removeImagesetSuffix(relativeImagename) else { - return nil - } - var parts = relativeImagenameWithoutSuffix.split(separator: "/") - guard parts.last != nil else { - return nil - } - var image = Image() - let filename = String(parts.last!) - image.file = Property(name: filename, propertyName: filename.propertyName()) - parts.removeLast() - for part in parts { - let folder = String(part) - image.folders.append(Property(name: folder, propertyName: folder.propertyName())) - } - - - /* - var rangeOfFolder = imagename.range(of: "/") - while rangeOfFolder != nil { - let folder = imagename[.. = Range(uncheckedBounds: (imagename.startIndex, rangeOfFolder!.upperBound)) - imagename.removeSubrange(rangeFromStart) - rangeOfFolder = imagename.range(of: "/") - }*/ -// imagename.removeLast(ImageFinder.imageSuffix.count) -// metadata.image = imagename -// metadata.property = imagename.propertyName() - - - -// guard var imageName = imageAsset.components(separatedBy: "/").last else { -// return metadata -// } - - /* - var mutableAsset = assets - if !assets.hasSuffix("/") { - mutableAsset = assets + "/" - } - - var imageName = imageAsset - let range = imageName.range(of: mutableAsset) - if let range = range { - imageName.removeSubrange(range) + + private static func removeImagesetSuffix(_ imageset: String) -> String? { + if imageset.hasSuffix(Images.imagesetSuffix) { + var mutableImageSet = imageset + mutableImageSet.removeLast(Images.imagesetSuffix.count) + return mutableImageSet + } else { + return nil + } } - - */ - - -// if imageName.contains("/") { + + func parse(_ imageset: String) -> Image? { + guard let relativeImagename = ImagesetParser.removeAssetsPath(imageset) else { + return nil + } + guard let relativeImagenameWithoutSuffix = ImagesetParser.removeImagesetSuffix(relativeImagename) else { + return nil + } + var parts = relativeImagenameWithoutSuffix.split(separator: "/") + guard parts.last != nil else { + return nil + } + var image = Image() + let filename = String(parts.last!) + image.file = Property(name: filename, propertyName: filename.propertyName()) + parts.removeLast() + for part in parts { + let folder = String(part) + image.folders.append(Property(name: folder, propertyName: folder.propertyName())) + } + + + /* + var rangeOfFolder = imagename.range(of: "/") + while rangeOfFolder != nil { + let folder = imagename[.. = Range(uncheckedBounds: (imagename.startIndex, rangeOfFolder!.upperBound)) + imagename.removeSubrange(rangeFromStart) + rangeOfFolder = imagename.range(of: "/") + }*/ + // imagename.removeLast(ImageFinder.imageSuffix.count) + // metadata.image = imagename + // metadata.property = imagename.propertyName() + + + + // guard var imageName = imageAsset.components(separatedBy: "/").last else { + // return metadata + // } + /* - metadata.path = String(imageName[.. Bool { return lhs.folder == rhs.folder } @@ -29,5 +23,10 @@ public class ImageNodeItem: Hashable { self.folder = folder self.folderClass = folderClass self.images = [] - } + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(folder) + } + } diff --git a/regen/Images/Model/ImagesParameters.swift b/regen/Images/Model/ImagesParameters.swift new file mode 100644 index 0000000..4c630f0 --- /dev/null +++ b/regen/Images/Model/ImagesParameters.swift @@ -0,0 +1,18 @@ +// +// ImagesParameters.swift +// regen +// +// Created by Ido Mizrachi on 16/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +extension Images { + struct Parameters { + let assetsFile: String + let templateFile: String + let outputFilename: String + let outputClassName: String + } +} diff --git a/regen/Utilities/PropertyName.swift b/regen/Images/PropertyName.swift similarity index 93% rename from regen/Utilities/PropertyName.swift rename to regen/Images/PropertyName.swift index 29bd173..2c22644 100644 --- a/regen/Utilities/PropertyName.swift +++ b/regen/Images/PropertyName.swift @@ -19,7 +19,7 @@ private extension String { extension String { func propertyName() -> String { - if self.characters.count == 0 { + if self.isEmpty { return self } var propertyName = "" @@ -27,12 +27,12 @@ extension String { var currentToken = String(self[self.index(self.startIndex, offsetBy: 0)]) var previousCharacterType = CharacterType.fromCharacter(self[self.index(self.startIndex, offsetBy: 0)]) var isFirstToken = true - for i in 1.. [String] { - let enumerator = fileManager.enumerator(atPath: path) - var localizationFiles : [String] = [] - Logger.debug("\tSearching localization files: started") - while let element = enumerator?.nextObject() as? String { - if element.hasSuffix(LocalizationOperation.localizableStrings) { - localizationFiles.append(path + "/" + element) +extension Localization { + class Finder { + + let searchPath: String + let stringsFilename: String + + init(searchPath: String, stringsFilename: String) { + self.searchPath = searchPath + self.stringsFilename = stringsFilename + } + + func findLocalizationFiles() -> [String] { + let enumerator = FileManager.default.enumerator(atPath: searchPath) + var localizationFiles : [String] = [] + while let element = enumerator?.nextObject() as? String { + if element.hasSuffix(stringsFilename) { + localizationFiles.append(searchPath + "/" + element) + } } + return localizationFiles } - Logger.debug("\tSearching localization files: finished (\(localizationFiles.count) file\\s found)") - return localizationFiles + } - } diff --git a/regen/Localization/LocalizationNamespace.swift b/regen/Localization/LocalizationNamespace.swift new file mode 100644 index 0000000..2be1f9d --- /dev/null +++ b/regen/Localization/LocalizationNamespace.swift @@ -0,0 +1,12 @@ +// +// LocalizationNamespace.swift +// regen +// +// Created by Ido Mizrachi on 12/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +struct Localization { +} diff --git a/regen/Localization/LocalizationOperation.swift b/regen/Localization/LocalizationOperation.swift index 37c228b..e19afcc 100644 --- a/regen/Localization/LocalizationOperation.swift +++ b/regen/Localization/LocalizationOperation.swift @@ -7,41 +7,75 @@ import Foundation -class LocalizationOperation{ - - static let localizableStrings = "Localizable.strings" - - let fileManager : FileManager - let language: Language - - init(fileManager : FileManager, language: Language) { - self.fileManager = fileManager - self.language = language - } - - func run(_ searchPath : String, output : String) { - Logger.info("Localization scan: started") - let localizationFinder = LocalizationFinder(fileManager: fileManager) - let files = localizationFinder.findLocalizationFiles(inPath: searchPath) - let parser = LocalizationParser() - var localizationEntries : [LocalizationEntry] = [] - Logger.debug("\tParse localization files: started") - for file in files { - //Scanning only the english file saves a few seconds - if file.hasSuffix("en.lproj/Localizable.strings") { - let parsedFile = parser.parseLocalizationFile(file) - parser.appendEntries(parsedFile, to: &localizationEntries) +extension Localization { + class Operation { + + let parameters: Parameters + + init(parameters: Parameters) { + self.parameters = parameters + } + + func run() { + let finder = Finder(searchPath: parameters.searchPath, stringsFilename: "Localizable.strings") + let files = finder.findLocalizationFiles() + let whitelist = parseWhitelist() + let parser = Localization.Parser(parameterDetection: parameters.parameterDetection, whitelist: whitelist) + guard let baseLanguageFile = files.first(where: { $0.hasSuffix(parameters.baseLanguageCode + ".lproj/Localizable.strings")}) else { + //error + return + } + var localizationEntries: [Entry] = [] + localizationEntries.append(contentsOf: parser.parseLocalizationFile(baseLanguageFile)) + let duplicates = findDuplicates(localizationEntries: localizationEntries) + guard duplicates.isEmpty else { + duplicates.forEach { print($0) } + return + } + generate(localizationEntries: localizationEntries) + + } + + private func findDuplicates(localizationEntries: [Entry]) -> [String] { + var duplicatesReport: [String] = [] + var scanner = localizationEntries + while !scanner.isEmpty { + let entry = scanner.removeFirst() + if let duplicate = scanner.first(where: { $0.property == entry.property }) { + duplicatesReport.append("Duplicate - key:\(entry.key) value:\(entry.value) property name:\(entry.property)") + duplicatesReport.append("With - key:\(duplicate.key) value:\(duplicate.value) property name:\(duplicate.property)") + } } + return duplicatesReport } - //TODO: Add validator for duplicated properties - Logger.debug("\tParse localization files: finished (\(localizationEntries.count) keys found)") - let generator: LocalizationClassGenerator - if language == .ObjC { - generator = LocalizationClassGeneratorObjC() - } else { - generator = LocalizationClassGeneratorSwift() + + private func generate(localizationEntries: [Entry]) { + let data = try! JSONEncoder().encode(localizationEntries) + let array = try! JSONSerialization.jsonObject(with: data, options: []) as! [AnyHashable] + let context: [String: AnyHashable] = ["className": parameters.outputClassName, "properties": array] + + let environment = Environment(loader: FileSystemLoader(paths: [""])) + let render = try! environment.renderTemplate(name: parameters.templateFile, context: context) + try! render.data(using: .utf8)!.write(to: URL(fileURLWithPath: parameters.outputFilename)) + } + + private func parseWhitelist() -> [String]? { + guard let whitelistFile = parameters.whitelistFile else { + return nil + } + do { + let whitelistString = try String(contentsOfFile: whitelistFile) + let whitelistArray = whitelistString.split(separator: "\n").compactMap { + return String($0).trimmingCharacters(in: .whitespacesAndNewlines) + } + return whitelistArray + } catch { + print("Could not load whitelist from \(whitelistFile)") + return nil + } } - generator.generateClass(fromLocalizationEntries: localizationEntries, generatedFile: output) - Logger.info("Localization scan: finished") } + } + + diff --git a/regen/Localization/LocalizationParametersParser.swift b/regen/Localization/LocalizationParametersParser.swift new file mode 100644 index 0000000..e4a0c19 --- /dev/null +++ b/regen/Localization/LocalizationParametersParser.swift @@ -0,0 +1,41 @@ +// +// LocalizationParametersParser.swift +// regen +// +// Created by Ido Mizrachi on 17/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +class LocalizationParametersParser { + let arguments: [String] + + init(arguments: [String]) { + self.arguments = arguments + } + + func parse() -> Localization.Parameters? { + guard let searchPath: String = tryParse(.searchPath, from: arguments) else { + return nil + } + guard let templateFile: String = tryParse(.template, from: arguments) else { + return nil + } + let outputFilename: String = tryParse(.outputFilename, from: arguments) ?? "Localization.swift" + let outputClassName: String = tryParse(.outputClassName, from: arguments) ?? "Localization" + let baseLanguageCode: String = tryParse(.baseLanguageCode, from: arguments) ?? "en" + let whitelistFile: String? = tryParse(.whitelist, from: arguments) + let parameterDetection: Localization.ParameterDetection? + if + let startRegex: String = tryParse(.parameterStartRegex, from: arguments), + let endRegex: String = tryParse(.parameterEndRegex, from: arguments), + let startOffset: Int = tryParse(.parameterStartOffset, from: arguments), + let endOffset: Int = tryParse(.parameterEndOffset, from: arguments) { + parameterDetection = Localization.ParameterDetection(startRegex: startRegex, endRegex: endRegex, startOffset: startOffset, endOffset: endOffset) + } else { + parameterDetection = nil + } + return Localization.Parameters(searchPath: searchPath, templateFile: templateFile, outputFilename: outputFilename, outputClassName: outputClassName, baseLanguageCode: baseLanguageCode, parameterDetection: parameterDetection, whitelistFile: whitelistFile) + } +} diff --git a/regen/Localization/LocalizationParser.swift b/regen/Localization/LocalizationParser.swift index 93cc844..00e8211 100644 --- a/regen/Localization/LocalizationParser.swift +++ b/regen/Localization/LocalizationParser.swift @@ -7,107 +7,119 @@ import Foundation -struct LocalizationEntry { - var path : String - var key : String - var value : String - var property : String -} +extension Localization { -class LocalizationParser { - - func parseLocalizationFile(_ file : String) -> [LocalizationEntry] { - var localizationEntries : [LocalizationEntry] = [] - let content : String - do { - Logger.verbose("\t\tLoading localization file: started") - content = try String(contentsOfFile: file) - Logger.verbose("\t\tLoading localization file: finished") - Logger.verbose("\t\tAnalyzing localization file: started \(file)") - let lines = content.components(separatedBy: "\n") - for line in lines { - let keyValue = parseLocalizationLine(line) - if let key = keyValue.0, let value = keyValue.1 { - let property = key.propertyName() - localizationEntries.append(LocalizationEntry(path: file, key: key, value: value, property: property)) - } - } - Logger.verbose("\t\tAnalyzing localization file: finished \(file)") - } catch { - Logger.error("\(error)") - content = "" - } - return localizationEntries + struct KeyValue: Codable { + let key: String + let value: String } - - func appendEntries(_ from : [LocalizationEntry], to : inout [LocalizationEntry]) { - for entry in from { - if LocalizationParser.contains(to, key: entry.key) { - continue - } - to.append(entry) - } - } - - static func contains(_ entries : [LocalizationEntry], key : String) -> Bool { - for entry in entries { - if entry.key == key { - return true - } - } - return false + + struct Entry: Codable { + let path : String + let key : String + let value : String + let property : String + let params: [KeyValue] } - - func parseLocalizationLine(_ line: String) -> (String?, String?) { - var key:String = "" - var value:String = "" - let trimmedLine = line.trimmingCharacters(in: CharacterSet.whitespaces) - guard trimmedLine.hasPrefix("\"") else { - return (nil, nil) + + class Parser { + let parameterDetection: ParameterDetection? + let whitelist: [String]? + + init(parameterDetection: ParameterDetection?, whitelist: [String]?) { + self.parameterDetection = parameterDetection + self.whitelist = whitelist } - var keyStarted = false - var keyFinished = false - var valueStarted = false - var valueFinished = false - var previousCharacter:Character = "\0" - for character in line.characters { - if character == "\"" { - if previousCharacter != "\\" { - if keyStarted == false && keyFinished == false { - keyStarted = true - continue - } - if keyStarted == true && keyFinished == false { - keyFinished = true - continue - } - if keyFinished == true && valueStarted == false { - valueStarted = true - continue - } - if valueStarted == true && valueFinished == false { - valueFinished = true - continue + + func parseLocalizationFile(_ file : String) -> [Entry] { + var localizationEntries : [Entry] = [] + let content : String + do { + content = try String(contentsOfFile: file) + let lines = content.components(separatedBy: "\n") + for line in lines { + let keyValue = parseLocalizationLine(line) + if let key = keyValue.0, let value = keyValue.1 { + if let whitelist = self.whitelist { + if !whitelist.contains(key) { + continue + } + } + let property = key.propertyName() + var params: [KeyValue] = [] + if let parameterDetection = parameterDetection { + var range = value.startIndex.. (String?, String?) { + var key:String = "" + var value:String = "" + let trimmedLine = line.trimmingCharacters(in: CharacterSet.whitespaces) + guard trimmedLine.hasPrefix("\"") else { + return (nil, nil) } - if keyStarted == true && keyFinished == false { - key += String(character) + var keyStarted = false + var keyFinished = false + var valueStarted = false + var valueFinished = false + var previousCharacter:Character = "\0" + line.forEach { character in + if character == "\"" { + if previousCharacter != "\\" { + if keyStarted == false && keyFinished == false { + keyStarted = true + return + } + if keyStarted == true && keyFinished == false { + keyFinished = true + return + } + if keyFinished == true && valueStarted == false { + valueStarted = true + return + } + if valueStarted == true && valueFinished == false { + valueFinished = true + return + } + } + } + if character == "#" && keyStarted == true && keyFinished == false { + keyFinished = true + return + } + if keyStarted == true && keyFinished == false { + key += String(character) + } + if valueStarted == true && valueFinished == false { + value += String(character) + } + previousCharacter = character } - if valueStarted == true && valueFinished == false { - value += String(character) + if keyStarted == true && keyFinished == true && valueStarted == true && valueFinished == true { + return (key, value) + } else { + return (nil, nil) } - previousCharacter = character - } - if keyStarted == true && keyFinished == true && valueStarted == true && valueFinished == true { - return (key, value) - } else { - return (nil, nil) } + } - } diff --git a/regen/Localization/Models/LocalizationParameters.swift b/regen/Localization/Models/LocalizationParameters.swift new file mode 100644 index 0000000..02c187b --- /dev/null +++ b/regen/Localization/Models/LocalizationParameters.swift @@ -0,0 +1,35 @@ +// +// LocalizationParams.swift +// regen +// +// Created by Ido Mizrachi on 14/07/2019. +// Copyright © 2019 Ido Mizrachi. All rights reserved. +// + +import Foundation + +extension Localization { + struct Parameters { + let searchPath: String + let templateFile: String + let outputFilename: String + let outputClassName: String + let baseLanguageCode: String + let parameterDetection: ParameterDetection? + let whitelistFile: String? + } + + // This is useful if you localization strings contains custom parameters, for example: + // "Hello #{user}, you have #{unread_count} unread messages" + // In this case: + // the start regex will detect #{ + // the end regex will detect } + // the start offset will be 2 + // the end offset will be 1 + struct ParameterDetection { + let startRegex: String + let endRegex: String + let startOffset: Int + let endOffset: Int + } +} diff --git a/regen/OperationType.swift b/regen/OperationType.swift index 763ea75..1840385 100644 --- a/regen/OperationType.swift +++ b/regen/OperationType.swift @@ -8,8 +8,19 @@ import Foundation enum OperationType { + // Info case version - case images - case localization case usage + // Actions + case images(parameters: Images.Parameters) + case localization(parameters: Localization.Parameters) +} + +extension OperationType { + enum Keys: String { + case version = "--version" + case usage + case images + case localization + } } diff --git a/regen/Resources/ImagesTemplate-1.swift b/regen/Resources/ImagesTemplate-1.swift new file mode 100644 index 0000000..f9eeb58 --- /dev/null +++ b/regen/Resources/ImagesTemplate-1.swift @@ -0,0 +1,8 @@ +public enum {{ className }} { + {% if root %}static let sharedInstance = {{ className }}() + + {% endif %}{% for folder in folders %} let _{{ folder.propertyName }}: {{folder.className }} = {{folder.className }}() + {% endfor %} + {% for image in images %} let {{ image.propertyName }}: String = "{{ image.imageSetName }}" + {% endfor %} +} diff --git a/regen/Resources/ImagesTemplate.swift b/regen/Resources/ImagesTemplate.swift new file mode 100644 index 0000000..829f677 --- /dev/null +++ b/regen/Resources/ImagesTemplate.swift @@ -0,0 +1,12 @@ +{% if parent %} +extension {{ parent }} { + enum {{ className }} { + {% for image in images %} static let {{ image.propertyName }}: String = "{{ image.imageSetName }}" + {% endfor %}} +} +{% else %} +enum {{ className }} { +{% for image in images %} static let {{ image.propertyName }}: String = "{{ image.imageSetName }}" +{% endfor %}} +{% endif %} + diff --git a/regen/Resources/LocalizationTemplate.swift b/regen/Resources/LocalizationTemplate.swift new file mode 100644 index 0000000..ac4738b --- /dev/null +++ b/regen/Resources/LocalizationTemplate.swift @@ -0,0 +1,5 @@ +import Foundation + +public struct {{ className }} { +{% for property in properties %} static let {{ property.property }} = NSLocalizedString("{{ property.key }}", comment: "{{ property.value }}") +{% endfor %}} diff --git a/regen/Resources/ParametersLocalizationTemplate.swift b/regen/Resources/ParametersLocalizationTemplate.swift new file mode 100644 index 0000000..243f067 --- /dev/null +++ b/regen/Resources/ParametersLocalizationTemplate.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct {{ className }} { +{% for property in properties %} + static func {{ property.property }}({% for param in property.params %}{{ param.key }}: String{% if forloop.last == false %}, {% endif %}{% endfor %}) { + return localize("{{ property.key }}", + defaultValue: "{{ property.value }}", + parameters: {% if property.params.count == 0 %}nil{% else %} [{% for param in property.params %}"{{ param.value }}": {{ param.key }}{% if forloop.last == false %}, {% endif %}{% empty %}:{% endfor %}]{% endif %}) + } +{% endfor %} +} + + diff --git a/regen/Resources/whitelist.txt b/regen/Resources/whitelist.txt new file mode 100644 index 0000000..51c8da9 --- /dev/null +++ b/regen/Resources/whitelist.txt @@ -0,0 +1,173 @@ +error_showing_qr_scanner_title +welcome_to_title +driver_app_name +login_view_login_with_email_and_password +login_version_label +prompt_password +prompt_email +prompt_phone +hint_verification_code +forgot_password +back_to_login_button_title +next_button_title +action_sign_in_short +send_login_code_via_sms_button_title +error +OK +login_view_select_merchant_header_title +login_view_recover_password_screen_header +alternative_login_phone +alternative_login_qrcode +login_view_enter_sms_verification_code +choose_country_screen_title +cancel +recovery_email_sent_alert_title +recovery_email_sent_alert_message +change_phone_number +wrong_credentials_message +error_login_not_a_driver +failed_to_login_message +refresh_hint +drawer_my_tasks +drawer_future_tasks +order_by_time +order_by_priority +order_by_distance +unknown +yesterday +days_ago +empty_task_list_message +failedLoadingOrderErrorMessage +waypoint_view_scheduled_to_title +waypoint_view_promised_time_title +eta_title +contact +title_order_with_id +inventory_collect_label +inventory_deliver_label +inventory_quantity +inventory_item_title +inventory_quantity_title +inventory_pickup_count_title +inventory_dropoff_count_title +no_internet_connection +wifi_not_connected_to_internet +gps_off_message_ios +payments_history_screen_title +payments_history_section_header_payments +payments_history_section_header_blocks_and_orders +payments_history_deliveries +payments_history_deliveries_rate +payments_history_total_paid_label +payments_history_tab_daily +payments_history_tab_weekly +payments_history_tab_monthly +payments_history_miles +payments_history_miles_rate +payments_history_kilometers +payments_history_kilometers_rate +payments_history_returned_orders +payments_history_returned_orders_rate +payments_history_hours +payments_history_hours_rate +payments_history_minutes +payments_history_minutes_rate +payments_history_tips_label +payments_history_orders_offered_label +payments_history_orders_accepted_label +payments_history_delivery_blocks_label +payments_history_try_again +payments_history_error_title +payments_history_error_subtitle +order_completed_label +waypoint_completed +pending_button +checkin_button +checkout_button_on_route_mode +checkout_button +accept_button +start_button +start_button_pick_up +start_button_drop_off +checkout_button_pick_up +checkout_button_drop_off +error_accepting_task +error_arriving_at_waypoint +error_locally_starting_task +error_checking_out_from_waypoint +sidebar_app_subtitle +sidebar_version_label +sidebar_option_title_call_dispatch +sidebar_option_title_work_schedule +action_bar_inbox +sidebar_option_title_add_an_order +sidebar_option_title_scan_an_item +sidebar_option_title_role +sidebar_option_title_routes +sidebar_option_title_order_history +sidebar_option_title_options +drawer_logout +shift_status_label_on_shift_state_title +shift_status_label_off_shift_state_title +shortcut_end_shift +shortcut_start_shift +start_shift +slide_to_start_shift +side_menu_on_shift +starting_shift +another_device_error +another_device_button +stop_index_and_total_count +incoming_text_title_no_params +incoming_task_button_accept +not_now +got_it_text +canceled_task_title +single_order_pending_accept +few_orders_pending_accept +todays_orders +sort_orders_by_time +sort_orders_by_location +sort_orders_by_priority +address_type_commercial +address_type_residential +address_type_educational +address_type_government +address_type_medical +address_type_industrial +messaging_center_title +retry_button_label_text +failed_to_load_conversations +conversation_list_driver_to_team_dispatchers_title +conversation_view_failed_loading_messages +failed_to_load_conversation +conversation_view_start_shift_to_send_and_receive_message +conversation_view_connection_label_text +conversation_message_cell_sending_status +conversation_message_cell_failed_sending_status +label_vehicle +vehicle_selection_title +vehicle_fetch_failed +vehicle_update_failed +start_shift_form_title +clear_signature_button_label +action_add_signature +drawer_feedback +drawer_about +action_add_photo +action_add_note +type_your_note +feedback_write_note_view_placeholder +action_add_form +action_add_payment +collect_cash +action_edit_inventory +action_reject_inventory +cancel_task_title +take_scan_button +take_tip_button +browser_task_action_default_title +order_removed_by_dispatcher_alert_title +dismiss +orders_list_failed_loading_orders_message +error_mandatory_actions_not_taken_dialog diff --git a/regen/Usage.swift b/regen/Usage.swift index 632246b..e83116b 100644 --- a/regen/Usage.swift +++ b/regen/Usage.swift @@ -7,40 +7,50 @@ import Foundation + +// Usage: +// regen supports two type of code generation: +// 1. Images - by scanning ".xcassets" packages +// 2. Localization - by scanning string files +// +// Type any of the following for additiona help: +// regen images --help +// regen localization --help + class Usage { - static func display(color: Bool) { - let options = [ ["--version ", "Prints the current version"], - ["--output FILE ", "Set the generated file name (without extension)"] , - ["--scanType TYPE ", "Use images or localization\n\t\timages - scans the projects .xcassets files\n\t\tlocalization - scans the projects Localizable.strings file"], - ["--language LANGUAGE", "Use swift or objc\n\t\tSets the language of the generated filess"], - ["--verbose or -v ", "Print detailed information while running"], - ["--nocolor ", "Don't use colors in console output"] - ] - var usageHeader: String = "Usage:" - if color { - usageHeader = usageHeader.bold.underline - } - - print(usageHeader) - levelPrint("$ regen [options]") - newLine() - - var optionsHeader: String = "Options" - if color { - optionsHeader = optionsHeader.bold.underline - } - print(optionsHeader) - for (option) in options { - levelPrint("\(option[0]) \t\t\t \(option[1])") - } - } - - private static func levelPrint(_ string : String) { - print("\t" + string) - } - - private static func newLine() { - print("") + + static func display() { + let usage = #""" + Usage: + $ regen localization [options] + or + $ regen images [options] + or + + General: + --version Prints the current tool version + + Localization Parameters: + --search-path The path of the .strings files, for example: /Users/me/dev/project + --template Stencil template file, for example: Template.txt + --output-filename The output filename, for example: Localization.swift, default value: Localization.swift + --output-class-name The generated class name, for example: LocalizableStrings, default value: Localization + --base-language-code The locate of the .strings file that will be scanned, default value: en + --whitelist-filename If specified, the file with a list of strings keys to include in the generated file + Handling Localization Parameters: "hello %d" / "Welcome #{user}" + --parameter-start-regex The regex for the beginning of the parameters, in the above cases a regex for % or #{ + --parameter-end-regex The regex for the beginning of the parameters, in the above cases a regex for d or } + --parameter-start-offset The number of characters to skip when a parameter for example to extract only "user" from #{users} skip 2 characters from the start + --parameter-end-offset The number of characters to skip when a parameter for example to extract only "user" from #{users} skip 1 character from the end + + Images Parameters: + --assets The .xcassets file path + --template Stencil template file, for example: Template.txt + --output-filename The output filename, for example: Localization.swift, default value: Localization.swift + --output-class-name The generated class name, for example: LocalizableStrings, default value: Localization + """# + + + print(usage) } - } diff --git a/regen/Version.swift b/regen/Version.swift index 998b3ef..310ce5b 100644 --- a/regen/Version.swift +++ b/regen/Version.swift @@ -8,7 +8,7 @@ import Cocoa class Version { - static let current = "0.0.9" + static let current = "0.0.10" static func display() { print(Version.current) diff --git a/regen/main.swift b/regen/main.swift index 8b2d60f..836a831 100644 --- a/regen/main.swift +++ b/regen/main.swift @@ -1,4 +1,4 @@ -// +// // main.swift // Regen // @@ -7,72 +7,26 @@ import Foundation -class Main { - - private let fileManager: FileManager = FileManager.default - - public func run() { - var skipOperationTimePrint:Bool = false - let operationTimer = OperationTimer() - operationTimer.start() - - let argumentsParser = ArgumentsParser(arguments: CommandLine.arguments) - let operationType = argumentsParser.operationType() - let path = fileManager.currentDirectoryPath - - Logger.logLevel = argumentsParser.verbose ? .verbose : .info; - Logger.color = argumentsParser.color - - switch operationType { - case .version: - skipOperationTimePrint = true - Version.display() - - case .images: - self.runImageOperation(inPath: path, language: argumentsParser.language, nullableOutputFile: argumentsParser.output) - - case .localization: - self.runLocalizationOperation(inPath: path, language: argumentsParser.language, nullableOutputFile: argumentsParser.output) - - default: - skipOperationTimePrint = true - Usage.display(color: argumentsParser.color) - } - - if skipOperationTimePrint == false { - let totalTime = String(format: "%.2f", operationTimer.end()) - Logger.info("Finish in \(totalTime) seconds") - } - - exit(EXIT_SUCCESS) - } - - private func runImageOperation(inPath: String, language: Language, nullableOutputFile: String?) { - Logger.info("Searching for images in path: \(inPath)") - let imageOperation = ImageOperation(fileManager: fileManager, language: language) - let outputFile: String - if let nonNilOutputFile = nullableOutputFile { - outputFile = nonNilOutputFile - } else { - outputFile = "Images" - } - imageOperation.run(inPath, output: fileManager.currentDirectoryPath + "/" + outputFile) - } - - private func runLocalizationOperation(inPath: String, language: Language, nullableOutputFile: String?) { - Logger.info("Searching for localization files in path: \(inPath)") - let localizationOperation = LocalizationOperation(fileManager: fileManager, language: language) - let outputFile: String - if let nonNilOutputFile = nullableOutputFile { - outputFile = nonNilOutputFile - } else { - outputFile = "Strings" - } - localizationOperation.run(inPath, output: fileManager.currentDirectoryPath + "/" + outputFile) - - } +let operationTimer = OperationTimer() +operationTimer.start() + +let arguments = Array(CommandLine.arguments.dropFirst()) + +let argumentsParser = ArgumentsParser(arguments: arguments) +switch argumentsParser.operationType { +case .version: + Version.display() +case .usage: + Usage.display() +case .localization(let parameters): + let operation = Localization.Operation(parameters: parameters) + operation.run() + print("Finished in: \(String(format: "%.5f", operationTimer.end())) seconds.") +case .images(let parameters): + let operation = Images.Operation(parameters: parameters) + operation.run() + print("Finished in: \(String(format: "%.5f", operationTimer.end())) seconds.") } -let main = Main() -main.run() +