├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .swift-format ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Resourceror │ ├── Generate.swift │ └── main.swift └── ResourcerorCore │ ├── Extensions │ └── StringExtensions.swift │ ├── ResourceListGenerator.swift │ ├── Results │ ├── ResultType.swift │ └── ScanResult.swift │ └── Scanners │ ├── AssetCatalogScanner.swift │ ├── AudioFileScanner.swift │ ├── ImageFileScanner.swift │ ├── InterfaceBuilderDocumentScanner.swift │ ├── ResourceScanning.swift │ └── StoryboardScanner.swift └── Tests └── ResourcerorCoreTests └── StringExtensionsTests.swift /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Provide a descriptive summary of the issue. 2 | 3 | ## Steps to Reproduce 4 | 5 | 1. Describe any setup of pre-work (e.g. Run this command…) 6 | 2. Detail the exact steps taken to produce the problem 7 | 3. Number each step 8 | 9 | ## Expected Results 10 | 11 | Describe what you expected to happen after completing the steps above. 12 | 13 | ## Actual Results 14 | 15 | Describe what actually happened after completing the steps above. 16 | 17 | ## Additional Notes 18 | 19 | Provide additional information, such as references to related problems or workarounds. 20 | 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Provide a descriptive summary of the changes in this PR. 2 | 3 | ## Fixed Issues 4 | 5 | - Fixes #9999 6 | 7 | ## Summary of Changes 8 | 9 | Changes proposed in this pull request: 10 | - 11 | - 12 | - 13 | 14 | ## Items of Note 15 | 16 | Document anything here that you think the reviewer(s) of this PR may need to know, or would be of specific interest. 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | .swiftpm/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | fastlane/report.xml 66 | fastlane/Preview.html 67 | fastlane/screenshots 68 | fastlane/test_output 69 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "blankLineBetweenMembers" : { 3 | "ignoreSingleLineProperties" : false 4 | }, 5 | "indentation" : { 6 | "spaces" : 2 7 | }, 8 | "lineLength" : 999999, 9 | "maximumBlankLines" : 1, 10 | "respectsExistingLineBreaks" : false, 11 | "rules" : { 12 | "AllPublicDeclarationsHaveDocumentation" : true, 13 | "AlwaysUseLowerCamelCase" : true, 14 | "AmbiguousTrailingClosureOverload" : true, 15 | "AvoidInitializersForLiterals" : true, 16 | "BeginDocumentationCommentWithOneLineSummary" : true, 17 | "BlankLineBetweenMembers" : false, 18 | "CaseIndentLevelEqualsSwitch" : true, 19 | "DoNotUseSemicolons" : true, 20 | "DontRepeatTypeInStaticProperties" : true, 21 | "FullyIndirectEnum" : true, 22 | "GroupNumericLiterals" : true, 23 | "IdentifiersMustBeASCII" : true, 24 | "MultiLineTrailingCommas" : true, 25 | "NeverForceUnwrap" : true, 26 | "NeverUseForceTry" : true, 27 | "NeverUseImplicitlyUnwrappedOptionals" : true, 28 | "NoAccessLevelOnExtensionDeclaration" : true, 29 | "NoBlockComments" : true, 30 | "NoCasesWithOnlyFallthrough" : true, 31 | "NoEmptyAssociatedValues" : true, 32 | "NoEmptyTrailingClosureParentheses" : true, 33 | "NoLabelsInCasePatterns" : true, 34 | "NoLeadingUnderscores" : true, 35 | "NoParensAroundConditions" : true, 36 | "NoVoidReturnOnFunctionSignature" : true, 37 | "OneCasePerLine" : true, 38 | "OneVariableDeclarationPerLine" : true, 39 | "OnlyOneTrailingClosureArgument" : true, 40 | "OrderedImports" : true, 41 | "ReturnVoidInsteadOfEmptyTuple" : true, 42 | "UseEnumForNamespacing" : true, 43 | "UseLetInEveryBoundCaseVariable" : true, 44 | "UseOnlyUTF8" : true, 45 | "UseShorthandTypeNames" : true, 46 | "UseSingleLinePropertyGetter" : true, 47 | "UseSpecialEscapeSequences" : true, 48 | "UseSynthesizedInitializer" : true, 49 | "UseTripleSlashForDocumentationComments" : true, 50 | "ValidateDocumentationComments" : true 51 | }, 52 | "tabWidth" : 8, 53 | "version" : 1 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 The CocoaBots 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Files", 6 | "repositoryURL": "https://github.com/johnsundell/Files.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "22fe84797d499ffca911ccd896b34efaf06a50b9", 10 | "version": "4.1.1" 11 | } 12 | }, 13 | { 14 | "package": "Regex", 15 | "repositoryURL": "https://github.com/sharplet/Regex.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "76c2b73d4281d77fc3118391877efd1bf972f515", 19 | "version": "2.1.1" 20 | } 21 | }, 22 | { 23 | "package": "swift-argument-parser", 24 | "repositoryURL": "https://github.com/apple/swift-argument-parser", 25 | "state": { 26 | "branch": null, 27 | "revision": "8d31a0905c346a45c87773ad50862b5b3df8dff6", 28 | "version": "0.0.4" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "Resourceror", 6 | platforms: [ 7 | .macOS(.v10_13) 8 | ], 9 | products: [ 10 | .executable(name: "resourceror", targets: ["Resourceror"]), 11 | .library(name: "ResourcerorCore", targets: ["ResourcerorCore"]) 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.1"), 15 | .package(url: "https://github.com/johnsundell/Files.git", from: "4.1.1"), 16 | .package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1") 17 | ], 18 | targets: [ 19 | .target( 20 | name: "Resourceror", 21 | dependencies: [ 22 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 23 | .target(name: "ResourcerorCore") 24 | ] 25 | ), 26 | .target( 27 | name: "ResourcerorCore", 28 | dependencies: [ 29 | .product(name: "Files", package: "Files"), 30 | .product(name: "Regex", package: "Regex") 31 | ] 32 | ), 33 | .testTarget(name: "ResourcerorCoreTests", dependencies: [ 34 | .target(name: "ResourcerorCore") 35 | ]) 36 | ], 37 | swiftLanguageVersions: [.v5] 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resourceror 2 | 3 | This tool will generate static variables as extensions to the common AppKit types that accepted typed names, i.e.: 4 | 5 | ```swift 6 | // Resourceror outputs the following, which you can add to your project: 7 | extension NSImage { 8 | static let backgroundImage = NSImage(named: "background-image") 9 | } 10 | 11 | // Which means you can now do the following: 12 | let backgroundImage = NSImage.backgroundImage 13 | ``` 14 | 15 | Neato! 16 | 17 | Resourceror currently supports extensions on `NSImage`, `NSNib`, `NSStoryboard`, `NSStoryboard.SceneIdentifier`, `NSStoryboardSegue.Identifier` and `NSUserInterfaceItemIdentifier`. Feel free to submit PRs for anything else you'd like to see generated. 18 | 19 | It's not quite finished, but you should be able to use it. The project lacks tests, the code is not polished, and everything needs a bit of work - it meets my needs, and it might be useful to you. 20 | 21 | ## Usage 22 | 23 | Basically, you run this command against a directory containing your images, XIBs, and Storyboards, and it will print a nicely sorted list of properly typed Swift static variables for your resources. 24 | 25 | ```sh 26 | swift run resourceror generate $PATH_TO_YOUR_DIRECTORY --exclude first_directory second_directory 27 | ``` 28 | 29 | ## Requirements 30 | 31 | - macOS 10.13 or newer 32 | - Swift 5.2 or newer 33 | 34 | ## Notes 35 | 36 | If you use SwiftLint, you'll probably need to surround the output code with commands to disable some checks, like so: 37 | 38 | ```swift 39 | // swiftlint:disable file_length 40 | // swiftlint:disable force_unwrap 41 | // swiftlint:disable identifier_name 42 | 43 | extension NSImage { 44 | // … 45 | } 46 | 47 | extension NSNib { 48 | // … 49 | } 50 | 51 | extension NSStoryboard { 52 | // … 53 | } 54 | 55 | extension NSStoryboard.SceneIdentifier { 56 | // … 57 | } 58 | 59 | extension NSStoryboardSegue.Identifier { 60 | // … 61 | } 62 | 63 | extension NSUserInterfaceItemIdentifier { 64 | // … 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /Sources/Resourceror/Generate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import ArgumentParser 6 | import Foundation 7 | import ResourcerorCore 8 | 9 | struct Generate: ParsableCommand { 10 | static var configuration 11 | = CommandConfiguration( 12 | commandName: "generate", 13 | abstract: "Generates a list of images, XIB and Storyboard files and their contained identifiers." 14 | ) 15 | 16 | @Argument(default: ".", help: "Path to scan for resources. If none is provided, the current directory will be used.") 17 | var path: String 18 | 19 | @Argument(help: "List of directory or file names to exclude.") 20 | var excluded: [String] 21 | 22 | func run() throws { 23 | let urlToScan = URL(fileURLWithPath: path, isDirectory: true, relativeTo: currentWorkingDirectoryURL) 24 | guard urlToScan.isFileURL else { throw Error.notFileURL(urlToScan) } 25 | 26 | // Everything is ready, scan the passed file URL 27 | try ResourceListGenerator().scanDirectory(at: urlToScan, excluding: excluded) 28 | } 29 | 30 | 31 | private let currentWorkingDirectoryURL = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) 32 | 33 | enum Error: Swift.Error { 34 | case notFileURL(Foundation.URL) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/Resourceror/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import ArgumentParser 6 | 7 | struct Resourceror: ParsableCommand { 8 | static var configuration = CommandConfiguration( 9 | abstract: "Resourceror generates Swift names for your Xcode project resources.", 10 | subcommands: [Generate.self], 11 | defaultSubcommand: Generate.self 12 | ) 13 | } 14 | 15 | Resourceror.main() 16 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Extensions/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import Foundation 6 | 7 | extension String { 8 | func pascalCased(followingCharacters characterSet: CharacterSet = CharacterSet.alphanumerics.inverted) -> String { return components(separatedBy: characterSet).map { $0.uppercasedFirstCharacter() }.joined(separator: "") } 9 | 10 | func camelCased(followingCharacters characterSet: CharacterSet = CharacterSet.alphanumerics.inverted) -> String { return pascalCased(followingCharacters: characterSet).lowercasedFirstCharacter() } 11 | 12 | func lowercasedFirstCharacter() -> String { 13 | guard isEmpty == false else { return self } 14 | guard count > 1 else { return uppercased() } 15 | 16 | let firstIndex = index(startIndex, offsetBy: 1) 17 | return self[.. String { 21 | guard isEmpty == false else { return self } 22 | guard count > 1 else { return uppercased() } 23 | 24 | let firstIndex = index(startIndex, offsetBy: 1) 25 | return self[.. = fileContentsScanners.reduce(into: []) { $0.formUnion($1.scanFileSystem()) } 18 | let folderResults: Set = folderScanners.reduce(into: []) { $0.formUnion($1.scanFileSystem()) } 19 | let groupedResults = Dictionary(grouping: fileResults.union(folderResults), by: { $0.type }).sorted { lhs, rhs -> Bool in 20 | lhs.key.rawValue.lexicographicallyPrecedes(rhs.key.rawValue) 21 | } 22 | 23 | for (type, results) in groupedResults { 24 | print("extension \(type.rawValue) {") 25 | results.sorted { lhs, rhs in 26 | lhs.identifier.lexicographicallyPrecedes(rhs.identifier) 27 | }.forEach { print(" \($0.outputLine)") } 28 | print("}\n") 29 | } 30 | } 31 | 32 | private func updateScannables(at url: URL, excluding: [String]) throws { 33 | let root = try Folder(path: url.path) 34 | try updateScannables(in: root, excluding: excluding) 35 | } 36 | 37 | private func updateScannables(in folder: Folder, excluding: [String]) throws { 38 | guard excluding.contains(folder.name) == false else { 39 | return 40 | } 41 | 42 | folder.files 43 | .filter { excluding.contains($0.name) == false } 44 | .forEach { 45 | for var scanner in fileContentsScanners { scanner.appendIfScannable(item: $0) } 46 | } 47 | 48 | try folder.subfolders.forEach { 49 | for var scanner in folderScanners { scanner.appendIfScannable(item: $0) } 50 | 51 | try self.updateScannables(in: $0, excluding: excluding) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Results/ResultType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import Foundation 6 | 7 | enum ResultType: String, Hashable { 8 | case audio = "NSSound.Name" 9 | case image = "NSImage" 10 | case namedColor = "NSColor" 11 | case nibName = "NSNib" 12 | case storyboardName = "NSStoryboard" 13 | 14 | case storyboardSceneIdentifier = "NSStoryboard.SceneIdentifier" 15 | case storyboardSegueIdentifier = "NSStoryboardSegue.Identifier" 16 | case userInterfaceItemIdentifier = "NSUserInterfaceItemIdentifier" 17 | 18 | case browserColumnsAutosaveName = "NSBrowser.ColumnsAutosaveName" 19 | case searchFieldRecentsAutosaveName = "NSSearchField.RecentsAutosaveName" 20 | case splitViewAutosaveName = "NSSplitView.AutosaveName" 21 | case tableViewAutosaveName = "NSTableView.AutosaveName" 22 | case windowFrameAutosaveName = "NSWindow.FrameAutosaveName" 23 | 24 | func hash(into hasher: inout Hasher) { hasher.combine(rawValue) } 25 | 26 | static func == (lhs: ResultType, rhs: ResultType) -> Bool { return lhs.rawValue == rhs.rawValue } 27 | 28 | func outputLine(for fileName: String) -> String { 29 | let name = variableName(using: fileName) 30 | switch self { 31 | case .audio, .image, .namedColor: return "static let \(name) = \(rawValue)(named: \"\(fileName)\")!" 32 | case .nibName: return "static let \(name) = NSNib(nibNamed: \"\(fileName)\", bundle: .main)!" 33 | case .storyboardName: return "static let \(name) = NSStoryboard(name: \"\(fileName)\", bundle: .main)" 34 | case .userInterfaceItemIdentifier: return "static let \(name) = NSUserInterfaceItemIdentifier(rawValue: \"\(fileName)\")" 35 | default: return "static let \(name) = \"\(fileName)\"" 36 | } 37 | } 38 | 39 | private func variableName(using fileName: String) -> String { return fileName.camelCased() } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Results/ScanResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import Foundation 6 | 7 | struct ScanResult: Hashable { 8 | let type: ResultType 9 | let identifier: String 10 | 11 | var outputLine: String { return type.outputLine(for: identifier) } 12 | 13 | func hash(into hasher: inout Hasher) { 14 | hasher.combine(type) 15 | hasher.combine(identifier) 16 | } 17 | 18 | static func == (lhs: ScanResult, rhs: ScanResult) -> Bool { return lhs.type == rhs.type && lhs.identifier == rhs.identifier } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Scanners/AssetCatalogScanner.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Tony Arnold (@tonyarnold) 2 | // Licensed under the MIT license. See the LICENSE file for details. 3 | 4 | import Files 5 | import Foundation 6 | import os.log 7 | import Regex 8 | 9 | final class AssetCatalogScanner: FolderScanning { 10 | static let requestedPathExtensions = ["xcassets"] 11 | 12 | var itemsToScan: [Folder] = [] 13 | 14 | func scan(item: Folder) -> Set { 15 | let results = try? item.subfolders.recursive 16 | .filter { $0.extension == "colorset" || $0.extension == "imageset" } 17 | .compactMap(scanCatalog(folder:)) 18 | 19 | return Set(results ?? []) 20 | } 21 | 22 | private func scanAssetGroup(folder: Folder) throws -> String? { 23 | // Determine if this asset group defines a namespace 24 | let contentsFile = try folder.file(named: "Contents.json") 25 | let contentsData = try contentsFile.read() 26 | let metadata = try JSONDecoder().decode(AssetMetadata.self, from: contentsData) 27 | return metadata.providesNamespace ? folder.name : nil 28 | } 29 | 30 | private func scanCatalog(folder: Folder) throws -> ScanResult? { 31 | let parentFolders = sequence(first: folder) { element in 32 | guard 33 | let itemExtension = element.extension, 34 | AssetCatalogScanner.requestedPathExtensions.contains(itemExtension) 35 | else { 36 | return element.parent 37 | } 38 | 39 | return nil 40 | } 41 | 42 | let prefix = try parentFolders 43 | .compactMap { try self.scanAssetGroup(folder: $0) } 44 | .joined(separator: "/") 45 | 46 | let identifier: String 47 | if prefix.isEmpty { 48 | identifier = folder.nameExcludingExtension 49 | } else { 50 | identifier = prefix + "/" + folder.nameExcludingExtension 51 | } 52 | 53 | let type: ResultType = folder.extension == "colorset" ? .namedColor : .image 54 | return ScanResult(type: type, identifier: identifier) 55 | } 56 | } 57 | 58 | private struct AssetMetadata: Decodable { 59 | var providesNamespace: Bool { 60 | return properties?.providesNamespace ?? false 61 | } 62 | 63 | private let properties: Properties? 64 | 65 | enum CodingKeys: String, CodingKey { 66 | case properties 67 | } 68 | 69 | private struct Properties: Decodable { 70 | let providesNamespace: Bool? 71 | 72 | private enum CodingKeys: String, CodingKey { 73 | case providesNamespace = "provides-namespace" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Scanners/AudioFileScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import Files 6 | import Foundation 7 | 8 | final class AudioFileScanner: FolderScanning { 9 | static let requestedPathExtensions = ["aif", "aiff", "mp3", "mp4", "m4a", "m4p"] 10 | 11 | var itemsToScan: [Folder] = [] 12 | 13 | func scan(item: Folder) -> Set { 14 | [ScanResult(type: .audio, identifier: item.nameExcludingExtension)] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Scanners/ImageFileScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import Files 6 | import Foundation 7 | 8 | final class ImageFileScanner: FolderScanning { 9 | static let requestedPathExtensions = ["png", "jpg", "jpeg", "pdf", "jp2"] 10 | 11 | var itemsToScan: [Folder] = [] 12 | 13 | func scan(item: Folder) -> Set { 14 | let folderName: String 15 | if let range = item.nameExcludingExtension.range(of: "@2x") { folderName = String(item.nameExcludingExtension[.. Set { 17 | // Open Storyboard, and scan for: 18 | guard let fileContents = try? item.readAsString() else { 19 | return [] 20 | } 21 | 22 | var results = Set() 23 | 24 | let fileResult = ScanResult(type: .nibName, identifier: item.nameExcludingExtension) 25 | results.insert(fileResult) 26 | 27 | type(of: self).identifierRegex.allMatches(in: fileContents).forEach { match in 28 | guard let tagName = match.captures[0], let identifier = match.captures[1], InterfaceBuilderDocumentScanner.ignoredTags.contains(tagName) == false else { return } 29 | 30 | let resultType: ResultType = tagName == "segue" ? .storyboardSegueIdentifier : .userInterfaceItemIdentifier 31 | let newResult = ScanResult(type: resultType, identifier: identifier) 32 | results.insert(newResult) 33 | } 34 | 35 | type(of: self).browserViewAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 36 | guard let identifier = match.captures[0] else { return } 37 | 38 | let newResult = ScanResult(type: .browserColumnsAutosaveName, identifier: identifier) 39 | results.insert(newResult) 40 | } 41 | 42 | type(of: self).searchFieldAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 43 | guard let identifier = match.captures[0] else { return } 44 | 45 | let newResult = ScanResult(type: .searchFieldRecentsAutosaveName, identifier: identifier) 46 | results.insert(newResult) 47 | } 48 | 49 | type(of: self).splitViewAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 50 | guard let identifier = match.captures[0] else { return } 51 | 52 | let newResult = ScanResult(type: .splitViewAutosaveName, identifier: identifier) 53 | results.insert(newResult) 54 | } 55 | 56 | type(of: self).tableViewAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 57 | guard let identifier = match.captures[0] else { return } 58 | 59 | let newResult = ScanResult(type: .tableViewAutosaveName, identifier: identifier) 60 | results.insert(newResult) 61 | } 62 | 63 | type(of: self).windowFrameAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 64 | guard let identifier = match.captures[0] else { return } 65 | 66 | let newResult = ScanResult(type: .windowFrameAutosaveName, identifier: identifier) 67 | results.insert(newResult) 68 | } 69 | 70 | return results 71 | } 72 | 73 | private static let deploymentIdentifierRegex = Regex("", options: .anchorsMatchLines) 74 | 75 | private static let identifierRegex = Regex("<([A-Za-z0-9]+).* identifier=\"([^\"]+)\".*$", options: .anchorsMatchLines) 76 | 77 | private static let browserViewAutosaveNameRegex = Regex(" Bool 14 | func scanFileSystem() -> Set 15 | func scan(item: File) -> Set 16 | } 17 | 18 | extension FileContentsScanning { 19 | mutating func appendIfScannable(item: File) { 20 | guard canScan(item: item) else { 21 | return 22 | } 23 | 24 | itemsToScan.append(item) 25 | } 26 | 27 | func canScan(item: File) -> Bool { 28 | guard let pathExtension = item.extension else { return false } 29 | 30 | return type(of: self).requestedPathExtensions.contains(pathExtension) 31 | } 32 | 33 | func scanFileSystem() -> Set { 34 | return itemsToScan 35 | .map(scan(item:)) 36 | .reduce(into: Set()) { $0.formUnion($1) } 37 | } 38 | } 39 | 40 | protocol FolderScanning { 41 | static var requestedPathExtensions: [String] { get } 42 | var itemsToScan: [Folder] { get set } 43 | 44 | mutating func appendIfScannable(item: Folder) 45 | func canScan(item: Folder) -> Bool 46 | func scanFileSystem() -> Set 47 | func scan(item: Folder) -> Set 48 | } 49 | 50 | extension FolderScanning { 51 | mutating func appendIfScannable(item: Folder) { 52 | guard canScan(item: item) else { 53 | return 54 | } 55 | 56 | itemsToScan.append(item) 57 | } 58 | 59 | func canScan(item: Folder) -> Bool { 60 | guard let pathExtension = item.extension else { return false } 61 | 62 | return type(of: self).requestedPathExtensions.contains(pathExtension) 63 | } 64 | 65 | func scanFileSystem() -> Set { 66 | return itemsToScan 67 | .map(scan(item:)) 68 | .reduce(into: Set()) { $0.formUnion($1) } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ResourcerorCore/Scanners/StoryboardScanner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright © 2017 Tony Arnold (@tonyarnold) 3 | // Licensed under the MIT license. See the LICENSE file for details. 4 | 5 | import Files 6 | import Foundation 7 | import Regex 8 | 9 | final class StoryboardScanner: FileContentsScanning { 10 | static let requestedPathExtensions = ["storyboard"] 11 | 12 | static let ignoredTags = ["deployment", "plugIn"] 13 | 14 | var itemsToScan: [File] = [] 15 | 16 | func scan(item: File) -> Set { 17 | // Open Storyboard, and scan for: 18 | guard let fileContents = try? item.readAsString() else { 19 | return [] 20 | } 21 | 22 | var results = Set() 23 | 24 | let fileResult = ScanResult(type: .storyboardName, identifier: item.nameExcludingExtension) 25 | results.insert(fileResult) 26 | 27 | type(of: self).storyboardIdentifierRegex.allMatches(in: fileContents).forEach { match in 28 | guard let identifier = match.captures[0] else { return } 29 | 30 | let newResult = ScanResult(type: .storyboardSceneIdentifier, identifier: identifier) 31 | results.insert(newResult) 32 | } 33 | 34 | type(of: self).identifierRegex.allMatches(in: fileContents).forEach { match in 35 | guard let tagName = match.captures[0], let identifier = match.captures[1], StoryboardScanner.ignoredTags.contains(tagName) == false else { return } 36 | 37 | let resultType: ResultType = tagName == "segue" ? .storyboardSegueIdentifier : .userInterfaceItemIdentifier 38 | let newResult = ScanResult(type: resultType, identifier: identifier) 39 | results.insert(newResult) 40 | } 41 | 42 | type(of: self).browserViewAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 43 | guard let identifier = match.captures[0] else { return } 44 | 45 | let newResult = ScanResult(type: .browserColumnsAutosaveName, identifier: identifier) 46 | results.insert(newResult) 47 | } 48 | 49 | type(of: self).searchFieldAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 50 | guard let identifier = match.captures[0] else { return } 51 | 52 | let newResult = ScanResult(type: .searchFieldRecentsAutosaveName, identifier: identifier) 53 | results.insert(newResult) 54 | } 55 | 56 | type(of: self).splitViewAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 57 | guard let identifier = match.captures[0] else { return } 58 | 59 | let newResult = ScanResult(type: .splitViewAutosaveName, identifier: identifier) 60 | results.insert(newResult) 61 | } 62 | 63 | type(of: self).tableViewAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 64 | guard let identifier = match.captures[0] else { return } 65 | 66 | let newResult = ScanResult(type: .tableViewAutosaveName, identifier: identifier) 67 | results.insert(newResult) 68 | } 69 | 70 | type(of: self).windowFrameAutosaveNameRegex.allMatches(in: fileContents).forEach { match in 71 | guard let identifier = match.captures[0] else { return } 72 | 73 | let newResult = ScanResult(type: .windowFrameAutosaveName, identifier: identifier) 74 | results.insert(newResult) 75 | } 76 | 77 | return results 78 | } 79 | 80 | private static let deploymentIdentifierRegex = Regex("", options: .anchorsMatchLines) 81 | private static let identifierRegex = Regex("<([A-Za-z0-9]+).* identifier=\"([^\"]+)\".*$", options: .anchorsMatchLines) 82 | private static let storyboardIdentifierRegex = Regex("<[windowController|viewController|splitViewController].* storyboardIdentifier=\"([^\"]+)\".*$", options: .anchorsMatchLines) 83 | private static let browserViewAutosaveNameRegex = Regex("