Skip to content

Commit 702976b

Browse files
authored
Merge pull request #15 from RedMadRobot/feature/export_filter
Add support for exporting selected resources
2 parents f28eb3b + b7f4454 commit 702976b

File tree

11 files changed

+231
-92
lines changed

11 files changed

+231
-92
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Scheme
3+
LastUpgradeVersion = "1160"
4+
version = "1.3">
5+
<BuildAction
6+
parallelizeBuildables = "YES"
7+
buildImplicitDependencies = "YES">
8+
<BuildActionEntries>
9+
<BuildActionEntry
10+
buildForTesting = "YES"
11+
buildForRunning = "YES"
12+
buildForProfiling = "YES"
13+
buildForArchiving = "YES"
14+
buildForAnalyzing = "YES">
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "0F20186424D1B4AF002068B4"
18+
BuildableName = "Example.app"
19+
BlueprintName = "Example"
20+
ReferencedContainer = "container:Example.xcodeproj">
21+
</BuildableReference>
22+
</BuildActionEntry>
23+
</BuildActionEntries>
24+
</BuildAction>
25+
<TestAction
26+
buildConfiguration = "Debug"
27+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
28+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
shouldUseLaunchSchemeArgsEnv = "YES">
30+
<Testables>
31+
</Testables>
32+
</TestAction>
33+
<LaunchAction
34+
buildConfiguration = "Debug"
35+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
36+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
37+
launchStyle = "0"
38+
useCustomWorkingDirectory = "NO"
39+
ignoresPersistentStateOnLaunch = "NO"
40+
debugDocumentVersioning = "YES"
41+
debugServiceExtension = "internal"
42+
allowLocationSimulation = "YES">
43+
<BuildableProductRunnable
44+
runnableDebuggingMode = "0">
45+
<BuildableReference
46+
BuildableIdentifier = "primary"
47+
BlueprintIdentifier = "0F20186424D1B4AF002068B4"
48+
BuildableName = "Example.app"
49+
BlueprintName = "Example"
50+
ReferencedContainer = "container:Example.xcodeproj">
51+
</BuildableReference>
52+
</BuildableProductRunnable>
53+
</LaunchAction>
54+
<ProfileAction
55+
buildConfiguration = "Release"
56+
shouldUseLaunchSchemeArgsEnv = "YES"
57+
savedToolIdentifier = ""
58+
useCustomWorkingDirectory = "NO"
59+
debugDocumentVersioning = "YES">
60+
<BuildableProductRunnable
61+
runnableDebuggingMode = "0">
62+
<BuildableReference
63+
BuildableIdentifier = "primary"
64+
BlueprintIdentifier = "0F20186424D1B4AF002068B4"
65+
BuildableName = "Example.app"
66+
BlueprintName = "Example"
67+
ReferencedContainer = "container:Example.xcodeproj">
68+
</BuildableReference>
69+
</BuildableProductRunnable>
70+
</ProfileAction>
71+
<AnalyzeAction
72+
buildConfiguration = "Debug">
73+
</AnalyzeAction>
74+
<ArchiveAction
75+
buildConfiguration = "Release"
76+
revealArchiveInOrganizer = "YES">
77+
</ArchiveAction>
78+
</Scheme>

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ let package = Package(
5151
),
5252

