Skip to content

Commit 5feec29

Browse files
alexey1312claude
andcommitted
refactor(colors): decompose ExportColors.swift into platform files
Split large ExportColors.swift (989 lines) into focused extensions: - iOSColorsExport.swift: iOS/Xcode colors export - AndroidColorsExport.swift: Android colors + theme attributes - FlutterColorsExport.swift: Flutter/Dart colors export - WebColorsExport.swift: CSS/TS/JSON colors export Each file contains both multiple entries and legacy format handlers. Main ExportColors.swift now contains command definition and compact orchestration logic with shared helpers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent ede9c1c commit 5feec29

5 files changed

Lines changed: 809 additions & 862 deletions

File tree

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import AndroidExport
2+
import ExFigCore
3+
import FigmaAPI
4+
import Foundation
5+
6+
// MARK: - Android Colors Export
7+
8+
extension ExFigCommand.ExportColors {
9+
/// Exports Android colors using multiple entries format.
10+
func exportAndroidColorsMultiple(
11+
entries: [Params.Android.ColorsEntry],
12+
android: Params.Android,
13+
client: Client,
14+
ui: TerminalUI
15+
) async throws -> Int {
16+
var totalCount = 0
17+
18+
for entry in entries {
19+
let colors = try await ui.withSpinner(
20+
"Fetching colors from Figma (\(entry.tokensCollectionName))..."
21+
) {
22+
let loader = ColorsVariablesLoader(
23+
client: client,
24+
figmaParams: options.params.figma,
25+
variableParams: Params.Common.VariablesColors(
26+
tokensFileId: entry.tokensFileId,
27+
tokensCollectionName: entry.tokensCollectionName,
28+
lightModeName: entry.lightModeName,
29+
darkModeName: entry.darkModeName,
30+
lightHCModeName: entry.lightHCModeName,
31+
darkHCModeName: entry.darkHCModeName,
32+
primitivesModeName: entry.primitivesModeName,
33+
nameValidateRegexp: entry.nameValidateRegexp,
34+
nameReplaceRegexp: entry.nameReplaceRegexp
35+
),
36+
filter: filter
37+
)
38+
return try await loader.load()
39+
}
40+
41+
let colorPairs = try await ui.withSpinner("Processing colors for Android...") {
42+
let processor = ColorsProcessor(
43+
platform: .android,
44+
nameValidateRegexp: entry.nameValidateRegexp,
45+
nameReplaceRegexp: entry.nameReplaceRegexp,
46+
nameStyle: .snakeCase
47+
)
48+
let result = processor.process(light: colors.light, dark: colors.dark)
49+
if let warning = result.warning {
50+
ui.warning(warning)
51+
}
52+
return try result.get()
53+
}
54+
55+
try await ui.withSpinner("Exporting colors to Android Studio project...") {
56+
try await exportAndroidColorsEntry(colorPairs: colorPairs, entry: entry, android: android, ui: ui)
57+
}
58+
59+
totalCount += colorPairs.count
60+
}
61+
62+
if BatchProgressViewStorage.progressView == nil {
63+
await checkForUpdate(logger: ExFigCommand.logger)
64+
}
65+
66+
ui.success("Done! Exported \(totalCount) colors to Android project.")
67+
return totalCount
68+
}
69+
70+
/// Exports Android colors using legacy format (common.variablesColors or common.colors).
71+
func exportAndroidColorsLegacy(
72+
colorsConfig: Params.Android.ColorsConfiguration,
73+
android: Params.Android,
74+
config: LegacyExportConfig
75+
) async throws -> Int {
76+
try validateLegacyConfig(config.commonParams)
77+
78+
let colors = try await loadLegacyColors(config: config)
79+
80+
let (finalNameValidateRegexp, finalNameReplaceRegexp) = extractNameRegexps(
81+
from: config.commonParams
82+
)
83+
84+
let entry = colorsConfig.entries[0]
85+
86+
let colorPairs = try await config.ui.withSpinner("Processing colors for Android...") {
87+
let processor = ColorsProcessor(
88+
platform: .android,
89+
nameValidateRegexp: finalNameValidateRegexp,
90+
nameReplaceRegexp: finalNameReplaceRegexp,
91+
nameStyle: .snakeCase
92+
)
93+
let result = processor.process(light: colors.light, dark: colors.dark)
94+
if let warning = result.warning {
95+
config.ui.warning(warning)
96+
}
97+
return try result.get()
98+
}
99+
100+
try await config.ui.withSpinner("Exporting colors to Android Studio project...") {
101+
try await exportAndroidColorsEntry(
102+
colorPairs: colorPairs, entry: entry, android: android, ui: config.ui
103+
)
104+
}
105+
106+
if BatchProgressViewStorage.progressView == nil {
107+
await checkForUpdate(logger: ExFigCommand.logger)
108+
}
109+
110+
config.ui.success("Done! Exported \(colorPairs.count) colors to Android project.")
111+
return colorPairs.count
112+
}
113+
114+
// MARK: - Android Entry Export
115+
116+
func exportAndroidColorsEntry(
117+
colorPairs: [AssetPair<Color>],
118+
entry: Params.Android.ColorsEntry,
119+
android: Params.Android,
120+
ui: TerminalUI
121+
) async throws {
122+
let output = AndroidOutput(
123+
xmlOutputDirectory: android.mainRes,
124+
xmlResourcePackage: android.resourcePackage,
125+
srcDirectory: android.mainSrc,
126+
packageName: entry.composePackageName,
127+
templatesPath: android.templatesPath
128+
)
129+
let exporter = AndroidColorExporter(
130+
output: output,
131+
xmlOutputFileName: entry.xmlOutputFileName
132+
)
133+
let files = try exporter.export(colorPairs: colorPairs)
134+
135+
let fileName = entry.xmlOutputFileName ?? "colors.xml"
136+
137+
let lightColorsFileURL = android.mainRes.appendingPathComponent(
138+
"values/" + fileName)
139+
let darkColorsFileURL = android.mainRes.appendingPathComponent(
140+
"values-night/" + fileName)
141+
142+
try? FileManager.default.removeItem(atPath: lightColorsFileURL.path)
143+
try? FileManager.default.removeItem(atPath: darkColorsFileURL.path)
144+
145+
try ExFigCommand.fileWriter.write(files: files)
146+
147+
// Theme attributes export
148+
if let themeConfig = entry.themeAttributes, themeConfig.isEnabled {
149+
try await exportThemeAttributes(
150+
colorPairs: colorPairs,
151+
config: themeConfig,
152+
android: android,
153+
ui: ui
154+
)
155+
}
156+
}
157+
158+
// MARK: - Theme Attributes Export
159+
160+
func exportThemeAttributes(
161+
colorPairs: [AssetPair<Color>],
162+
config: Params.Android.ThemeAttributes,
163+
android: Params.Android,
164+
ui: TerminalUI
165+
) async throws {
166+
let nameTransform = config.nameTransform
167+
168+
// Create exporter with name transformation config
169+
let exporter = AndroidThemeAttributesExporter(
170+
stripPrefixes: nameTransform?.resolvedStripPrefixes ?? [],
171+
style: nameTransform?.resolvedStyle ?? .pascalCase,
172+
prefix: nameTransform?.resolvedPrefix ?? "color"
173+
)
174+
175+
// Export theme attributes content
176+
let result = exporter.export(colorPairs: colorPairs)
177+
178+
// Warn about collisions
179+
if result.hasCollisions {
180+
let collisionInfos = result.collisions.map {
181+
ThemeAttributeCollisionInfo(
182+
attr: $0.attributeName,
183+
kept: $0.keptXmlName,
184+
discarded: $0.discardedXmlName
185+
)
186+
}
187+
ui.warning(.themeAttributesNameCollision(count: result.collisions.count, collisions: collisionInfos))
188+
}
189+
190+
// Skip if no attributes generated
191+
guard !result.attributeMap.isEmpty else { return }
192+
193+
// Resolve file paths relative to mainRes, normalizing .. components
194+
let basePath = android.mainRes.path
195+
let attrsPath = (basePath as NSString).appendingPathComponent(config.resolvedAttrsFile)
196+
let stylesPath = (basePath as NSString).appendingPathComponent(config.resolvedStylesFile)
197+
let stylesNightPath = (basePath as NSString).appendingPathComponent(config.resolvedStylesNightFile)
198+
199+
let attrsURL = URL(fileURLWithPath: (attrsPath as NSString).standardizingPath)
200+
let stylesURL = URL(fileURLWithPath: (stylesPath as NSString).standardizingPath)
201+
let stylesNightURL = URL(fileURLWithPath: (stylesNightPath as NSString).standardizingPath)
202+
203+
// Check if we're in batch mode
204+
if let collector = SharedThemeAttributesStorage.collector {
205+
// Batch mode: collect for later merge
206+
let collection = ThemeAttributesCollection(
207+
themeName: config.themeName,
208+
markerStart: config.resolvedMarkerStart,
209+
markerEnd: config.resolvedMarkerEnd,
210+
attrsContent: result.attrsContent,
211+
stylesContent: result.stylesContent,
212+
attrsFile: attrsURL,
213+
stylesFile: stylesURL,
214+
stylesNightFile: FileManager.default.fileExists(atPath: stylesNightURL.path) ? stylesNightURL : nil,
215+
autoCreateMarkers: config.shouldAutoCreateMarkers
216+
)
217+
await collector.add(collection)
218+
} else {
219+
// Standalone mode: write immediately
220+
try writeThemeAttributesImmediately(
221+
config: config,
222+
result: result,
223+
attrsURL: attrsURL,
224+
stylesURL: stylesURL,
225+
stylesNightURL: stylesNightURL
226+
)
227+
}
228+
}
229+
230+
func writeThemeAttributesImmediately(
231+
config: Params.Android.ThemeAttributes,
232+
result: ThemeAttributesExportResult,
233+
attrsURL: URL,
234+
stylesURL: URL,
235+
stylesNightURL: URL
236+
) throws {
237+
// Create marker updater
238+
let updater = MarkerFileUpdater(
239+
markerStart: config.resolvedMarkerStart,
240+
markerEnd: config.resolvedMarkerEnd,
241+
themeName: config.themeName
242+
)
243+
244+
// Update attrs.xml
245+
try updateThemeAttributesFile(
246+
url: attrsURL,
247+
content: result.attrsContent,
248+
updater: updater,
249+
autoCreate: config.shouldAutoCreateMarkers,
250+
template: attrsXMLTemplate(updater: updater)
251+
)
252+
253+
// Update styles.xml (light)
254+
try updateThemeAttributesFile(
255+
url: stylesURL,
256+
content: result.stylesContent,
257+
updater: updater,
258+
autoCreate: config.shouldAutoCreateMarkers,
259+
template: nil // No auto-create for styles.xml - requires manual theme setup
260+
)
261+
262+
// Update styles-night.xml (dark) if file exists
263+
if FileManager.default.fileExists(atPath: stylesNightURL.path) {
264+
try updateThemeAttributesFile(
265+
url: stylesNightURL,
266+
content: result.stylesContent,
267+
updater: updater,
268+
autoCreate: false,
269+
template: nil
270+
)
271+
}
272+
}
273+
274+
func updateThemeAttributesFile(
275+
url: URL,
276+
content: String,
277+
updater: MarkerFileUpdater,
278+
autoCreate: Bool,
279+
template: String?
280+
) throws {
281+
// Ensure parent directory exists
282+
let directory = url.deletingLastPathComponent()
283+
try FileManager.default.createDirectory(
284+
at: directory,
285+
withIntermediateDirectories: true
286+
)
287+
288+
let updatedContent = try updater.update(
289+
content: content,
290+
in: url,
291+
autoCreate: autoCreate,
292+
templateContent: template
293+
)
294+
295+
try Data(updatedContent.utf8).write(to: url, options: .atomic)
296+
}
297+
298+
func attrsXMLTemplate(updater: MarkerFileUpdater) -> String {
299+
"""
300+
<?xml version="1.0" encoding="utf-8"?>
301+
<resources>
302+
\(updater.fullStartMarker)
303+
\(updater.fullEndMarker)
304+
</resources>
305+
"""
306+
}
307+
}

0 commit comments

Comments
 (0)