From 8adc6020723f9ffce3c67341305e9b8e9376b963 Mon Sep 17 00:00:00 2001 From: Ilia <> Date: Wed, 15 Jun 2022 16:45:40 +0200 Subject: [PATCH] Added initial version of spacing grid export for review by RMR --- CONFIG.md | 33 ++++++ .../ui/figmaexport/Spacings.kt | 73 ++++++++++++ .../app/src/main/res/values/dimens.xml | 20 +++- .../UIComponents/Source/Spacings.swift | 30 +++++ README.md | 47 ++++++++ .../AndroidSpacingsExporter.swift | 78 +++++++++++++ .../Resources/Spacings.kt.stencil | 14 +++ .../Resources/spacings.xml.stencil | 4 + .../Endpoint/ComponentsEndpoint.swift | 7 ++ Sources/FigmaExport/Input/Params.swift | 30 +++++ .../FigmaExport/Loaders/SpacingsLoader.swift | 86 ++++++++++++++ .../Subcommands/ExportSpacings.swift | 110 ++++++++++++++++++ Sources/FigmaExport/main.swift | 1 + .../Processor/AssetsProcessor.swift | 16 +++ Sources/FigmaExportCore/Spacing.swift | 29 +++++ Sources/FigmaExportCore/TextStyle.swift | 5 +- .../Model/XcodeSpacingsOutput.swift | 17 +++ .../Resources/Spacings.swift.stencil | 6 + .../XcodeExport/XcodeSpacingsExporter.swift | 25 ++++ 19 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt create mode 100644 Examples/Example/UIComponents/Source/Spacings.swift create mode 100644 Sources/AndroidExport/AndroidSpacingsExporter.swift create mode 100644 Sources/AndroidExport/Resources/Spacings.kt.stencil create mode 100644 Sources/AndroidExport/Resources/spacings.xml.stencil create mode 100644 Sources/FigmaExport/Loaders/SpacingsLoader.swift create mode 100644 Sources/FigmaExport/Subcommands/ExportSpacings.swift create mode 100644 Sources/FigmaExportCore/Spacing.swift create mode 100644 Sources/XcodeExport/Model/XcodeSpacingsOutput.swift create mode 100644 Sources/XcodeExport/Resources/Spacings.swift.stencil create mode 100644 Sources/XcodeExport/XcodeSpacingsExporter.swift diff --git a/CONFIG.md b/CONFIG.md index d202c641..94589018 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -38,6 +38,7 @@ common: lightHCModeSuffix: '_lightHC' # [optional] If useSingleFile is true, customize the suffix to denote a dark high contrast color. Defaults to '_darkHC' darkHCModeSuffix: '_darkHC' + # [optional] icons: # [optional] Name of the Figma's frame where icons components are located @@ -50,6 +51,7 @@ common: useSingleFile: true # [optional] If useSingleFile is true, customize the suffix to denote a dark mode icons. Defaults to '_dark' darkModeSuffix: '_dark' + # [optional] images: # [optional]Name of the Figma's frame where image components are located @@ -62,12 +64,26 @@ common: useSingleFile: true # [optional] If useSingleFile is true, customize the suffix to denote a dark mode icons. Defaults to '_dark' darkModeSuffix: '_dark' + # [optional] typography: # [optional] RegExp pattern for text style name validation before exporting. If a name contains "/" symbol it will be replaced by "_" before executing the RegExp nameValidateRegexp: '^[a-zA-Z0-9_]+$' # RegExp pattern for: h1_regular, h1_medium # [optional] RegExp pattern for replacing. Supports only $n nameReplaceRegexp: 'font_$1' + + # [optional] + spacings: + # [optional] Frame name containing spacings + figmaFrameName: 'Spacings' + # [optional] Containing state name for vertical spacings + figmaVerticalStateName: 'Vertical' + # [optional] Containing state name for horizontal spacings + figmaHorizontalStateName: 'Horizontal' + # [optional] RegExp pattern for text style name validation before exporting. If a name contains "/" symbol it will be replaced by "_" before executing the RegExp + nameValidateRegexp: '^[a-zA-Z0-9_]+$' # RegExp pattern for: h1_regular, h1_medium + # [optional] RegExp pattern for replacing. Supports only $n + nameReplaceRegexp: 'font_$1' # [optional] iOS export parameters ios: @@ -157,6 +173,13 @@ ios: labelsDirectory: "./Source/UIComponents/" # Typography name style: camelCase or snake_case nameStyle: camelCase + + # [optional] Parameters for exporting spacings + spacings: + # Relative or absolute path to directory where to place generated Spacings.swift file + spacingsSwift: "./Source/UIComponents/" + # Spacing name style: camelCase or snake_case + nameStyle: camelCase # [optional] Android export parameters android: @@ -173,12 +196,14 @@ android: colors: # [optional] The package to export the Jetpack Compose color code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set composePackageName: "com.example" + # Parameters for exporting icons icons: # Where to place icons relative to `mainRes`? FigmaExport clears this directory every time your execute `figma-export icons` command output: "figma-import-icons" # [optional] The package to export the Jetpack Compose icon code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set composePackageName: "com.example" + # Parameters for exporting images images: # Image file format: svg or png @@ -193,10 +218,18 @@ android: encoding: lossy # Encoding quality in percents. Only for lossy encoding. quality: 90 + # Parameters for exporting typography typography: # Typography name style: camelCase or snake_case nameStyle: camelCase # [optional] The package to export the Jetpack Compose typography code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set composePackageName: "com.example" + + # Parameters for exporting spacings + spacings: + # Spacings name style: camelCase or snake_case + nameStyle: camelCase + # [optional] The package to export the Jetpack Compose typography code to. Note: To export Jetpack Compose code, also `mainSrc` and `resourcePackage` above must be set + composePackageName: "com.example" ``` diff --git a/Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt b/Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt new file mode 100644 index 00000000..fc4ff780 --- /dev/null +++ b/Examples/AndroidComposeExample/app/src/main/java/com/redmadrobot/androidcomposeexample/ui/figmaexport/Spacings.kt @@ -0,0 +1,73 @@ +package com.redmadrobot.androidcomposeexample.ui.figmaexport + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Dp +import com.redmadrobot.androidcomposeexample.R + +object Spacings + +@Composable +@ReadOnlyComposable +fun Spacings.h12(): Dp = dimensionResource(id = R.dimen.h12) + +@Composable +@ReadOnlyComposable +fun Spacings.h16(): Dp = dimensionResource(id = R.dimen.h16) + +@Composable +@ReadOnlyComposable +fun Spacings.h24(): Dp = dimensionResource(id = R.dimen.h24) + +@Composable +@ReadOnlyComposable +fun Spacings.h32(): Dp = dimensionResource(id = R.dimen.h32) + +@Composable +@ReadOnlyComposable +fun Spacings.h4(): Dp = dimensionResource(id = R.dimen.h4) + +@Composable +@ReadOnlyComposable +fun Spacings.h48(): Dp = dimensionResource(id = R.dimen.h48) + +@Composable +@ReadOnlyComposable +fun Spacings.h64(): Dp = dimensionResource(id = R.dimen.h64) + +@Composable +@ReadOnlyComposable +fun Spacings.h8(): Dp = dimensionResource(id = R.dimen.h8) + +@Composable +@ReadOnlyComposable +fun Spacings.v12(): Dp = dimensionResource(id = R.dimen.v12) + +@Composable +@ReadOnlyComposable +fun Spacings.v16(): Dp = dimensionResource(id = R.dimen.v16) + +@Composable +@ReadOnlyComposable +fun Spacings.v24(): Dp = dimensionResource(id = R.dimen.v24) + +@Composable +@ReadOnlyComposable +fun Spacings.v32(): Dp = dimensionResource(id = R.dimen.v32) + +@Composable +@ReadOnlyComposable +fun Spacings.v4(): Dp = dimensionResource(id = R.dimen.v4) + +@Composable +@ReadOnlyComposable +fun Spacings.v48(): Dp = dimensionResource(id = R.dimen.v48) + +@Composable +@ReadOnlyComposable +fun Spacings.v64(): Dp = dimensionResource(id = R.dimen.v64) + +@Composable +@ReadOnlyComposable +fun Spacings.v8(): Dp = dimensionResource(id = R.dimen.v8) diff --git a/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml b/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml index e00c2dd1..4d0381d7 100644 --- a/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml +++ b/Examples/AndroidComposeExample/app/src/main/res/values/dimens.xml @@ -1,5 +1,19 @@ + - - 16dp - 16dp + 12.0dp + 16.0dp + 24.0dp + 32.0dp + 4.0dp + 48.0dp + 64.0dp + 8.0dp + 12.0dp + 16.0dp + 24.0dp + 32.0dp + 4.0dp + 48.0dp + 64.0dp + 8.0dp \ No newline at end of file diff --git a/Examples/Example/UIComponents/Source/Spacings.swift b/Examples/Example/UIComponents/Source/Spacings.swift new file mode 100644 index 00000000..d21ebb29 --- /dev/null +++ b/Examples/Example/UIComponents/Source/Spacings.swift @@ -0,0 +1,30 @@ +// swiftlint:disable all +// +// The code generated using FigmaExport — Command line utility to export +// colors, typography, icons and images from Figma to Xcode project. +// +// https://github.com/RedMadRobot/figma-export +// +// Don’t edit this code manually to avoid runtime crashes +// + +import Foundation + +public struct Spacings { + static let h12: Double = 12.0 + static let h16: Double = 16.0 + static let h24: Double = 24.0 + static let h32: Double = 32.0 + static let h4: Double = 4.0 + static let h48: Double = 48.0 + static let h64: Double = 64.0 + static let h8: Double = 8.0 + static let v12: Double = 12.0 + static let v16: Double = 16.0 + static let v24: Double = 24.0 + static let v32: Double = 32.0 + static let v4: Double = 4.0 + static let v48: Double = 48.0 + static let v64: Double = 64.0 + static let v8: Double = 8.0 +} diff --git a/README.md b/README.md index d85dc3f7..d9a6c61e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Command line utility to export colors, typography, icons and images from Figma t * typography - Figma's text style * icon — Figma's component with small black/colorized vector image * image — Figma's components with colorized image (Light/Dark) +* spacings — Figma's components defining spacing between other components The utility supports Dark Mode, SwiftUI and Jetpack Compose. @@ -199,6 +200,31 @@ Example of these files: - [./Examples/Example/UIComponents/Source/LabelStyle.swift](./Examples/Example/UIComponents/Source/LabelStyle.swift) - [./Examples/Example/UIComponents/Source/UIFont+extension.swift](./Examples/Example/UIComponents/Source/UIFont+extension.swift) +#### Spacings +When your execute `figma-export spacings` command `figma-export` generates a Spacings.swift that looks like this: +```swift +import Foundation + +public struct Spacings { + static let h12: Double = 12.0 + static let h16: Double = 16.0 + static let h24: Double = 24.0 + static let h32: Double = 32.0 + static let h4: Double = 4.0 + static let h48: Double = 48.0 + static let h64: Double = 64.0 + static let h8: Double = 8.0 + static let v12: Double = 12.0 + static let v16: Double = 16.0 + static let v24: Double = 24.0 + static let v32: Double = 32.0 + static let v4: Double = 4.0 + static let v48: Double = 48.0 + static let v64: Double = 64.0 + static let v8: Double = 8.0 +} +``` + ### Android Colors will be exported to `values/colors.xml` and `values-night/colors.xml` files. @@ -258,6 +284,27 @@ object Typography { } ``` +Spacings will be exported to `values/dimens.xml`. For Jetpack Compose, following code will be generated, if configured: +```kotlin +package com.redmadrobot.androidcomposeexample.ui.figmaexport + +import ... + +object Spacings + +@Composable +@ReadOnlyComposable +fun Spacings.h12(): Dp = dimensionResource(id = R.dimen.h12) + +@Composable +@ReadOnlyComposable +fun Spacings.h16(): Dp = dimensionResource(id = R.dimen.h16) + +@Composable +@ReadOnlyComposable +fun Spacings.h24(): Dp = dimensionResource(id = R.dimen.h24) +``` + ## Installation Before installation you must provide Figma personal access token via environment variables. diff --git a/Sources/AndroidExport/AndroidSpacingsExporter.swift b/Sources/AndroidExport/AndroidSpacingsExporter.swift new file mode 100644 index 00000000..068afa9a --- /dev/null +++ b/Sources/AndroidExport/AndroidSpacingsExporter.swift @@ -0,0 +1,78 @@ +import Foundation +import FigmaExportCore + +final public class AndroidSpacingsExporter: AndroidExporter { + + private let output: AndroidOutput + + public init(output: AndroidOutput) { + self.output = output + super.init(templatesPath: output.templatesPath) + } + + public func exportSpacings(spacings: [Spacing]) throws -> [FileContents] { + var files: [FileContents] = [] + + // typography.xml + files.append(try makeSpacingsXMLFileContents(spacings: spacings)) + + // Typography.kt + if + let composeOutputDirectory = output.composeOutputDirectory, + let packageName = output.packageName, + let xmlResourcePackage = output.xmlResourcePackage { + + files.append( + try makeSpacingsComposeFileContents( + spacings: spacings, + outputDirectory: composeOutputDirectory, + package: packageName, + xmlResourcePackage: xmlResourcePackage + ) + ) + } + + return files + } + + private func makeSpacingsXMLFileContents(spacings: [Spacing]) throws -> FileContents { + let env = makeEnvironment() + let contents = try env.renderTemplate(name: "spacings.xml.stencil", context: [ + "spacings": spacings.map { spacing in + [ + "name": spacing.name, + "size": spacing.size + ] + } + ]) + + let directoryURL = output.xmlOutputDirectory.appendingPathComponent("values") + let fileURL = URL(string: "dimens.xml")! + return try makeFileContents(for: contents, directory: directoryURL, file: fileURL) + } + + private func makeSpacingsComposeFileContents( + spacings: [Spacing], + outputDirectory: URL, + package: String, + xmlResourcePackage: String + ) throws -> FileContents { + let spacings: [[String: Any]] = spacings.map { spacing in + [ + "functionName": spacing.name.lowerCamelCased(), + "name": spacing.name + ] + } + let context: [String: Any] = [ + "spacings": spacings, + "package": package, + "xmlResourcePackage": xmlResourcePackage + ] + let env = makeEnvironment() + let contents = try env.renderTemplate(name: "Spacings.kt.stencil", context: context) + + let fileURL = URL(string: "Spacings.kt")! + return try makeFileContents(for: contents, directory: outputDirectory, file: fileURL) + } +} + diff --git a/Sources/AndroidExport/Resources/Spacings.kt.stencil b/Sources/AndroidExport/Resources/Spacings.kt.stencil new file mode 100644 index 00000000..4a4d9af9 --- /dev/null +++ b/Sources/AndroidExport/Resources/Spacings.kt.stencil @@ -0,0 +1,14 @@ +package {{package}} + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Dp +import {{xmlResourcePackage}}.R + +object Spacings +{% for spacing in spacings %} +@Composable +@ReadOnlyComposable +fun Spacings.{{ spacing.functionName }}(): Dp = dimensionResource(id = R.dimen.{{ spacing.name }}) +{% endfor %} \ No newline at end of file diff --git a/Sources/AndroidExport/Resources/spacings.xml.stencil b/Sources/AndroidExport/Resources/spacings.xml.stencil new file mode 100644 index 00000000..3d69d4d8 --- /dev/null +++ b/Sources/AndroidExport/Resources/spacings.xml.stencil @@ -0,0 +1,4 @@ + +{% for spacing in spacings %} + {{ spacing.size }}dp{% endfor %} + \ No newline at end of file diff --git a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift index 35fdd41e..a375480b 100644 --- a/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift +++ b/Sources/FigmaAPI/Endpoint/ComponentsEndpoint.swift @@ -46,4 +46,11 @@ public struct ContainingFrame: Codable { public let nodeID: String? public let name: String? public let pageName: String + public let containingStateGroup: ContainingStateGroup? +} + +// MARK: - ContainingStateGroup +public struct ContainingStateGroup: Codable { + public let nodeID: String? + public let name: String? } diff --git a/Sources/FigmaExport/Input/Params.swift b/Sources/FigmaExport/Input/Params.swift index 4f8db0a5..e4e27881 100644 --- a/Sources/FigmaExport/Input/Params.swift +++ b/Sources/FigmaExport/Input/Params.swift @@ -14,6 +14,7 @@ struct Params: Decodable { } struct Common: Decodable { + struct Colors: Decodable { let nameValidateRegexp: String? let nameReplaceRegexp: String? @@ -44,10 +45,19 @@ struct Params: Decodable { let nameReplaceRegexp: String? } + struct Spacings: Decodable { + let figmaFrameName: String? + let figmaVerticalStateName: String? + let figmaHorizontalStateName: String? + let nameValidateRegexp: String? + let nameReplaceRegexp: String? + } + let colors: Colors? let icons: Icons? let images: Images? let typography: Typography? + let spacings: Spacings? } enum VectorFormat: String, Decodable { @@ -100,6 +110,11 @@ struct Params: Decodable { let nameStyle: NameStyle } + struct Spacings: Decodable { + let spacingsSwift: URL? + let nameStyle: NameStyle + } + let xcodeprojPath: String let target: String let xcassetsPath: URL @@ -112,22 +127,28 @@ struct Params: Decodable { let icons: Icons? let images: Images? let typography: Typography? + let spacings: Spacings? } struct Android: Decodable { + struct Icons: Decodable { let output: String let composePackageName: String? } + struct Colors: Decodable { let composePackageName: String? } + struct Images: Decodable { + enum Format: String, Decodable { case svg case png case webp } + struct FormatOptions: Decodable { enum Encoding: String, Decodable { case lossy @@ -136,15 +157,23 @@ struct Params: Decodable { let encoding: Encoding let quality: Int? } + let scales: [Double]? let output: String let format: Format let webpOptions: FormatOptions? } + struct Typography: Decodable { let nameStyle: NameStyle let composePackageName: String? } + + struct Spacings: Decodable { + let nameStyle: NameStyle + let composePackageName: String? + } + let mainRes: URL let resourcePackage: String? let mainSrc: URL? @@ -152,6 +181,7 @@ struct Params: Decodable { let icons: Icons? let images: Images? let typography: Typography? + let spacings: Spacings? let templatesPath: URL? } diff --git a/Sources/FigmaExport/Loaders/SpacingsLoader.swift b/Sources/FigmaExport/Loaders/SpacingsLoader.swift new file mode 100644 index 00000000..1097ab4d --- /dev/null +++ b/Sources/FigmaExport/Loaders/SpacingsLoader.swift @@ -0,0 +1,86 @@ +import Foundation +import FigmaAPI +import FigmaExportCore + +/// Loads spacings from Figma +final class SpacingsLoader { + + private let client: Client + private let params: Params + private let platform: Platform + + private var spacingsFrameName: String { + params.common?.spacings?.figmaFrameName ?? "Spacing" + } + + private var spacingsVerticalFrameName: String { + params.common?.spacings?.figmaVerticalStateName ?? "Vertical" + } + + private var spacingsHorizontalFrameName: String { + params.common?.spacings?.figmaHorizontalStateName ?? "Horizontal" + } + + init(client: Client, params: Params, platform: Platform) { + self.client = client + self.params = params + self.platform = platform + } + + func load() throws -> [Spacing] { + return try loadSpacings(fileId: params.figma.lightFileId) + } + + private func loadSpacings(fileId: String) throws -> [Spacing] { + let formatter = NumberFormatter() + + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 1 + + return try fetchSpacingsComponents(fileId: fileId).compactMap { component in + guard let size = extractSizeFrom(componentName: component.name), + let formattedSize = formatter.string(from: NSNumber(value: size)), + let containingStateGroup = component.containingFrame.containingStateGroup?.name, + let prefix = calculatePrefixFrom(stateGroup: containingStateGroup) else { return nil } + + return Spacing(name: "\(prefix)_\(formattedSize)", size: size) + } + } + + // MARK: - Helpers + + private func fetchSpacingsComponents(fileId: String) throws -> [Component] { + return try loadComponents(fileId: fileId) + .filter { + $0.containingFrame.name == spacingsFrameName && $0.useForPlatform(platform) + } + } + + // MARK: - Figma + private func loadComponents(fileId: String) throws -> [Component] { + let endpoint = ComponentsEndpoint(fileId: fileId) + return try client.request(endpoint) + } + + private func calculatePrefixFrom(stateGroup: String) -> String? { + switch (stateGroup) { + case spacingsVerticalFrameName: return "v" + case spacingsHorizontalFrameName: return "h" + default: return nil + } + } + + private func extractSizeFrom(componentName: String) -> Double? { + let regex = try? NSRegularExpression(pattern: #"[Ss]ize=(?\d+)"#) + let matches = regex?.matches( + in: componentName, + options: [], + range: NSRange(componentName.startIndex.. XcodeSpacingsOutput { + return XcodeSpacingsOutput( + spacingsUrl: iosParams.spacings?.spacingsSwift, + addObjcAttribute: iosParams.addObjcAttribute, + templatesPath: iosParams.templatesPath + ) + } + + private func exportXcodeSpacings(spacings: [Spacing], iosParams: Params.iOS) throws { + let output = createXcodeOutput(from: iosParams) + let exporter = XcodeSpacingsExporter(output: output) + let files = try exporter.export(spacings: spacings) + + try fileWriter.write(files: files) + + guard iosParams.xcassetsInSwiftPackage == false else { return } + + do { + let xcodeProject = try XcodeProjectWriter(xcodeProjPath: iosParams.xcodeprojPath, target: iosParams.target) + try files.forEach { file in + if file.destination.file.pathExtension == "swift" { + try xcodeProject.addFileReferenceToXcodeProj(file.destination.url) + } + } + try xcodeProject.save() + } catch { + logger.error("Unable to add some file references to Xcode project") + } + } + + private func exportAndroidSpacings(spacings: [Spacing], androidParams: Params.Android) throws { + let output = AndroidOutput( + xmlOutputDirectory: androidParams.mainRes, + xmlResourcePackage: androidParams.resourcePackage, + srcDirectory: androidParams.mainSrc, + packageName: androidParams.typography?.composePackageName, + templatesPath: androidParams.templatesPath + ) + let exporter = AndroidSpacingsExporter(output: output) + let files = try exporter.exportSpacings(spacings: spacings) + + let fileURL = androidParams.mainRes.appendingPathComponent("values/dimens.xml") + + try? FileManager.default.removeItem(atPath: fileURL.path) + try fileWriter.write(files: files) + } + } +} diff --git a/Sources/FigmaExport/main.swift b/Sources/FigmaExport/main.swift index 793693fe..c6125624 100644 --- a/Sources/FigmaExport/main.swift +++ b/Sources/FigmaExport/main.swift @@ -48,6 +48,7 @@ struct FigmaExportCommand: ParsableCommand { ExportIcons.self, ExportImages.self, ExportTypography.self, + ExportSpacings.self, GenerateConfigFile.self ], defaultSubcommand: ExportColors.self diff --git a/Sources/FigmaExportCore/Processor/AssetsProcessor.swift b/Sources/FigmaExportCore/Processor/AssetsProcessor.swift index cc11cd51..0f92cb87 100644 --- a/Sources/FigmaExportCore/Processor/AssetsProcessor.swift +++ b/Sources/FigmaExportCore/Processor/AssetsProcessor.swift @@ -72,6 +72,22 @@ public struct TypographyProcessor: AssetsProcessable { } } +public struct SpacingsProcessor: AssetsProcessable { + public typealias AssetType = Spacing + + public let platform: Platform + public let nameValidateRegexp: String? + public let nameReplaceRegexp: String? + public let nameStyle: NameStyle? + + public init(platform: Platform, nameValidateRegexp: String?, nameReplaceRegexp: String?, nameStyle: NameStyle?) { + self.platform = platform + self.nameValidateRegexp = nameValidateRegexp + self.nameReplaceRegexp = nameReplaceRegexp + self.nameStyle = nameStyle + } +} + public struct ImagesProcessor: AssetsProcessable { public typealias AssetType = ImagePack diff --git a/Sources/FigmaExportCore/Spacing.swift b/Sources/FigmaExportCore/Spacing.swift new file mode 100644 index 00000000..e32b3d9a --- /dev/null +++ b/Sources/FigmaExportCore/Spacing.swift @@ -0,0 +1,29 @@ +import Foundation + +public struct Spacing: Asset { + + public var name: String + public var platform: Platform? + public let size: Double + + public init( + name: String, + platform: Platform? = nil, + size: Double + ) { + + self.name = name + self.platform = platform + self.size = size + } + + // MARK: Hashable + + public static func == (lhs: Spacing, rhs: Spacing) -> Bool { + return lhs.name == rhs.name + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + } +} diff --git a/Sources/FigmaExportCore/TextStyle.swift b/Sources/FigmaExportCore/TextStyle.swift index 9081a776..c75b30f0 100644 --- a/Sources/FigmaExportCore/TextStyle.swift +++ b/Sources/FigmaExportCore/TextStyle.swift @@ -64,9 +64,10 @@ public struct TextStyle: Asset { fontStyle: DynamicTypeStyle?, lineHeight: Double? = nil, letterSpacing: Double, - textCase: TextCase = .original) { - + textCase: TextCase = .original + ) { self.name = name + self.platform = platform self.fontName = fontName self.fontSize = fontSize self.fontStyle = fontStyle diff --git a/Sources/XcodeExport/Model/XcodeSpacingsOutput.swift b/Sources/XcodeExport/Model/XcodeSpacingsOutput.swift new file mode 100644 index 00000000..0c4c3c89 --- /dev/null +++ b/Sources/XcodeExport/Model/XcodeSpacingsOutput.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct XcodeSpacingsOutput { + let spacingsUrl: URL? + let addObjcAttribute: Bool + let templatesPath: URL? + + public init( + spacingsUrl: URL?, + addObjcAttribute: Bool? = false, + templatesPath: URL? = nil + ) { + self.spacingsUrl = spacingsUrl + self.addObjcAttribute = addObjcAttribute ?? false + self.templatesPath = templatesPath + } +} diff --git a/Sources/XcodeExport/Resources/Spacings.swift.stencil b/Sources/XcodeExport/Resources/Spacings.swift.stencil new file mode 100644 index 00000000..74def582 --- /dev/null +++ b/Sources/XcodeExport/Resources/Spacings.swift.stencil @@ -0,0 +1,6 @@ +{% include "header.stencil" %} +import Foundation + +public struct Spacings {{ "{" }}{% for spacing in spacings %} + {% if addObjcPrefix %}@objc {% endif %}static let {{ spacing.name }}: Double = {{ spacing.size }}{% endfor %} +} diff --git a/Sources/XcodeExport/XcodeSpacingsExporter.swift b/Sources/XcodeExport/XcodeSpacingsExporter.swift new file mode 100644 index 00000000..5b0310c0 --- /dev/null +++ b/Sources/XcodeExport/XcodeSpacingsExporter.swift @@ -0,0 +1,25 @@ +import Foundation +import FigmaExportCore +import Stencil + +final public class XcodeSpacingsExporter: XcodeExporterBase { + private let output: XcodeSpacingsOutput + + public init(output: XcodeSpacingsOutput) { + self.output = output + } + + public func export(spacings: [Spacing]) throws -> [FileContents] { + guard let spacingsUrl = output.spacingsUrl else { return [] } + return [try makeSpacingsStruct(spacings: spacings, spacingsUrl: spacingsUrl)] + } + + private func makeSpacingsStruct(spacings: [Spacing], spacingsUrl: URL) throws -> FileContents { + let env = makeEnvironment(templatesPath: output.templatesPath) + let contents = try env.renderTemplate(name: "Spacings.swift.stencil", context: [ + "spacings": spacings.map { spacing in [ "name": spacing.name.lowerCamelCased(), "size": spacing.size ] }, + "addObjcPrefix": output.addObjcAttribute + ]) + return try makeFileContents(for: contents, url: spacingsUrl) + } +}