diff --git a/Sources/FigmaGen/Commands/ImagesCommand.swift b/Sources/FigmaGen/Commands/ImagesCommand.swift index f9aeda0..492e5b1 100644 --- a/Sources/FigmaGen/Commands/ImagesCommand.swift +++ b/Sources/FigmaGen/Commands/ImagesCommand.swift @@ -156,6 +156,15 @@ final class ImagesCommand: AsyncExecutableCommand, GenerationConfigurableCommand """ ) + let namingStyle = Key( + "--naming-style", + "-s", + description: """ + Optional image output naming style, can be 'camelCase' or 'snakeCase'. + Defaults to 'camelCase'. + """ + ) + // MARK: - Initializers init(generator: ImagesGenerator) { @@ -191,6 +200,20 @@ final class ImagesCommand: AsyncExecutableCommand, GenerationConfigurableCommand } ?? [.none] } + private func resolveNamingStyle() -> ImageNamingStyle { + switch namingStyle.value { + case nil: + return .camelCase + + case let rawNamingStyle?: + guard let format = ImageNamingStyle(rawValue: rawNamingStyle) else { + fail(message: "Failed to generated images: Invalid naming style (\(rawNamingStyle))") + } + + return format + } + } + private func resolveImagesConfiguration() -> ImagesConfiguration { ImagesConfiguration( generatation: generationConfiguration, @@ -202,7 +225,8 @@ final class ImagesCommand: AsyncExecutableCommand, GenerationConfigurableCommand onlyExportables: onlyExportables.value, useAbsoluteBounds: useAbsoluteBounds.value, preserveVectorData: preserveVectorData.value, - groupByFrame: groupByFrame.value + groupByFrame: groupByFrame.value, + namingStyle: resolveNamingStyle() ) } diff --git a/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift b/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift index 8f56379..9789489 100644 --- a/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift +++ b/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift @@ -64,7 +64,8 @@ extension ImagesConfiguration { onlyExportables: onlyExportables, useAbsoluteBounds: useAbsoluteBounds, preserveVectorData: preserveVectorData, - groupByFrame: groupByFrame + groupByFrame: groupByFrame, + namingStyle: namingStyle ) } } diff --git a/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift b/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift index de94666..9985ca2 100644 --- a/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift +++ b/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift @@ -14,6 +14,7 @@ struct ImagesConfiguration: Decodable { case useAbsoluteBounds case preserveVectorData case groupByFrame + case namingStyle } // MARK: - Instance Properties @@ -28,6 +29,7 @@ struct ImagesConfiguration: Decodable { let useAbsoluteBounds: Bool let preserveVectorData: Bool let groupByFrame: Bool + let namingStyle: ImageNamingStyle // MARK: - Initializers @@ -41,7 +43,8 @@ struct ImagesConfiguration: Decodable { onlyExportables: Bool, useAbsoluteBounds: Bool, preserveVectorData: Bool, - groupByFrame: Bool + groupByFrame: Bool, + namingStyle: ImageNamingStyle ) { self.generatation = generatation self.assets = assets @@ -53,6 +56,7 @@ struct ImagesConfiguration: Decodable { self.useAbsoluteBounds = useAbsoluteBounds self.preserveVectorData = preserveVectorData self.groupByFrame = groupByFrame + self.namingStyle = namingStyle } init(from decoder: Decoder) throws { @@ -68,6 +72,7 @@ struct ImagesConfiguration: Decodable { useAbsoluteBounds = try container.decodeIfPresent(forKey: .useAbsoluteBounds) ?? false preserveVectorData = try container.decodeIfPresent(forKey: .preserveVectorData) ?? false groupByFrame = try container.decodeIfPresent(forKey: .groupByFrame) ?? false + namingStyle = try container.decodeIfPresent(forKey: .namingStyle) ?? .camelCase generatation = try GenerationConfiguration(from: decoder) } @@ -85,7 +90,8 @@ struct ImagesConfiguration: Decodable { onlyExportables: onlyExportables, useAbsoluteBounds: useAbsoluteBounds, preserveVectorData: preserveVectorData, - groupByFrame: groupByFrame + groupByFrame: groupByFrame, + namingStyle: namingStyle ) } } diff --git a/Sources/FigmaGen/Models/Images/ImageNamingStyle.swift b/Sources/FigmaGen/Models/Images/ImageNamingStyle.swift new file mode 100644 index 0000000..8155e05 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageNamingStyle.swift @@ -0,0 +1,9 @@ +import Foundation + +enum ImageNamingStyle: String, Codable { + + // MARK: - Enumeration Cases + + case camelCase + case snakeCase +} diff --git a/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift b/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift index f61c279..4f89f4d 100644 --- a/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift +++ b/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift @@ -13,4 +13,5 @@ struct ImagesParameters { let useAbsoluteBounds: Bool let preserveVectorData: Bool let groupByFrame: Bool + let namingStyle: ImageNamingStyle } diff --git a/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift b/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift index d562f44..0c9b29e 100644 --- a/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift +++ b/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift @@ -19,32 +19,49 @@ final class DefaultImageAssetsProvider: ImageAssetsProvider, ImagesFolderPathRes // MARK: - Instance Methods + private func resolveName( + for node: ImageRenderedNode, + setNode: ImageComponentSetRenderedNode, + namingStyle: ImageNamingStyle + ) -> String { + let name = setNode.isSingleComponent ? node.base.name : "\(setNode.name) \(node.base.name)" + + switch namingStyle { + case .camelCase: + return name.camelized + + case .snakeCase: + return name.snakeCased + } + } + private func makeAsset( for node: ImageRenderedNode, setNode: ImageComponentSetRenderedNode, - format: ImageFormat, - preserveVectorData: Bool, - groupByFrame: Bool, + parameters: ImagesParameters, folderPath: Path ) -> ImageAsset { - let name = setNode.isSingleComponent ? node.base.name.camelized : "\(setNode.name) \(node.base.name)".camelized - let folderPath = resolveFolderPath(groupByFrame: groupByFrame, setNode: setNode, folderPath: folderPath) + let name = resolveName(for: node, setNode: setNode, namingStyle: parameters.namingStyle) + + let folderPath = resolveFolderPath( + groupByFrame: parameters.groupByFrame, + setNode: setNode, + folderPath: folderPath + ) let filePaths = node.urls.keys.reduce(into: [:]) { result, scale in result[scale] = folderPath .appending(fileName: name, extension: AssetImageSet.pathExtension) - .appending(fileName: name.appending(scale.fileNameSuffix), extension: format.fileExtension) + .appending(fileName: name.appending(scale.fileNameSuffix), extension: parameters.format.fileExtension) .string } - return ImageAsset(name: name, filePaths: filePaths, preserveVectorData: preserveVectorData) + return ImageAsset(name: name, filePaths: filePaths, preserveVectorData: parameters.preserveVectorData) } private func makeAssets( for nodes: [ImageComponentSetRenderedNode], - format: ImageFormat, - preserveVectorData: Bool, - groupByFrame: Bool, + parameters: ImagesParameters, folderPath: Path ) -> [ImageComponentSetAsset] { nodes.map { setNode in @@ -54,9 +71,7 @@ final class DefaultImageAssetsProvider: ImageAssetsProvider, ImagesFolderPathRes assets[node] = makeAsset( for: node, setNode: setNode, - format: format, - preserveVectorData: preserveVectorData, - groupByFrame: groupByFrame, + parameters: parameters, folderPath: folderPath ) } @@ -130,17 +145,13 @@ final class DefaultImageAssetsProvider: ImageAssetsProvider, ImagesFolderPathRes func saveImages( nodes: [ImageComponentSetRenderedNode], - format: ImageFormat, - preserveVectorData: Bool, - groupByFrame: Bool, + parameters: ImagesParameters, in folderPath: String ) -> Promise<[ImageComponentSetAsset]> { perform(on: DispatchQueue.global(qos: .userInitiated)) { self.makeAssets( for: nodes, - format: format, - preserveVectorData: preserveVectorData, - groupByFrame: groupByFrame, + parameters: parameters, folderPath: Path(folderPath) ) }.nest { assets in @@ -152,7 +163,7 @@ final class DefaultImageAssetsProvider: ImageAssetsProvider, ImagesFolderPathRes ) } }.then { assets in - try self.saveAssetFolders(assets: assets, groupByFrame: groupByFrame, in: folderPath) + try self.saveAssetFolders(assets: assets, groupByFrame: parameters.groupByFrame, in: folderPath) }.then { self.saveImageFiles(assets: assets) } diff --git a/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift b/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift index 4a67357..2eb7389 100644 --- a/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift +++ b/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift @@ -7,9 +7,7 @@ protocol ImageAssetsProvider { func saveImages( nodes: [ImageComponentSetRenderedNode], - format: ImageFormat, - preserveVectorData: Bool, - groupByFrame: Bool, + parameters: ImagesParameters, in folderPath: String ) -> Promise<[ImageComponentSetAsset]> } diff --git a/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift b/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift index 72b3716..bc0097c 100644 --- a/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift +++ b/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift @@ -149,17 +149,13 @@ final class DefaultImagesProvider: ImagesProvider { private func saveAssetImagesIfNeeded( nodes: [ImageComponentSetRenderedNode], - format: ImageFormat, - preserveVectorData: Bool, - groupByFrame: Bool, + parameters: ImagesParameters, in assets: String? ) -> Promise<[ImageComponentSetAsset]> { assets.map { folderPath in imageAssetsProvider.saveImages( nodes: nodes, - format: format, - preserveVectorData: preserveVectorData, - groupByFrame: groupByFrame, + parameters: parameters, in: folderPath ) } ?? .value([]) @@ -170,6 +166,7 @@ final class DefaultImagesProvider: ImagesProvider { groupByFrame: Bool, format: ImageFormat, postProcessor: String?, + namingStyle: ImageNamingStyle, in resources: String? ) -> Promise<[ImageRenderedNode: ImageResource]> { resources.map { folderPath in @@ -178,6 +175,7 @@ final class DefaultImagesProvider: ImagesProvider { groupByFrame: groupByFrame, format: format, postProcessor: postProcessor, + namingStyle: namingStyle, in: folderPath ) } ?? .value([:]) @@ -190,9 +188,7 @@ final class DefaultImagesProvider: ImagesProvider { when( fulfilled: self.saveAssetImagesIfNeeded( nodes: nodes, - format: parameters.format, - preserveVectorData: parameters.preserveVectorData, - groupByFrame: parameters.groupByFrame, + parameters: parameters, in: parameters.assets ), self.saveResourceImagesIfNeeded( @@ -200,6 +196,7 @@ final class DefaultImagesProvider: ImagesProvider { groupByFrame: parameters.groupByFrame, format: parameters.format, postProcessor: parameters.postProcessor, + namingStyle: parameters.namingStyle, in: parameters.resources ) ) diff --git a/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift b/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift index ddef299..21bcd36 100644 --- a/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift +++ b/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift @@ -19,17 +19,31 @@ final class DefaultImageResourcesProvider: ImageResourcesProvider, ImagesFolderP // MARK: - Instance Methods + private func resolveFileName( + for node: ImageRenderedNode, + setNode: ImageComponentSetRenderedNode, + namingStyle: ImageNamingStyle + ) -> String { + let fileName = setNode.isSingleComponent ? node.base.name : "\(setNode.name) \(node.base.name)" + + switch namingStyle { + case .camelCase: + return fileName.camelized + + case .snakeCase: + return fileName.snakeCased + } + } + private func makeResource( for node: ImageRenderedNode, setNode: ImageComponentSetRenderedNode, groupByFrame: Bool, format: ImageFormat, + namingStyle: ImageNamingStyle, folderPath: Path ) -> ImageResource { - let fileName = setNode.isSingleComponent - ? node.base.name.camelized - : "\(setNode.name) \(node.base.name)".camelized - + let fileName = resolveFileName(for: node, setNode: setNode, namingStyle: namingStyle) let folderPath = resolveFolderPath(groupByFrame: groupByFrame, setNode: setNode, folderPath: folderPath) let fileExtension = format.fileExtension @@ -46,6 +60,7 @@ final class DefaultImageResourcesProvider: ImageResourcesProvider, ImagesFolderP for nodes: [ImageComponentSetRenderedNode], groupByFrame: Bool, format: ImageFormat, + namingStyle: ImageNamingStyle, folderPath: Path ) -> [ImageRenderedNode: ImageResource] { var resources: [ImageRenderedNode: ImageResource] = [:] @@ -57,6 +72,7 @@ final class DefaultImageResourcesProvider: ImageResourcesProvider, ImagesFolderP setNode: setNode, groupByFrame: groupByFrame, format: format, + namingStyle: namingStyle, folderPath: folderPath ) } @@ -108,6 +124,7 @@ final class DefaultImageResourcesProvider: ImageResourcesProvider, ImagesFolderP groupByFrame: Bool, format: ImageFormat, postProcessor: String?, + namingStyle: ImageNamingStyle, in folderPath: String ) -> Promise<[ImageRenderedNode: ImageResource]> { perform(on: DispatchQueue.global(qos: .userInitiated)) { @@ -115,6 +132,7 @@ final class DefaultImageResourcesProvider: ImageResourcesProvider, ImagesFolderP for: nodes, groupByFrame: groupByFrame, format: format, + namingStyle: namingStyle, folderPath: Path(folderPath) ) }.nest { resources in diff --git a/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift index da7d173..ea39d88 100644 --- a/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift +++ b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift @@ -1,4 +1,5 @@ import Foundation +import PathKit final class ImageResourcesPostProcessor { @@ -25,6 +26,9 @@ final class ImageResourcesPostProcessor { // MARK: - func execute(postProcessorPath: String, filePath: String) throws { + let postProcessorPath = Path(postProcessorPath).absolute() + let filePath = Path(filePath).absolute() + try shell("\(postProcessorPath) --filePath \(filePath)") } } diff --git a/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesProvider.swift b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesProvider.swift index 44f7f9e..6cbbd9f 100644 --- a/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesProvider.swift +++ b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesProvider.swift @@ -10,6 +10,7 @@ protocol ImageResourcesProvider { groupByFrame: Bool, format: ImageFormat, postProcessor: String?, + namingStyle: ImageNamingStyle, in folderPath: String ) -> Promise<[ImageRenderedNode: ImageResource]> } diff --git a/Sources/FigmaGenTools/Extensions/String+Extensions.swift b/Sources/FigmaGenTools/Extensions/String+Extensions.swift index 6227f9a..9fb6162 100644 --- a/Sources/FigmaGenTools/Extensions/String+Extensions.swift +++ b/Sources/FigmaGenTools/Extensions/String+Extensions.swift @@ -26,6 +26,13 @@ extension String { .joined() } + public var snakeCased: String { + components(separatedBy: CharacterSet.alphanumerics.inverted) + .filter { !$0.isEmpty } + .map { $0.lowercased() } + .joined(separator: "_") + } + // MARK: - Instance Methods public func slice(