5353
.testTarget(
54-
name: "figma-exportTests",
55-
dependencies: ["FigmaExport"]
54+
name: "FigmaExportCoreTests",
55+
dependencies: ["FigmaExportCore"]
5656
),
5757
.testTarget(
5858
name: "XcodeExportTests",

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,21 @@ Run `fastlane sync_colors` to run FigmaExport.
147147
`./figma-export images -i figma-export.yaml`
148148

149149

150-
## Parameters
150+
## Arguments
151+
152+
**Export specific icons/images**
153+
154+
If you want to export specific icons/images you can list their names in the last argument like this:
155+
156+
`./figma-export icons "ic/24/edit"` — Exports only one icon.
157+
158+
`./figma-export icons "ic/24/edit, ic/16/notification"` — Exports two icons
159+
160+
`./figma-export icons "ic/24/videoplayer/*"` — Exports all icons which names starts with `ic/24/videoplayer/`
161+
162+
`./figma-export icons` — Exports all the icons.
163+
164+
**Configuration file**
151165

152166
Argument `-i` or `-input` specifies path to `figma-export.yaml` file where all the properties stores: figma, ios, android.
153167

Sources/FigmaExport/Loaders/ImagesLoader.swift

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,54 @@ final class ImagesLoader {
1414
self.platform = platform
1515
}
1616

17-
func loadIcons() throws -> [ImagePack] {
17+
func loadIcons(filter: String? = nil) throws -> [ImagePack] {
1818
if platform == .android {
19-
return try _loadImages(fileId: params.lightFileId, frameName: .icons, params: SVGParams()).map { ImagePack.singleScale($0) }
19+
return try _loadImages(
20+
fileId: params.lightFileId,
21+
frameName: .icons,
22+
params: SVGParams(),
23+
filter: filter
24+
).map { ImagePack.singleScale($0) }
2025
} else {
21-
return try _loadImages(fileId: params.lightFileId, frameName: .icons, params: PDFParams()).map { ImagePack.singleScale($0) }
26+
return try _loadImages(
27+
fileId: params.lightFileId,
28+
frameName: .icons,
29+
params: PDFParams(),
30+
filter: filter
31+
).map { ImagePack.singleScale($0) }
2232
}
2333
}
2434

25-
func loadImages() throws -> (light: [ImagePack], dark: [ImagePack]?) {
35+
func loadImages(filter: String? = nil) throws -> (light: [ImagePack], dark: [ImagePack]?) {
2636
if platform == .android {
27-
let light = try _loadImages(fileId: params.lightFileId, frameName: .illustrations, params: SVGParams())
28-
let dark = try params.darkFileId.map { try _loadImages(fileId: $0, frameName: .illustrations, params: SVGParams()) }
37+
let light = try _loadImages(
38+
fileId: params.lightFileId,
39+
frameName: .illustrations,
40+
params: SVGParams(),
41+
filter: filter)
42+
43+
let dark = try params.darkFileId.map {
44+
try _loadImages(
45+
fileId: $0,
46+
frameName: .illustrations,
47+
params: SVGParams(),
48+
filter: filter)
49+
}
2950
return (
3051
light.map { ImagePack.singleScale($0) },
3152
dark?.map { ImagePack.singleScale($0) }
3253
)
3354
} else {
34-
let lightImages = try _loadPNGImages(fileId: params.lightFileId, frameName: .illustrations)
35-
let darkImages = try params.darkFileId.map { try _loadPNGImages(fileId: $0, frameName: .illustrations) }
55+
let lightImages = try _loadPNGImages(
56+
fileId: params.lightFileId,
57+
frameName: .illustrations,
58+
filter: filter)
59+
let darkImages = try params.darkFileId.map {
60+
try _loadPNGImages(
61+
fileId: $0,
62+
frameName: .illustrations,
63+
filter: filter)
64+
}
3665
return (
3766
lightImages,
3867
darkImages
@@ -42,33 +71,41 @@ final class ImagesLoader {
4271

4372
// MARK: - Helpers
4473

45-
private func fetchImageComponents(fileId: String, frameName: FrameName) throws -> [NodeId: Component] {
46-
let components = try loadComponents(fileId: fileId)
74+
private func fetchImageComponents(fileId: String, frameName: FrameName, filter: String? = nil) throws -> [NodeId: Component] {
75+
var components = try loadComponents(fileId: fileId)
4776
.filter {
4877
$0.containingFrame.name == frameName.rawValue &&
4978
($0.description == platform.rawValue ||
5079
$0.description == nil || $0.description == "")
5180
}
81+
82+
if let filter = filter {
83+
let assetsFilter = AssetsFilter(filter: filter)
84+
components = components.filter { component -> Bool in
85+
assetsFilter.match(name: component.name)
86+
}
87+
}
88+
5289
return Dictionary(uniqueKeysWithValues: components.map { ($0.nodeId, $0) })
5390
}
5491

55-
private func _loadImages(fileId: String, frameName: FrameName, params: FormatParams) throws -> [Image] {
56-
let imagesDict = try fetchImageComponents(fileId: fileId, frameName: frameName)
92+
private func _loadImages(fileId: String, frameName: FrameName, params: FormatParams, filter: String? = nil) throws -> [Image] {
93+
let imagesDict = try fetchImageComponents(fileId: fileId, frameName: frameName, filter: filter)
5794
let imagesIds: [NodeId] = imagesDict.keys.map { $0 }
58-
5995
let imageIdToImagePath = try loadImages(fileId: fileId, nodeIds: imagesIds, params: params)
60-
96+
6197
return imageIdToImagePath.map { (imageId, imagePath) -> Image in
62-
Image(
63-
name: imagesDict[imageId]!.name,
98+
let name = imagesDict[imageId]!.name
99+
return Image(
100+
name: name,
64101
url: URL(string: imagePath)!,
65102
format: params.format
66103
)
67104
}
68105
}
69106

70-
private func _loadPNGImages(fileId: String, frameName: FrameName) throws -> [ImagePack] {
71-
let imagesDict = try fetchImageComponents(fileId: fileId, frameName: frameName)
107+
private func _loadPNGImages(fileId: String, frameName: FrameName, filter: String? = nil) throws -> [ImagePack] {
108+
let imagesDict = try fetchImageComponents(fileId: fileId, frameName: frameName, filter: filter)
72109
let imagesIds: [NodeId] = imagesDict.keys.map { $0 }
73110

74111
let imageIdToImagePath1 = try loadImages(fileId: fileId, nodeIds: imagesIds, params: PNGParams(scale: 1))

Sources/FigmaExport/Subcommands/ExportIcons.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ extension FigmaExportCommand {
1818
@Option(name: .shortAndLong, default: "figma-export.yaml", help: "An input YAML file with figma and platform properties.")
1919
var input: String
2020

21+
@Argument(help: "[Optional] Name of the icons to export. For example \"ic/24/edit\" to export single icon, \"ic/24/edit, ic/16/notification\" to export several icons and \"ic/16/*\" to export all icons of size 16 pt")
22+
var filter: String?
23+
2124
func run() throws {
2225
let logger = Logger(label: "com.redmadrobot.figma-export")
2326

@@ -45,7 +48,7 @@ extension FigmaExportCommand {
4548

4649
logger.info("Fetching icons info from Figma. Please wait...")
4750
let loader = ImagesLoader(figmaClient: client, params: params.figma, platform: .ios)
48-
let images = try loader.loadIcons()
51+
let images = try loader.loadIcons(filter: filter)
4952

5053
logger.info("Processing icons...")
5154
let processor = ImagesProcessor(
@@ -62,7 +65,9 @@ extension FigmaExportCommand {
6265

6366
let exporter = XcodeIconsExporter(output: output)
6467
let localAndRemoteFiles = exporter.export(assets: icons.map { $0.single })
65-
try? FileManager.default.removeItem(atPath: assetsURL.path)
68+
if filter == nil {
69+
try? FileManager.default.removeItem(atPath: assetsURL.path)
70+
}
6671

6772
logger.info("Downloading remote files...")
6873
let localFiles = try fileDownloader.fetch(files: localAndRemoteFiles)
@@ -79,7 +84,7 @@ extension FigmaExportCommand {
7984
// 1. Get Icons info
8085
logger.info("Fetching icons info from Figma. Please wait...")
8186
let loader = ImagesLoader(figmaClient: client, params: params.figma, platform: .android)
82-
let images = try loader.loadIcons()
87+
let images = try loader.loadIcons(filter: filter)
8388

8489
// 2. Proccess images
8590
logger.info("Processing icons...")

Sources/FigmaExport/Subcommands/ExportImages.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ extension FigmaExportCommand {
1717

1818
@Option(name: .shortAndLong, default: "figma-export.yaml", help: "An input YAML file with figma and platform properties.")
1919
var input: String
20-
20+
21+
@Argument(help: "[Optional] Name of the images to export. For example \"img/login\" to export single image, \"img/onboarding/1, img/onboarding/2\" to export several images and \"img/onboarding/*\" to export all images from onboarding group")
22+
var filter: String?
23+
2124
func run() throws {
2225
let logger = Logger(label: "com.redmadrobot.figma-export")
2326

@@ -45,7 +48,7 @@ extension FigmaExportCommand {
4548

4649
logger.info("Fetching images info from Figma. Please wait...")
4750
let loader = ImagesLoader(figmaClient: client, params: params.figma, platform: .ios)
48-
let imagesTuple = try loader.loadImages()
51+
let imagesTuple = try loader.loadImages(filter: filter)
4952

5053
logger.info("Processing images...")
5154
let processor = ImagesProcessor(
@@ -59,8 +62,9 @@ extension FigmaExportCommand {
5962
let output = XcodeImagesOutput(assetsFolderURL: assetsURL)
6063
let exporter = XcodeImagesExporter(output: output)
6164
let localAndRemoteFiles = exporter.export(assets: images)
62-
try? FileManager.default.removeItem(atPath: assetsURL.path)
63-
65+
if filter == nil {
66+
try? FileManager.default.removeItem(atPath: assetsURL.path)
67+
}
6468
logger.info("Downloading remote files...")
6569
let localFiles = try fileDownloader.fetch(files: localAndRemoteFiles)
6670

@@ -76,7 +80,7 @@ extension FigmaExportCommand {
7680
// 1. Get Images info
7781
logger.info("Fetching images info from Figma. Please wait...")
7882
let loader = ImagesLoader(figmaClient: client, params: params.figma, platform: .android)
79-
let imagesTuple = try loader.loadImages()
83+
let imagesTuple = try loader.loadImages(filter: filter)
8084

8185
// 2. Proccess images
8286
logger.info("Processing images...")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Foundation
2+
3+
public struct AssetsFilter {
4+
5+
private let filters: [String]
6+
7+
public init(filter: String) {
8+
filters = filter
9+
.split(separator: ",")
10+
.map { $0.trimmingCharacters(in: .whitespaces) }
11+
}
12+
13+
/// Returns true if name matches with filter
14+
/// - Parameters:
15+
/// - name: Name of the asset
16+
/// - filter: Name of the assets separated by comma
17+
public func match(name: String) -> Bool {
18+
return filters.contains(where: { filter -> Bool in
19+
if filter.contains("*") {
20+
return wildcard(name, pattern: filter)
21+
} else {
22+
return name == filter
23+
}
24+
})
25+
}
26+
27+
private func wildcard(_ string: String, pattern: String) -> Bool {
28+
let pred = NSPredicate(format: "self LIKE %@", pattern)
29+
return !NSArray(object: string).filtered(using: pred).isEmpty
30+
}
31+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import XCTest
2+
import Foundation
3+
@testable import FigmaExportCore
4+
5+
final class AssetsFilterTests: XCTestCase {
6+
7+
func testSingleMatch() {
8+
let filter = AssetsFilter(filter: "ic/24/edit")
9+
10+
XCTAssert(filter.match(name: "ic/24/edit"))
11+
XCTAssertFalse(filter.match(name: "ic/24/call"))
12+
}
13+
14+
func testMultipleMatch() {
15+
let filter = AssetsFilter(filter: "ic/24/edit, ic/16/notification")
16+
17+
XCTAssert(filter.match(name: "ic/24/edit"))
18+
XCTAssert(filter.match(name: "ic/16/notification"))
19+
20+
XCTAssertFalse(filter.match(name: "ic/24/call"))
21+
XCTAssertFalse(filter.match(name: "ic/16/edit"))
22+
}
23+
24+
func testMatchWithAsterisk() {
25+
let filter = AssetsFilter(filter: "ic/24/*")
26+
27+
XCTAssert(filter.match(name: "ic/24/call"))
28+
XCTAssert(filter.match(name: "ic/24/test/my"))
29+
30+
XCTAssertFalse(filter.match(name: "ic/16/call"))
31+
XCTAssertFalse(filter.match(name: "img/24/edit"))
32+
}
33+
}

Tests/LinuxMain.swift

Lines changed: 0 additions & 7 deletions
This file was deleted.

Tests/figma-exportTests/XCTestManifests.swift

Lines changed: 0 additions & 9 deletions
This file was deleted.

0 commit comments

Comments
 (0)