diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8ca95b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ +name: "CI" + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +env: + LC_CTYPE: en_US.UTF-8 + LANG: en_US.UTF-8 + SKIP_SWIFTLINT: YES + +jobs: + macOS: + name: macOS + runs-on: macOS-13 + env: + DEVELOPER_DIR: /Applications/Xcode_15.1.app/Contents/Developer + SWIFTLINT_VERSION: 0.52.2 + DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} + steps: + - uses: actions/checkout@v2 + - name: Ruby + uses: ruby/setup-ruby@v1 + - name: Bundler + run: | + gem install bundler + bundle install --without=documentation + - name: Swift + uses: fwal/setup-swift@v1 + - name: Preparation + run: set -o pipefail + - name: Build + run: make build + - name: Test + run: make test + - name: Test Demo + run: make test_demo + - name: Danger + continue-on-error: true + run: bundle exec danger --remove-previous-comments + + linux: + name: Linux + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + - name: Ruby + uses: ruby/setup-ruby@v1 + - name: Bundler + run: | + gem install bundler + bundle install --without=documentation + - name: Swift + uses: fwal/setup-swift@v1 + - name: Preparation + run: set -o pipefail + - name: Build + run: make build + - name: Test + run: make test + + CocoaPods: + name: CocoaPods + runs-on: macOS-latest + steps: + - uses: actions/checkout@v2 + - name: Ruby + uses: ruby/setup-ruby@v1 + - name: Bundler + run: | + gem install bundler + bundle install --without=documentation + - name: Spec Lint + continue-on-error: true + run: bundle exec pod spec lint + - name: Lib Lint + run: bundle exec pod lib lint --skip-tests --allow-warnings --verbose diff --git a/.gitignore b/.gitignore index 1be3f4a..a47b095 100755 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ xcuserdata/ *.moved-aside *.xcuserstate *.xcscmblueprint +*.profraw ## Obj-C/Swift specific *.hmap @@ -53,5 +54,10 @@ fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output -# Releases +# FigmaGen +Demo/figmagen +Demo/Templates figmagen-*.zip + +# Intellij IDEA +.idea diff --git a/.ruby-version b/.ruby-version index ec1cf33..fd2a018 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.6.3 +3.1.0 diff --git a/.swiftlint.yml b/.swiftlint.yml index 435b8cd..339ce19 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,11 +1,22 @@ included: - Sources - - Tests -whitelist_rules: - - anyobject_protocol +excluded: + - "*/Pods" + +analyzer_rules: + - capture_variable + - typesafe_array_init + - unused_declaration + - unused_import + +only_rules: + - accessibility_label_for_image + - accessibility_trait_for_button + - anonymous_argument_in_multiline_closure - array_init - attributes + - blanket_disable_command - block_based_kvo - class_delegate_protocol - closing_brace @@ -16,41 +27,58 @@ whitelist_rules: - collection_alignment - colon - comma + - comma_inheritance + - comment_spacing - compiler_protocol_init + - computed_accessors_order - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty - contains_over_first_not_nil - contains_over_range_nil_comparison - control_statement - convenience_type - custom_rules - cyclomatic_complexity + - direct_return - discarded_notification_center_observer + - discouraged_assert - discouraged_direct_init - discouraged_object_literal + - duplicate_conditions - duplicate_enum_cases - duplicate_imports + - duplicated_key_in_dictionary_literal - dynamic_inline + - empty_collection_literal - empty_count - empty_enum_arguments - empty_parameters - empty_parentheses_with_trailing_closure - empty_string - empty_xctest_method + - enum_case_associated_values_count - explicit_init - - extension_access_modifier + - fatal_error_message - file_header - file_length - file_name + - file_types_order - first_where - flatmap_over_map_reduce - for_where - force_try - function_body_length + - function_parameter_count - generic_type_name + - ibinspectable_in_extension - identical_operands - identifier_name - implicit_getter - - inert_defer + - implicit_return + - implicitly_unwrapped_optional + - inclusive_language + - invalid_swiftlint_command - is_disjoint - joined_default_parameter - large_tuple @@ -63,8 +91,10 @@ whitelist_rules: - legacy_multiple - legacy_nsgeometry_functions - legacy_random + - let_var_whitespace - line_length - literal_expression_end_indentation + - local_doc_comment - lower_acl_than_parent - mark - modifier_order @@ -75,39 +105,54 @@ whitelist_rules: - multiline_parameters_brackets - multiple_closures_with_trailing_closure - nesting + - no_extension_access_modifier + - no_fallthrough_only + - no_space_in_method_call + - ns_number_init_as_function_reference - nsobject_prefer_isequal - number_separator - opening_brace - - operator_usage_whitespace - operator_whitespace + - operator_usage_whitespace + - optional_enum_case_matching + - orphaned_doc_comment - overridden_super_call - override_in_extension - pattern_matching_keywords + - period_spacing + - prefer_self_in_static_references + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init - private_action - private_outlet - private_over_fileprivate + - private_subject - private_unit_test + - prohibited_interface_builder - prohibited_super_call - protocol_property_accessors_order - - quick_discouraged_call - - quick_discouraged_focused_test - - quick_discouraged_pending_test - reduce_boolean + - reduce_into - redundant_discardable_let - redundant_nil_coalescing - redundant_objc_attribute - redundant_optional_initialization + - redundant_self_in_closure - redundant_set_access_control - redundant_string_enum_value - redundant_type_annotation - redundant_void_return - return_arrow_whitespace + - self_binding + - self_in_property_initialization - shorthand_operator + - shorthand_optional_binding - single_test_class - sorted_first_last - statement_position - static_operator - superfluous_disable_command + - superfluous_else - switch_case_alignment - switch_case_on_newline - syntactic_sugar @@ -121,14 +166,15 @@ whitelist_rules: - type_body_length - type_contents_order - type_name + - unavailable_condition - unavailable_function + - unhandled_throwing_task - unneeded_break_in_switch + - unneeded_parentheses_in_closure_argument - untyped_error_in_catch - - unused_capture_list - unused_closure_parameter - unused_control_flow_label - unused_enumerated - - unused_import - unused_optional_binding - unused_setter_value - valid_ibinspectable @@ -137,46 +183,62 @@ whitelist_rules: - vertical_whitespace - vertical_whitespace_between_cases - vertical_whitespace_closing_braces + - void_function_in_ternary - void_return - weak_delegate - - xct_specific_matcher - xctfail_message + - xct_specific_matcher - yoda_condition attributes: always_on_same_line: + - '@dynamic' + - '@GKInspectable' - '@IBAction' - - '@IBOutlet' - '@IBDesignable' - '@IBInspectable' - - '@GKInspectable' + - '@IBOutlet' + - '@nonobjc' - '@NSCopying' - '@NSManaged' - - '@dynamic' - - '@inlinable' - - '@nonobjc' - '@objc' - '@objcMembers' + - '@testable' always_on_line_above: - - '@dynamicMemberLookup' - - '@UIApplicationMain' - - '@NSApplicationMain' - '@available' - '@convention' - '@discardableResult' + - '@dynamicCallable' + - '@dynamicMemberLookup' + - '@frozen' + - '@inlinable' + - '@MainActor' + - '@NSApplicationMain' + - '@propertyWrapper' + - '@requires_stored_property_inits' + - '@UIApplicationMain' - '@usableFromInline' - '@warn_unqualified_access' - - '@testable' + +blanket_disable_command: + allowed_rules: [] + always_blanket_disable: + - file_header + - file_length + - file_name + - file_name_no_space + - file_types_order + - single_test_class closure_body_length: - warning: 20 - error: 100 + warning: 30 + error: 300 collection_alignment: align_colons: false colon: - flexible_right_spacing: false + flexible_right_spacing: true apply_to_dictionaries: true conditional_returns_on_newline: @@ -184,19 +246,23 @@ conditional_returns_on_newline: cyclomatic_complexity: warning: 16 - error: 100 + error: 160 ignores_case_statements: true discouraged_direct_init: types: - - 'Bundle' - - 'UIDevice' - - 'AVAudioSession' + - AVAudioSession + - Bundle + - NSError + - UIDevice discouraged_object_literal: image_literal: true color_literal: true +duplicate_conditions: + severity: warning + duplicate_enum_cases: severity: warning @@ -205,33 +271,50 @@ dynamic_inline: empty_count: severity: warning + only_after_dot: false + +empty_xctest_method: + test_parent_classes: + - XCTestCase + - QuickSpec + +enum_case_associated_values_count: + warning: 6 + error: 60 file_header: - required_pattern: | - \/\/ - \/\/ FigmaGen - \/\/ Copyright © \d{4} HeadHunter\ - \/\/ MIT Licence - \/\/ + forbidden_pattern: ".?" file_length: warning: 500 - error: 2000 + error: 5000 ignore_comment_only_lines: true file_name: excluded: - - 'main.swift' + - main.swift prefix_pattern: '' suffix_pattern: '[+][A-z][A-z]+' nested_type_separator: '' +file_types_order: + order: + - main_type + - extension + - preview_provider + - library_content_provider + force_try: severity: warning function_body_length: warning: 40 - error: 100 + error: 400 + +function_parameter_count: + warning: 6 + error: 60 + ignores_default_parameters: false generic_type_name: min_length: @@ -239,75 +322,102 @@ generic_type_name: error: 0 max_length: warning: 20 - error: 100 - validates_start_with_lowercase: true + error: 200 excluded: - - 'T' - - 'U' - - 'V' + - T + - U + - V + allowed_symbols: [] + validates_start_with_lowercase: warning identifier_name: min_length: warning: 2 - error: 1 + error: 0 max_length: - warning: 40 - error: 100 - validates_start_with_lowercase: true + warning: 50 + error: 500 excluded: - - 'a' - - 'r' - - 'g' - - 'b' - - 'i' - - 'j' - - 'x' - - 'y' - - 'z' + - a + - r + - g + - b + - i + - j + - x + - y + - z + - w + allowed_symbols: [] + validates_start_with_lowercase: warning + +implicit_return: + included: + - closure + - function + - getter + +implicitly_unwrapped_optional: + mode: all_except_iboutlets large_tuple: warning: 3 - error: 8 + error: 30 line_length: warning: 120 - error: 1000 - ignores_urls: false + error: 1200 + ignores_urls: true ignores_function_declarations: false ignores_comments: false ignores_interpolated_strings: false modifier_order: preferred_modifier_order: - - 'acl' - - 'setterACL' - - 'override' - - 'owned' - - 'mutators' - - 'final' - - 'typeMethods' - - 'required' - - 'convenience' - - 'lazy' - - 'dynamic' + - acl + - setterACL + - override + - owned + - mutators + - final + - typeMethods + - required + - convenience + - lazy + - dynamic multiline_arguments: - first_argument_location: 'next_line' + first_argument_location: next_line only_enforce_after_first_closure_on_first_line: false nesting: type_level: warning: 1 error: 10 - statement_level: - warning: 4 - error: 10 + function_level: + warning: 2 + error: 20 + check_nesting_in_closures_and_statements: true + always_allow_one_type_in_functions: false + +no_extension_access_modifier: + severity: warning number_separator: minimum_length: 5 minimum_fraction_length: 5 exclude_ranges: [] +opening_brace: + allow_multiline_func: false + +operator_usage_whitespace: + lines_look_around: 2 + skip_aligned_constants: false + allowed_no_space_operators: + - '...' + - '..<' + overridden_super_call: included: - '*' @@ -324,11 +434,19 @@ prohibited_super_call: - '*' excluded: [] +self_binding: + bind_identifier: self + shorthand_operator: severity: warning +single_test_class: + test_parent_classes: + - XCTestCase + - QuickSpec + statement_position: - statement_mode: 'default' + statement_mode: default switch_case_alignment: indented_cases: false @@ -343,35 +461,48 @@ trailing_whitespace: ignores_empty_lines: false ignores_comments: false +type_body_length: + warning: 450 + error: 4500 + type_contents_order: order: - associated_type - type_alias - subtype + - case - type_property - type_method - - case - ib_outlet - ib_inspectable - instance_property - initializer + - deinitializer - ib_action - other_method - view_life_cycle_method - subscript -type_body_length: - warning: 400 - error: 2000 - type_name: min_length: warning: 3 error: 0 max_length: warning: 50 - error: 100 - validates_start_with_lowercase: true + error: 500 + excluded: [] + allowed_symbols: [] + validates_start_with_lowercase: warning + validate_protocols: true + +unused_declaration: + severity: warning + include_public_and_open: false + +unused_import: + require_explicit_imports: false + allowed_transitive_imports: [] + always_keep_imports: [] unused_optional_binding: ignore_optional_try: false @@ -379,4 +510,12 @@ unused_optional_binding: vertical_whitespace: max_empty_lines: 1 -warning_threshold: 500 +vertical_whitespace_closing_braces: + only_enforce_before_trivial_lines: false + +xct_specific_matcher: + matchers: + - one-argument-asserts + - two-argument-asserts + +warning_threshold: 100 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 482fe65..0000000 --- a/.travis.yml +++ /dev/null @@ -1,29 +0,0 @@ -language: swift -osx_image: xcode11.6 -rvm: 2.6.3 -gemfile: Gemfile - -cache: - - bundler - - cocoapods - -env: - global: - - LC_CTYPE=en_US.UTF-8 - - LANG=en_US.UTF-8 - - SKIP_SWIFT_LINT=TRUE - -install: - - gem update bundler - - bundle install --without=documentation - -script: - - set -o pipefail - - swift --version - - swift test - - bundle exec pod spec lint --skip-tests --allow-warnings - - bundle exec danger --remove-previous-comments - -branches: - only: - - master diff --git a/.xcode-version b/.xcode-version new file mode 100644 index 0000000..0f8f73c --- /dev/null +++ b/.xcode-version @@ -0,0 +1 @@ +14.2 \ No newline at end of file diff --git a/Demo/.figmagen.yml b/Demo/.figmagen.yml index e39af3f..a027a90 100644 --- a/Demo/.figmagen.yml +++ b/Demo/.figmagen.yml @@ -1,18 +1,81 @@ base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 -colors: - includingNodes: - - 7:24 - destinationPath: FigmaGenDemo/Generated/Colors.swift - -spacings: - includingNodes: - - 413:88 - destinationPath: FigmaGenDemo/Generated/Spacings.swift +colorStyles: + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/Fugen-Demo?node-id=91%3A3 + assets: FigmaGenDemo/Resources/Colors.xcassets + destination: FigmaGenDemo/Generated/ColorStyle.swift + templateOptions: + publicAccess: true textStyles: - includingNodes: - - 3:19 - destinationPath: FigmaGenDemo/Generated/TextStyle.swift + file: + key: 61xw2FQn61Xr7VVFYwiHHy + includedNodes: + - 91%3A38 + destination: FigmaGenDemo/Generated/TextStyle.swift + templateOptions: + publicAccess: true + usingSystemFonts: false + +images: + file: + key: W6dy4CFSZWUVpVnSzNZIxA + includedNodes: + - 92:106 + - 92:242 + assets: FigmaGenDemo/Resources/Images.xcassets/Generated + destination: FigmaGenDemo/Generated/Images.swift + groupByFrame: true + groupByComponentSet: true + templateOptions: + publicAccess: true + +shadowStyles: + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/Fugen-Demo?node-id=236%3A4 + destination: FigmaGenDemo/Generated/ShadowStyle.swift + templateOptions: + publicAccess: true + +tokens: + file: https://www.figma.com/file/W6dy4CFSZWUVpVnSzNZIxA/FigmaGen-Tokens-Demo + themes: + - hh-day + - hh-night + fallbackTheme: hh-day + templates: + colors: + - destination: FigmaGenDemo/Generated/ColorTokens.swift + templateOptions: + publicAccess: true + baseColors: + destination: FigmaGenDemo/Generated/BaseColorTokens.swift + templateOptions: + publicAccess: true + fontFamilies: + destination: FigmaGenDemo/Generated/FontFamilyTokens.swift + templateOptions: + publicAccess: true + typographies: + destination: FigmaGenDemo/Generated/TypographyTokens.swift + templateOptions: + publicAccess: true + styleTypeName: Typography + boxShadows: + destination: FigmaGenDemo/Generated/BoxShadowTokens.swift + templateOptions: + publicAccess: true + shadowTypeName: ShadowToken + spacing: + destination: FigmaGenDemo/Generated/SpacingTokens.swift + templateOptions: + publicAccess: true + borders: + destination: FigmaGenDemo/Generated/BorderTokens.swift + templateOptions: + publicAccess: true + theme: + destination: FigmaGenDemo/Generated/Theme.swift + templateOptions: + publicAccess: true + shadowTypeName: ShadowToken diff --git a/Demo/FigmaGenDemo.xcodeproj/project.pbxproj b/Demo/FigmaGenDemo.xcodeproj/project.pbxproj index 5de8709..dd1ea17 100644 --- a/Demo/FigmaGenDemo.xcodeproj/project.pbxproj +++ b/Demo/FigmaGenDemo.xcodeproj/project.pbxproj @@ -6,145 +6,296 @@ objectVersion = 51; objects = { +/* Begin PBXAggregateTarget section */ + C01B801E25060F6B0080227E /* FigmaGen */ = { + isa = PBXAggregateTarget; + buildConfigurationList = C01B801F25060F6B0080227E /* Build configuration list for PBXAggregateTarget "FigmaGen" */; + buildPhases = ( + C01B802225060F710080227E /* FigmaGen */, + ); + dependencies = ( + ); + name = FigmaGen; + productName = Fugen; + }; +/* End PBXAggregateTarget section */ + /* Begin PBXBuildFile section */ - C09AD30D238FFE0F005C787A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09AD30C238FFE0F005C787A /* AppDelegate.swift */; }; - C09AD311238FFE0F005C787A /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09AD310238FFE0F005C787A /* ViewController.swift */; }; - C09AD314238FFE0F005C787A /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C09AD312238FFE0F005C787A /* Main.storyboard */; }; - C09AD316238FFE10005C787A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C09AD315238FFE10005C787A /* Assets.xcassets */; }; - C09AD319238FFE10005C787A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C09AD317238FFE10005C787A /* LaunchScreen.storyboard */; }; - C0FE20572394540F003A6701 /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FE20552394540F003A6701 /* TextStyle.swift */; }; - C0FE20582394540F003A6701 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FE20562394540F003A6701 /* Colors.swift */; }; - C0FE2066239573BF003A6701 /* SF-Pro-Display-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = C0FE205C239573BF003A6701 /* SF-Pro-Display-Light.otf */; }; - C0FE2067239573BF003A6701 /* SF-Pro-Display-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = C0FE205D239573BF003A6701 /* SF-Pro-Display-Regular.otf */; }; - C0FE2068239573BF003A6701 /* SF-Pro-Display-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = C0FE205E239573BF003A6701 /* SF-Pro-Display-Bold.otf */; }; - C0FE2069239573BF003A6701 /* SF-Pro-Display-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = C0FE205F239573BF003A6701 /* SF-Pro-Display-Medium.otf */; }; - C0FE206B239573BF003A6701 /* SF-Pro-Display-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = C0FE2061239573BF003A6701 /* SF-Pro-Display-Semibold.otf */; }; + 7ED1B6F1C418840A5C1245BE /* Pods_FigmaGenDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 11B7DAD9DD79D94016B46F17 /* Pods_FigmaGenDemo.framework */; }; + 8E44BFD42A67ECB300EE5D7E /* BaseColorTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E44BFD22A67ECB300EE5D7E /* BaseColorTokens.swift */; }; + 8E44BFD52A67ECB300EE5D7E /* ColorTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E44BFD32A67ECB300EE5D7E /* ColorTokens.swift */; }; + 8E5334092A420D9B006D6569 /* ShadowStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5333F42A420D9B006D6569 /* ShadowStyle.swift */; }; + 8E53340A2A420D9B006D6569 /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5333F52A420D9B006D6569 /* TextStyle.swift */; }; + 8E53340B2A420D9B006D6569 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5333F62A420D9B006D6569 /* Images.swift */; }; + 8E53340C2A420D9B006D6569 /* ColorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5333F72A420D9B006D6569 /* ColorStyle.swift */; }; + 8E53340D2A420D9B006D6569 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8E5333F92A420D9B006D6569 /* LaunchScreen.storyboard */; }; + 8E53340E2A420D9B006D6569 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8E5333FB2A420D9B006D6569 /* Main.storyboard */; }; + 8E53340F2A420D9B006D6569 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5333FD2A420D9B006D6569 /* ViewController.swift */; }; + 8E5334102A420D9B006D6569 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8E5333FF2A420D9B006D6569 /* Colors.xcassets */; }; + 8E5334112A420D9B006D6569 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334002A420D9B006D6569 /* Images.xcassets */; }; + 8E5334122A420D9B006D6569 /* SF-Pro-Display-Light.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334022A420D9B006D6569 /* SF-Pro-Display-Light.otf */; }; + 8E5334132A420D9B006D6569 /* SF-Pro-Display-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334032A420D9B006D6569 /* SF-Pro-Display-Regular.otf */; }; + 8E5334142A420D9B006D6569 /* SF-Pro-Display-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334042A420D9B006D6569 /* SF-Pro-Display-Bold.otf */; }; + 8E5334152A420D9B006D6569 /* SF-Pro-Display-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334052A420D9B006D6569 /* SF-Pro-Display-Medium.otf */; }; + 8E5334162A420D9B006D6569 /* SF-Pro-Display-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8E5334062A420D9B006D6569 /* SF-Pro-Display-Semibold.otf */; }; + 8E5334172A420D9B006D6569 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E5334072A420D9B006D6569 /* AppDelegate.swift */; }; + 8E53341C2A420DA4006D6569 /* FigmaGenDemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E53341A2A420DA4006D6569 /* FigmaGenDemoTests.swift */; }; + 8E6B236E2A8A6BA100192E1A /* SpacingTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E6B236D2A8A6BA100192E1A /* SpacingTokens.swift */; }; + 8E97DB762A741FD600E8AAC1 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E97DB752A741FD600E8AAC1 /* Theme.swift */; }; + 8EAE283E2A716D4A007C477F /* BoxShadowTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EAE283D2A716D4A007C477F /* BoxShadowTokens.swift */; }; + 8EFF17F52A70096E00C47577 /* FontFamilyTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EFF17F32A70096D00C47577 /* FontFamilyTokens.swift */; }; + 8EFF17F62A70096E00C47577 /* TypographyTokens.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EFF17F42A70096D00C47577 /* TypographyTokens.swift */; }; + 8EFF17F92A70233300C47577 /* Inter-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8EFF17F72A70233300C47577 /* Inter-Regular.otf */; }; + 8EFF17FA2A70233300C47577 /* Inter-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 8EFF17F82A70233300C47577 /* Inter-Bold.otf */; }; + 8EFF17FC2A7023AE00C47577 /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 8EFF17FB2A7023AE00C47577 /* Roboto-Regular.ttf */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + C0D5EB6023EEBE92000AB1B0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C0E6B5EF238096C500083ADB /* Project object */; + proxyType = 1; + remoteGlobalIDString = C0E6B5F6238096C500083ADB; + remoteInfo = FugenDemo; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ - C09AD309238FFE0F005C787A /* FigmaGenDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FigmaGenDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; - C09AD30C238FFE0F005C787A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - C09AD310238FFE0F005C787A /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - C09AD313238FFE0F005C787A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - C09AD315238FFE10005C787A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - C09AD318238FFE10005C787A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - C09AD31A238FFE10005C787A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C0FE20552394540F003A6701 /* TextStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; - C0FE20562394540F003A6701 /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; - C0FE205C239573BF003A6701 /* SF-Pro-Display-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Light.otf"; sourceTree = ""; }; - C0FE205D239573BF003A6701 /* SF-Pro-Display-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Regular.otf"; sourceTree = ""; }; - C0FE205E239573BF003A6701 /* SF-Pro-Display-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Bold.otf"; sourceTree = ""; }; - C0FE205F239573BF003A6701 /* SF-Pro-Display-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Medium.otf"; sourceTree = ""; }; - C0FE2061239573BF003A6701 /* SF-Pro-Display-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Semibold.otf"; sourceTree = ""; }; + 11B7DAD9DD79D94016B46F17 /* Pods_FigmaGenDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FigmaGenDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 23C6A1263942D9DA0D5FFDCC /* Pods-FigmaGenDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FigmaGenDemo.release.xcconfig"; path = "Target Support Files/Pods-FigmaGenDemo/Pods-FigmaGenDemo.release.xcconfig"; sourceTree = ""; }; + 29B6BD966DD89C853A1AFFD2 /* Pods-FugenDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FugenDemo.debug.xcconfig"; path = "Target Support Files/Pods-FugenDemo/Pods-FugenDemo.debug.xcconfig"; sourceTree = ""; }; + 8E44BFD22A67ECB300EE5D7E /* BaseColorTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseColorTokens.swift; sourceTree = ""; }; + 8E44BFD32A67ECB300EE5D7E /* ColorTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorTokens.swift; sourceTree = ""; }; + 8E5333F42A420D9B006D6569 /* ShadowStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowStyle.swift; sourceTree = ""; }; + 8E5333F52A420D9B006D6569 /* TextStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; + 8E5333F62A420D9B006D6569 /* Images.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; + 8E5333F72A420D9B006D6569 /* ColorStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorStyle.swift; sourceTree = ""; }; + 8E5333FA2A420D9B006D6569 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 8E5333FC2A420D9B006D6569 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 8E5333FD2A420D9B006D6569 /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 8E5333FF2A420D9B006D6569 /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; + 8E5334002A420D9B006D6569 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + 8E5334022A420D9B006D6569 /* SF-Pro-Display-Light.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Light.otf"; sourceTree = ""; }; + 8E5334032A420D9B006D6569 /* SF-Pro-Display-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Regular.otf"; sourceTree = ""; }; + 8E5334042A420D9B006D6569 /* SF-Pro-Display-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Bold.otf"; sourceTree = ""; }; + 8E5334052A420D9B006D6569 /* SF-Pro-Display-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Medium.otf"; sourceTree = ""; }; + 8E5334062A420D9B006D6569 /* SF-Pro-Display-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SF-Pro-Display-Semibold.otf"; sourceTree = ""; }; + 8E5334072A420D9B006D6569 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 8E5334082A420D9B006D6569 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8E53341A2A420DA4006D6569 /* FigmaGenDemoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaGenDemoTests.swift; sourceTree = ""; }; + 8E53341B2A420DA4006D6569 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 8E6B236D2A8A6BA100192E1A /* SpacingTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpacingTokens.swift; sourceTree = ""; }; + 8E97DB752A741FD600E8AAC1 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 8EAE283D2A716D4A007C477F /* BoxShadowTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoxShadowTokens.swift; sourceTree = ""; }; + 8EFF17F32A70096D00C47577 /* FontFamilyTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FontFamilyTokens.swift; sourceTree = ""; }; + 8EFF17F42A70096D00C47577 /* TypographyTokens.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypographyTokens.swift; sourceTree = ""; }; + 8EFF17F72A70233300C47577 /* Inter-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Regular.otf"; sourceTree = ""; }; + 8EFF17F82A70233300C47577 /* Inter-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Inter-Bold.otf"; sourceTree = ""; }; + 8EFF17FB2A7023AE00C47577 /* Roboto-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Regular.ttf"; sourceTree = ""; }; + 8FC235E00E36DC62C4755546 /* Pods-Fugen Demo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fugen Demo.debug.xcconfig"; path = "Target Support Files/Pods-Fugen Demo/Pods-Fugen Demo.debug.xcconfig"; sourceTree = ""; }; + A8D0E422FCBD612FC060F4E3 /* Pods-FigmaGenDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FigmaGenDemo.debug.xcconfig"; path = "Target Support Files/Pods-FigmaGenDemo/Pods-FigmaGenDemo.debug.xcconfig"; sourceTree = ""; }; + BEAAC6E1082665BBB49FA803 /* Pods-FugenDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FugenDemo.release.xcconfig"; path = "Target Support Files/Pods-FugenDemo/Pods-FugenDemo.release.xcconfig"; sourceTree = ""; }; + C0D5EB5B23EEBE92000AB1B0 /* FigmaGenDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FigmaGenDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C0E6B5F7238096C500083ADB /* FigmaGenDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FigmaGenDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + C670EBD294B45540406F27CE /* Pods-Fugen Demo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Fugen Demo.release.xcconfig"; path = "Target Support Files/Pods-Fugen Demo/Pods-Fugen Demo.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - C09AD306238FFE0F005C787A /* Frameworks */ = { + C0D5EB5823EEBE92000AB1B0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; + C0E6B5F4238096C500083ADB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7ED1B6F1C418840A5C1245BE /* Pods_FigmaGenDemo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - C05B3B762393CF020042A8AA /* Resources */ = { + 45BFC2FE748FBFC15ABD446E /* Pods */ = { isa = PBXGroup; children = ( - C0FE205B239573BF003A6701 /* Fonts */, - C09AD315238FFE10005C787A /* Assets.xcassets */, - C09AD317238FFE10005C787A /* LaunchScreen.storyboard */, - C09AD312238FFE0F005C787A /* Main.storyboard */, + 8FC235E00E36DC62C4755546 /* Pods-Fugen Demo.debug.xcconfig */, + C670EBD294B45540406F27CE /* Pods-Fugen Demo.release.xcconfig */, + 29B6BD966DD89C853A1AFFD2 /* Pods-FugenDemo.debug.xcconfig */, + BEAAC6E1082665BBB49FA803 /* Pods-FugenDemo.release.xcconfig */, + A8D0E422FCBD612FC060F4E3 /* Pods-FigmaGenDemo.debug.xcconfig */, + 23C6A1263942D9DA0D5FFDCC /* Pods-FigmaGenDemo.release.xcconfig */, ); - path = Resources; + path = Pods; sourceTree = ""; }; - C09AD300238FFE0F005C787A = { + 8E5333F22A420D9B006D6569 /* FigmaGenDemo */ = { isa = PBXGroup; children = ( - C09AD30B238FFE0F005C787A /* FigmaGenDemo */, - C09AD30A238FFE0F005C787A /* Products */, + 8E5333F32A420D9B006D6569 /* Generated */, + 8E5333F82A420D9B006D6569 /* Storyboards */, + 8E5333FD2A420D9B006D6569 /* ViewController.swift */, + 8E5333FE2A420D9B006D6569 /* Resources */, + 8E5334072A420D9B006D6569 /* AppDelegate.swift */, + 8E5334082A420D9B006D6569 /* Info.plist */, ); + path = FigmaGenDemo; sourceTree = ""; }; - C09AD30A238FFE0F005C787A /* Products */ = { + 8E5333F32A420D9B006D6569 /* Generated */ = { isa = PBXGroup; children = ( - C09AD309238FFE0F005C787A /* FigmaGenDemo.app */, + 8E44BFD22A67ECB300EE5D7E /* BaseColorTokens.swift */, + 8EAE283D2A716D4A007C477F /* BoxShadowTokens.swift */, + 8E5333F72A420D9B006D6569 /* ColorStyle.swift */, + 8E44BFD32A67ECB300EE5D7E /* ColorTokens.swift */, + 8EFF17F32A70096D00C47577 /* FontFamilyTokens.swift */, + 8E5333F62A420D9B006D6569 /* Images.swift */, + 8E5333F42A420D9B006D6569 /* ShadowStyle.swift */, + 8E6B236D2A8A6BA100192E1A /* SpacingTokens.swift */, + 8E5333F52A420D9B006D6569 /* TextStyle.swift */, + 8E97DB752A741FD600E8AAC1 /* Theme.swift */, + 8EFF17F42A70096D00C47577 /* TypographyTokens.swift */, ); - name = Products; + path = Generated; sourceTree = ""; }; - C09AD30B238FFE0F005C787A /* FigmaGenDemo */ = { + 8E5333F82A420D9B006D6569 /* Storyboards */ = { isa = PBXGroup; children = ( - C0FE205423945089003A6701 /* Generated */, - C05B3B762393CF020042A8AA /* Resources */, - C09AD30C238FFE0F005C787A /* AppDelegate.swift */, - C09AD310238FFE0F005C787A /* ViewController.swift */, - C09AD31A238FFE10005C787A /* Info.plist */, + 8E5333F92A420D9B006D6569 /* LaunchScreen.storyboard */, + 8E5333FB2A420D9B006D6569 /* Main.storyboard */, ); - path = FigmaGenDemo; + path = Storyboards; sourceTree = ""; }; - C0FE205423945089003A6701 /* Generated */ = { + 8E5333FE2A420D9B006D6569 /* Resources */ = { isa = PBXGroup; children = ( - C0FE20562394540F003A6701 /* Colors.swift */, - C0FE20552394540F003A6701 /* TextStyle.swift */, + 8E5333FF2A420D9B006D6569 /* Colors.xcassets */, + 8E5334002A420D9B006D6569 /* Images.xcassets */, + 8E5334012A420D9B006D6569 /* Fonts */, ); - path = Generated; + path = Resources; sourceTree = ""; }; - C0FE205B239573BF003A6701 /* Fonts */ = { + 8E5334012A420D9B006D6569 /* Fonts */ = { isa = PBXGroup; children = ( - C0FE205E239573BF003A6701 /* SF-Pro-Display-Bold.otf */, - C0FE205C239573BF003A6701 /* SF-Pro-Display-Light.otf */, - C0FE205F239573BF003A6701 /* SF-Pro-Display-Medium.otf */, - C0FE205D239573BF003A6701 /* SF-Pro-Display-Regular.otf */, - C0FE2061239573BF003A6701 /* SF-Pro-Display-Semibold.otf */, + 8EFF17F82A70233300C47577 /* Inter-Bold.otf */, + 8EFF17F72A70233300C47577 /* Inter-Regular.otf */, + 8EFF17FB2A7023AE00C47577 /* Roboto-Regular.ttf */, + 8E5334042A420D9B006D6569 /* SF-Pro-Display-Bold.otf */, + 8E5334022A420D9B006D6569 /* SF-Pro-Display-Light.otf */, + 8E5334052A420D9B006D6569 /* SF-Pro-Display-Medium.otf */, + 8E5334032A420D9B006D6569 /* SF-Pro-Display-Regular.otf */, + 8E5334062A420D9B006D6569 /* SF-Pro-Display-Semibold.otf */, ); path = Fonts; sourceTree = ""; }; + 8E5334192A420DA4006D6569 /* FigmaGenDemoTests */ = { + isa = PBXGroup; + children = ( + 8E53341A2A420DA4006D6569 /* FigmaGenDemoTests.swift */, + 8E53341B2A420DA4006D6569 /* Info.plist */, + ); + path = FigmaGenDemoTests; + sourceTree = ""; + }; + C0E6B5EE238096C500083ADB = { + isa = PBXGroup; + children = ( + 8E5333F22A420D9B006D6569 /* FigmaGenDemo */, + 8E5334192A420DA4006D6569 /* FigmaGenDemoTests */, + C0E6B5F8238096C500083ADB /* Products */, + 45BFC2FE748FBFC15ABD446E /* Pods */, + CB1DDE6FE90E7C3F902816CB /* Frameworks */, + ); + sourceTree = ""; + }; + C0E6B5F8238096C500083ADB /* Products */ = { + isa = PBXGroup; + children = ( + C0E6B5F7238096C500083ADB /* FigmaGenDemo.app */, + C0D5EB5B23EEBE92000AB1B0 /* FigmaGenDemoTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CB1DDE6FE90E7C3F902816CB /* Frameworks */ = { + isa = PBXGroup; + children = ( + 11B7DAD9DD79D94016B46F17 /* Pods_FigmaGenDemo.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - C09AD308238FFE0F005C787A /* FigmaGenDemo */ = { + C0D5EB5A23EEBE92000AB1B0 /* FigmaGenDemoTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C0D5EB6423EEBE92000AB1B0 /* Build configuration list for PBXNativeTarget "FigmaGenDemoTests" */; + buildPhases = ( + C0D5EB5723EEBE92000AB1B0 /* Sources */, + C0D5EB5823EEBE92000AB1B0 /* Frameworks */, + C0D5EB5923EEBE92000AB1B0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + C0D5EB6123EEBE92000AB1B0 /* PBXTargetDependency */, + ); + name = FigmaGenDemoTests; + productName = FugenDemoTests; + productReference = C0D5EB5B23EEBE92000AB1B0 /* FigmaGenDemoTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + C0E6B5F6238096C500083ADB /* FigmaGenDemo */ = { isa = PBXNativeTarget; - buildConfigurationList = C09AD31D238FFE10005C787A /* Build configuration list for PBXNativeTarget "FigmaGenDemo" */; + buildConfigurationList = C0E6B60B238096C500083ADB /* Build configuration list for PBXNativeTarget "FigmaGenDemo" */; buildPhases = ( - C09AD305238FFE0F005C787A /* Sources */, - C09AD306238FFE0F005C787A /* Frameworks */, - C09AD307238FFE0F005C787A /* Resources */, + 2F36647D95B78805F756F3F1 /* [CP] Check Pods Manifest.lock */, + C0E6B5F3238096C500083ADB /* Sources */, + C0E6B5F4238096C500083ADB /* Frameworks */, + C0E6B5F5238096C500083ADB /* Resources */, ); buildRules = ( ); dependencies = ( ); name = FigmaGenDemo; - productName = FigmaGenDemo; - productReference = C09AD309238FFE0F005C787A /* FigmaGenDemo.app */; + productName = "Fugen Demo"; + productReference = C0E6B5F7238096C500083ADB /* FigmaGenDemo.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - C09AD301238FFE0F005C787A /* Project object */ = { + C0E6B5EF238096C500083ADB /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1120; - LastUpgradeCheck = 1120; - ORGANIZATIONNAME = HeadHunter; + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "Almaz Ibragimov"; TargetAttributes = { - C09AD308238FFE0F005C787A = { - CreatedOnToolsVersion = 11.2.1; + C01B801E25060F6B0080227E = { + CreatedOnToolsVersion = 11.5; + }; + C0D5EB5A23EEBE92000AB1B0 = { + CreatedOnToolsVersion = 11.3.1; + TestTargetID = C0E6B5F6238096C500083ADB; + }; + C0E6B5F6238096C500083ADB = { + CreatedOnToolsVersion = 11.0; }; }; }; - buildConfigurationList = C09AD304238FFE0F005C787A /* Build configuration list for PBXProject "FigmaGenDemo" */; + buildConfigurationList = C0E6B5F2238096C500083ADB /* Build configuration list for PBXProject "FigmaGenDemo" */; compatibilityVersion = "Xcode 10.0"; developmentRegion = en; hasScannedForEncodings = 0; @@ -152,69 +303,208 @@ en, Base, ); - mainGroup = C09AD300238FFE0F005C787A; - productRefGroup = C09AD30A238FFE0F005C787A /* Products */; + mainGroup = C0E6B5EE238096C500083ADB; + productRefGroup = C0E6B5F8238096C500083ADB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - C09AD308238FFE0F005C787A /* FigmaGenDemo */, + C0E6B5F6238096C500083ADB /* FigmaGenDemo */, + C0D5EB5A23EEBE92000AB1B0 /* FigmaGenDemoTests */, + C01B801E25060F6B0080227E /* FigmaGen */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - C09AD307238FFE0F005C787A /* Resources */ = { + C0D5EB5923EEBE92000AB1B0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C0E6B5F5238096C500083ADB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C09AD319238FFE10005C787A /* LaunchScreen.storyboard in Resources */, - C09AD316238FFE10005C787A /* Assets.xcassets in Resources */, - C0FE2068239573BF003A6701 /* SF-Pro-Display-Bold.otf in Resources */, - C0FE2066239573BF003A6701 /* SF-Pro-Display-Light.otf in Resources */, - C0FE2067239573BF003A6701 /* SF-Pro-Display-Regular.otf in Resources */, - C0FE206B239573BF003A6701 /* SF-Pro-Display-Semibold.otf in Resources */, - C09AD314238FFE0F005C787A /* Main.storyboard in Resources */, - C0FE2069239573BF003A6701 /* SF-Pro-Display-Medium.otf in Resources */, + 8E5334152A420D9B006D6569 /* SF-Pro-Display-Medium.otf in Resources */, + 8EFF17F92A70233300C47577 /* Inter-Regular.otf in Resources */, + 8E5334102A420D9B006D6569 /* Colors.xcassets in Resources */, + 8E5334122A420D9B006D6569 /* SF-Pro-Display-Light.otf in Resources */, + 8E5334112A420D9B006D6569 /* Images.xcassets in Resources */, + 8E53340D2A420D9B006D6569 /* LaunchScreen.storyboard in Resources */, + 8EFF17FC2A7023AE00C47577 /* Roboto-Regular.ttf in Resources */, + 8EFF17FA2A70233300C47577 /* Inter-Bold.otf in Resources */, + 8E5334142A420D9B006D6569 /* SF-Pro-Display-Bold.otf in Resources */, + 8E5334162A420D9B006D6569 /* SF-Pro-Display-Semibold.otf in Resources */, + 8E5334132A420D9B006D6569 /* SF-Pro-Display-Regular.otf in Resources */, + 8E53340E2A420D9B006D6569 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 2F36647D95B78805F756F3F1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FigmaGenDemo-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C01B802225060F710080227E /* FigmaGen */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = FigmaGen; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "Pods/FigmaGen/figmagen generate\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ - C09AD305238FFE0F005C787A /* Sources */ = { + C0D5EB5723EEBE92000AB1B0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8E53341C2A420DA4006D6569 /* FigmaGenDemoTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C0E6B5F3238096C500083ADB /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C0FE20572394540F003A6701 /* TextStyle.swift in Sources */, - C0FE20582394540F003A6701 /* Colors.swift in Sources */, - C09AD311238FFE0F005C787A /* ViewController.swift in Sources */, - C09AD30D238FFE0F005C787A /* AppDelegate.swift in Sources */, + 8EFF17F62A70096E00C47577 /* TypographyTokens.swift in Sources */, + 8EFF17F52A70096E00C47577 /* FontFamilyTokens.swift in Sources */, + 8E44BFD42A67ECB300EE5D7E /* BaseColorTokens.swift in Sources */, + 8E5334172A420D9B006D6569 /* AppDelegate.swift in Sources */, + 8EAE283E2A716D4A007C477F /* BoxShadowTokens.swift in Sources */, + 8E53340F2A420D9B006D6569 /* ViewController.swift in Sources */, + 8E6B236E2A8A6BA100192E1A /* SpacingTokens.swift in Sources */, + 8E53340A2A420D9B006D6569 /* TextStyle.swift in Sources */, + 8E53340B2A420D9B006D6569 /* Images.swift in Sources */, + 8E5334092A420D9B006D6569 /* ShadowStyle.swift in Sources */, + 8E97DB762A741FD600E8AAC1 /* Theme.swift in Sources */, + 8E53340C2A420D9B006D6569 /* ColorStyle.swift in Sources */, + 8E44BFD52A67ECB300EE5D7E /* ColorTokens.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + C0D5EB6123EEBE92000AB1B0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C0E6B5F6238096C500083ADB /* FigmaGenDemo */; + targetProxy = C0D5EB6023EEBE92000AB1B0 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ - C09AD312238FFE0F005C787A /* Main.storyboard */ = { + 8E5333F92A420D9B006D6569 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( - C09AD313238FFE0F005C787A /* Base */, + 8E5333FA2A420D9B006D6569 /* Base */, ); - name = Main.storyboard; + name = LaunchScreen.storyboard; sourceTree = ""; }; - C09AD317238FFE10005C787A /* LaunchScreen.storyboard */ = { + 8E5333FB2A420D9B006D6569 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( - C09AD318238FFE10005C787A /* Base */, + 8E5333FC2A420D9B006D6569 /* Base */, ); - name = LaunchScreen.storyboard; + name = Main.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - C09AD31B238FFE10005C787A /* Debug */ = { + C01B802025060F6B0080227E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + C01B802125060F6B0080227E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + C0D5EB6223EEBE92000AB1B0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = FigmaGenDemoTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "ru.hh.FigmaGen-Demo.Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FigmaGenDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FigmaGenDemo"; + }; + name = Debug; + }; + C0D5EB6323EEBE92000AB1B0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = FigmaGenDemoTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "ru.hh.FigmaGen-Demo.Tests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FigmaGenDemo.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FigmaGenDemo"; + }; + name = Release; + }; + C0E6B609238096C500083ADB /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -246,6 +536,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -264,7 +555,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -274,7 +565,7 @@ }; name = Debug; }; - C09AD31C238FFE10005C787A /* Release */ = { + C0E6B60A238096C500083ADB /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -306,6 +597,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; @@ -318,7 +610,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -328,62 +620,86 @@ }; name = Release; }; - C09AD31E238FFE10005C787A /* Debug */ = { + C0E6B60C238096C500083ADB /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = A8D0E422FCBD612FC060F4E3 /* Pods-FigmaGenDemo.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = FigmaGenDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.self.FigmaGenDemo; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "ru.hh.FigmaGen-Demo"; + PRODUCT_NAME = FigmaGenDemo; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; - C09AD31F238FFE10005C787A /* Release */ = { + C0E6B60D238096C500083ADB /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 23C6A1263942D9DA0D5FFDCC /* Pods-FigmaGenDemo.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = FigmaGenDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.self.FigmaGenDemo; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_BUNDLE_IDENTIFIER = "ru.hh.FigmaGen-Demo"; + PRODUCT_NAME = FigmaGenDemo; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; + TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - C09AD304238FFE0F005C787A /* Build configuration list for PBXProject "FigmaGenDemo" */ = { + C01B801F25060F6B0080227E /* Build configuration list for PBXAggregateTarget "FigmaGen" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C01B802025060F6B0080227E /* Debug */, + C01B802125060F6B0080227E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C0D5EB6423EEBE92000AB1B0 /* Build configuration list for PBXNativeTarget "FigmaGenDemoTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C0D5EB6223EEBE92000AB1B0 /* Debug */, + C0D5EB6323EEBE92000AB1B0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + C0E6B5F2238096C500083ADB /* Build configuration list for PBXProject "FigmaGenDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( - C09AD31B238FFE10005C787A /* Debug */, - C09AD31C238FFE10005C787A /* Release */, + C0E6B609238096C500083ADB /* Debug */, + C0E6B60A238096C500083ADB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C09AD31D238FFE10005C787A /* Build configuration list for PBXNativeTarget "FigmaGenDemo" */ = { + C0E6B60B238096C500083ADB /* Build configuration list for PBXNativeTarget "FigmaGenDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( - C09AD31E238FFE10005C787A /* Debug */, - C09AD31F238FFE10005C787A /* Release */, + C0E6B60C238096C500083ADB /* Debug */, + C0E6B60D238096C500083ADB /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; - rootObject = C09AD301238FFE0F005C787A /* Project object */; + rootObject = C0E6B5EF238096C500083ADB /* Project object */; } diff --git a/Demo/FigmaGenDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/FigmaGenDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata index b169fdd..7024d65 100644 --- a/Demo/FigmaGenDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/Demo/FigmaGenDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:Fugen Demo.xcodeproj"> diff --git a/Demo/FigmaGenDemo.xcodeproj/xcshareddata/IDETemplateMacros.plist b/Demo/FigmaGenDemo.xcodeproj/xcshareddata/IDETemplateMacros.plist deleted file mode 100644 index 91ef30b..0000000 --- a/Demo/FigmaGenDemo.xcodeproj/xcshareddata/IDETemplateMacros.plist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - FILEHEADER - -// ___PROJECTNAME___ -// Copyright © ___YEAR___ ___ORGANIZATIONNAME___ -// MIT Licence -// - - diff --git a/FigmaGen.xcodeproj/xcshareddata/xcschemes/FigmaGen Tests.xcscheme b/Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGen.xcscheme similarity index 62% rename from FigmaGen.xcodeproj/xcshareddata/xcschemes/FigmaGen Tests.xcscheme rename to Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGen.xcscheme index 4b55e1f..d057159 100644 --- a/FigmaGen.xcodeproj/xcshareddata/xcschemes/FigmaGen Tests.xcscheme +++ b/Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGen.xcscheme @@ -1,10 +1,26 @@ + + + + + + - - - - + + + + diff --git a/Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGenDemo.xcscheme b/Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGenDemo.xcscheme index 6378cde..3f7d7c1 100644 --- a/Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGenDemo.xcscheme +++ b/Demo/FigmaGenDemo.xcodeproj/xcshareddata/xcschemes/FigmaGenDemo.xcscheme @@ -1,6 +1,6 @@ @@ -28,6 +28,16 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -61,7 +71,7 @@ runnableDebuggingMode = "0"> diff --git a/FigmaGen.xcworkspace/contents.xcworkspacedata b/Demo/FigmaGenDemo.xcworkspace/contents.xcworkspacedata similarity index 78% rename from FigmaGen.xcworkspace/contents.xcworkspacedata rename to Demo/FigmaGenDemo.xcworkspace/contents.xcworkspacedata index 72c1d85..4a050d0 100644 --- a/FigmaGen.xcworkspace/contents.xcworkspacedata +++ b/Demo/FigmaGenDemo.xcworkspace/contents.xcworkspacedata @@ -2,7 +2,7 @@ + location = "group:FigmaGenDemo.xcodeproj"> diff --git a/FigmaGen.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/FigmaGenDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from FigmaGen.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to Demo/FigmaGenDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/Demo/FigmaGenDemo/AppDelegate.swift b/Demo/FigmaGenDemo/AppDelegate.swift index 07af35a..f4d3a1f 100644 --- a/Demo/FigmaGenDemo/AppDelegate.swift +++ b/Demo/FigmaGenDemo/AppDelegate.swift @@ -1,7 +1,9 @@ // -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence +// AppDelegate.swift +// FigmaGenDemo +// +// Created by Almaz Ibragimov on 16.11.2019. +// Copyright © 2019 Almaz Ibragimov. All rights reserved. // import UIKit @@ -11,7 +13,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { return true } } diff --git a/Demo/FigmaGenDemo/Generated/BaseColorTokens.swift b/Demo/FigmaGenDemo/Generated/BaseColorTokens.swift new file mode 100644 index 0000000..add81a6 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/BaseColorTokens.swift @@ -0,0 +1,494 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct BaseColorTokens { + /// #ffffff + public let colorsDefaultWhite = UIColor(hex: 0xFFFFFFFF) + /// #000000 + public let colorsDefaultBlack = UIColor(hex: 0x000000FF) + /// #00000000 + public let colorsDefaultTransparent = UIColor(hex: 0x00000000) + /// #f9fafb + public let colorsDefaultGray50 = UIColor(hex: 0xF9FAFBFF) + /// #f7fafc + public let colorsDefaultGray100 = UIColor(hex: 0xF7FAFCFF) + /// #edf2f7 + public let colorsDefaultGray200 = UIColor(hex: 0xEDF2F7FF) + /// #e2e8f0 + public let colorsDefaultGray300 = UIColor(hex: 0xE2E8F0FF) + /// #cbd5e0 + public let colorsDefaultGray400 = UIColor(hex: 0xCBD5E0FF) + /// #a0aec0 + public let colorsDefaultGray500 = UIColor(hex: 0xA0AEC0FF) + /// #718096 + public let colorsDefaultGray600 = UIColor(hex: 0x718096FF) + /// #4a5568 + public let colorsDefaultGray700 = UIColor(hex: 0x4A5568FF) + /// #2d3748 + public let colorsDefaultGray800 = UIColor(hex: 0x2D3748FF) + /// #1a202c + public let colorsDefaultGray900 = UIColor(hex: 0x1A202CFF) + /// #f8fafc + public let colorsSlate50 = UIColor(hex: 0xF8FAFCFF) + /// #f1f5f9 + public let colorsSlate100 = UIColor(hex: 0xF1F5F9FF) + /// #e2e8f0 + public let colorsSlate200 = UIColor(hex: 0xE2E8F0FF) + /// #cbd5e1 + public let colorsSlate300 = UIColor(hex: 0xCBD5E1FF) + /// #94a3b8 + public let colorsSlate400 = UIColor(hex: 0x94A3B8FF) + /// #64748b + public let colorsSlate500 = UIColor(hex: 0x64748BFF) + /// #475569 + public let colorsSlate600 = UIColor(hex: 0x475569FF) + /// #334155 + public let colorsSlate700 = UIColor(hex: 0x334155FF) + /// #1e293b + public let colorsSlate800 = UIColor(hex: 0x1E293BFF) + /// #0f172a + public let colorsSlate900 = UIColor(hex: 0x0F172AFF) + /// #f9fafb + public let colorsGray50 = UIColor(hex: 0xF9FAFBFF) + /// #f3f4f6 + public let colorsGray100 = UIColor(hex: 0xF3F4F6FF) + /// #e5e7eb + public let colorsGray200 = UIColor(hex: 0xE5E7EBFF) + /// #d1d5db + public let colorsGray300 = UIColor(hex: 0xD1D5DBFF) + /// #9ca3af + public let colorsGray400 = UIColor(hex: 0x9CA3AFFF) + /// #6b7280 + public let colorsGray500 = UIColor(hex: 0x6B7280FF) + /// #4b5563 + public let colorsGray600 = UIColor(hex: 0x4B5563FF) + /// #374151 + public let colorsGray700 = UIColor(hex: 0x374151FF) + /// #1f2937 + public let colorsGray800 = UIColor(hex: 0x1F2937FF) + /// #111827 + public let colorsGray900 = UIColor(hex: 0x111827FF) + /// #fafafa + public let colorsZinc50 = UIColor(hex: 0xFAFAFAFF) + /// #f4f4f5 + public let colorsZinc100 = UIColor(hex: 0xF4F4F5FF) + /// #e4e4e7 + public let colorsZinc200 = UIColor(hex: 0xE4E4E7FF) + /// #d4d4d8 + public let colorsZinc300 = UIColor(hex: 0xD4D4D8FF) + /// #a1a1aa + public let colorsZinc400 = UIColor(hex: 0xA1A1AAFF) + /// #71717a + public let colorsZinc500 = UIColor(hex: 0x71717AFF) + /// #52525b + public let colorsZinc600 = UIColor(hex: 0x52525BFF) + /// #3f3f46 + public let colorsZinc700 = UIColor(hex: 0x3F3F46FF) + /// #27272a + public let colorsZinc800 = UIColor(hex: 0x27272AFF) + /// #18181b + public let colorsZinc900 = UIColor(hex: 0x18181BFF) + /// #fafafa + public let colorsNeutral50 = UIColor(hex: 0xFAFAFAFF) + /// #f5f5f5 + public let colorsNeutral100 = UIColor(hex: 0xF5F5F5FF) + /// #e5e5e5 + public let colorsNeutral200 = UIColor(hex: 0xE5E5E5FF) + /// #d4d4d4 + public let colorsNeutral300 = UIColor(hex: 0xD4D4D4FF) + /// #a3a3a3 + public let colorsNeutral400 = UIColor(hex: 0xA3A3A3FF) + /// #737373 + public let colorsNeutral500 = UIColor(hex: 0x737373FF) + /// #525252 + public let colorsNeutral600 = UIColor(hex: 0x525252FF) + /// #404040 + public let colorsNeutral700 = UIColor(hex: 0x404040FF) + /// #262626 + public let colorsNeutral800 = UIColor(hex: 0x262626FF) + /// #171717 + public let colorsNeutral900 = UIColor(hex: 0x171717FF) + /// #fafaf9 + public let colorsStone50 = UIColor(hex: 0xFAFAF9FF) + /// #f5f5f4 + public let colorsStone100 = UIColor(hex: 0xF5F5F4FF) + /// #e7e5e4 + public let colorsStone200 = UIColor(hex: 0xE7E5E4FF) + /// #d6d3d1 + public let colorsStone300 = UIColor(hex: 0xD6D3D1FF) + /// #a8a29e + public let colorsStone400 = UIColor(hex: 0xA8A29EFF) + /// #78716c + public let colorsStone500 = UIColor(hex: 0x78716CFF) + /// #57534e + public let colorsStone600 = UIColor(hex: 0x57534EFF) + /// #44403c + public let colorsStone700 = UIColor(hex: 0x44403CFF) + /// #292524 + public let colorsStone800 = UIColor(hex: 0x292524FF) + /// #1c1917 + public let colorsStone900 = UIColor(hex: 0x1C1917FF) + /// #fef2f2 + public let colorsRed50 = UIColor(hex: 0xFEF2F2FF) + /// #fee2e2 + public let colorsRed100 = UIColor(hex: 0xFEE2E2FF) + /// #fecaca + public let colorsRed200 = UIColor(hex: 0xFECACAFF) + /// #fca5a5 + public let colorsRed300 = UIColor(hex: 0xFCA5A5FF) + /// #f87171 + public let colorsRed400 = UIColor(hex: 0xF87171FF) + /// #ef4444 + public let colorsRed500 = UIColor(hex: 0xEF4444FF) + /// #dc2626 + public let colorsRed600 = UIColor(hex: 0xDC2626FF) + /// #b91c1c + public let colorsRed700 = UIColor(hex: 0xB91C1CFF) + /// #991b1b + public let colorsRed800 = UIColor(hex: 0x991B1BFF) + /// #7f1d1d + public let colorsRed900 = UIColor(hex: 0x7F1D1DFF) + /// #fff7ed + public let colorsOrange50 = UIColor(hex: 0xFFF7EDFF) + /// #ffedd5 + public let colorsOrange100 = UIColor(hex: 0xFFEDD5FF) + /// #fed7aa + public let colorsOrange200 = UIColor(hex: 0xFED7AAFF) + /// #fdba74 + public let colorsOrange300 = UIColor(hex: 0xFDBA74FF) + /// #fb923c + public let colorsOrange400 = UIColor(hex: 0xFB923CFF) + /// #f97316 + public let colorsOrange500 = UIColor(hex: 0xF97316FF) + /// #ea580c + public let colorsOrange600 = UIColor(hex: 0xEA580CFF) + /// #c2410c + public let colorsOrange700 = UIColor(hex: 0xC2410CFF) + /// #9a3412 + public let colorsOrange800 = UIColor(hex: 0x9A3412FF) + /// #7c2d12 + public let colorsOrange900 = UIColor(hex: 0x7C2D12FF) + /// #fffbeb + public let colorsAmber50 = UIColor(hex: 0xFFFBEBFF) + /// #fef3c7 + public let colorsAmber100 = UIColor(hex: 0xFEF3C7FF) + /// #fde68a + public let colorsAmber200 = UIColor(hex: 0xFDE68AFF) + /// #fcd34d + public let colorsAmber300 = UIColor(hex: 0xFCD34DFF) + /// #fbbf24 + public let colorsAmber400 = UIColor(hex: 0xFBBF24FF) + /// #f59e0b + public let colorsAmber500 = UIColor(hex: 0xF59E0BFF) + /// #d97706 + public let colorsAmber600 = UIColor(hex: 0xD97706FF) + /// #b45309 + public let colorsAmber700 = UIColor(hex: 0xB45309FF) + /// #92400e + public let colorsAmber800 = UIColor(hex: 0x92400EFF) + /// #78350f + public let colorsAmber900 = UIColor(hex: 0x78350FFF) + /// #fefce8 + public let colorsYellow50 = UIColor(hex: 0xFEFCE8FF) + /// #fef9c3 + public let colorsYellow100 = UIColor(hex: 0xFEF9C3FF) + /// #fef08a + public let colorsYellow200 = UIColor(hex: 0xFEF08AFF) + /// #fde047 + public let colorsYellow300 = UIColor(hex: 0xFDE047FF) + /// #facc15 + public let colorsYellow400 = UIColor(hex: 0xFACC15FF) + /// #eab308 + public let colorsYellow500 = UIColor(hex: 0xEAB308FF) + /// #ca8a04 + public let colorsYellow600 = UIColor(hex: 0xCA8A04FF) + /// #a16207 + public let colorsYellow700 = UIColor(hex: 0xA16207FF) + /// #854d0e + public let colorsYellow800 = UIColor(hex: 0x854D0EFF) + /// #713f12 + public let colorsYellow900 = UIColor(hex: 0x713F12FF) + /// #f7fee7 + public let colorsLime50 = UIColor(hex: 0xF7FEE7FF) + /// #ecfccb + public let colorsLime100 = UIColor(hex: 0xECFCCBFF) + /// #d9f99d + public let colorsLime200 = UIColor(hex: 0xD9F99DFF) + /// #bef264 + public let colorsLime300 = UIColor(hex: 0xBEF264FF) + /// #a3e635 + public let colorsLime400 = UIColor(hex: 0xA3E635FF) + /// #84cc16 + public let colorsLime500 = UIColor(hex: 0x84CC16FF) + /// #65a30d + public let colorsLime600 = UIColor(hex: 0x65A30DFF) + /// #4d7c0f + public let colorsLime700 = UIColor(hex: 0x4D7C0FFF) + /// #3f6212 + public let colorsLime800 = UIColor(hex: 0x3F6212FF) + /// #365314 + public let colorsLime900 = UIColor(hex: 0x365314FF) + /// #f0fdf4 + public let colorsGreen50 = UIColor(hex: 0xF0FDF4FF) + /// #dcfce7 + public let colorsGreen100 = UIColor(hex: 0xDCFCE7FF) + /// #bbf7d0 + public let colorsGreen200 = UIColor(hex: 0xBBF7D0FF) + /// #86efac + public let colorsGreen300 = UIColor(hex: 0x86EFACFF) + /// #4ade80 + public let colorsGreen400 = UIColor(hex: 0x4ADE80FF) + /// #22c55e + public let colorsGreen500 = UIColor(hex: 0x22C55EFF) + /// #16a34a + public let colorsGreen600 = UIColor(hex: 0x16A34AFF) + /// #15803d + public let colorsGreen700 = UIColor(hex: 0x15803DFF) + /// #166534 + public let colorsGreen800 = UIColor(hex: 0x166534FF) + /// #14532d + public let colorsGreen900 = UIColor(hex: 0x14532DFF) + /// #ecfdf5 + public let colorsEmerald50 = UIColor(hex: 0xECFDF5FF) + /// #d1fae5 + public let colorsEmerald100 = UIColor(hex: 0xD1FAE5FF) + /// #a7f3d0 + public let colorsEmerald200 = UIColor(hex: 0xA7F3D0FF) + /// #6ee7b7 + public let colorsEmerald300 = UIColor(hex: 0x6EE7B7FF) + /// #34d399 + public let colorsEmerald400 = UIColor(hex: 0x34D399FF) + /// #10b981 + public let colorsEmerald500 = UIColor(hex: 0x10B981FF) + /// #059669 + public let colorsEmerald600 = UIColor(hex: 0x059669FF) + /// #047857 + public let colorsEmerald700 = UIColor(hex: 0x047857FF) + /// #065f46 + public let colorsEmerald800 = UIColor(hex: 0x065F46FF) + /// #064e3b + public let colorsEmerald900 = UIColor(hex: 0x064E3BFF) + /// #f0fdfa + public let colorsTeal50 = UIColor(hex: 0xF0FDFAFF) + /// #ccfbf1 + public let colorsTeal100 = UIColor(hex: 0xCCFBF1FF) + /// #99f6e4 + public let colorsTeal200 = UIColor(hex: 0x99F6E4FF) + /// #5eead4 + public let colorsTeal300 = UIColor(hex: 0x5EEAD4FF) + /// #2dd4bf + public let colorsTeal400 = UIColor(hex: 0x2DD4BFFF) + /// #14b8a6 + public let colorsTeal500 = UIColor(hex: 0x14B8A6FF) + /// #0d9488 + public let colorsTeal600 = UIColor(hex: 0x0D9488FF) + /// #0f766e + public let colorsTeal700 = UIColor(hex: 0x0F766EFF) + /// #115e59 + public let colorsTeal800 = UIColor(hex: 0x115E59FF) + /// #134e4a + public let colorsTeal900 = UIColor(hex: 0x134E4AFF) + /// #ecfeff + public let colorsCyan50 = UIColor(hex: 0xECFEFFFF) + /// #cffafe + public let colorsCyan100 = UIColor(hex: 0xCFFAFEFF) + /// #a5f3fc + public let colorsCyan200 = UIColor(hex: 0xA5F3FCFF) + /// #67e8f9 + public let colorsCyan300 = UIColor(hex: 0x67E8F9FF) + /// #22d3ee + public let colorsCyan400 = UIColor(hex: 0x22D3EEFF) + /// #06b6d4 + public let colorsCyan500 = UIColor(hex: 0x06B6D4FF) + /// #0891b2 + public let colorsCyan600 = UIColor(hex: 0x0891B2FF) + /// #0e7490 + public let colorsCyan700 = UIColor(hex: 0x0E7490FF) + /// #155e75 + public let colorsCyan800 = UIColor(hex: 0x155E75FF) + /// #164e63 + public let colorsCyan900 = UIColor(hex: 0x164E63FF) + /// #f0f9ff + public let colorsSky50 = UIColor(hex: 0xF0F9FFFF) + /// #e0f2fe + public let colorsSky100 = UIColor(hex: 0xE0F2FEFF) + /// #bae6fd + public let colorsSky200 = UIColor(hex: 0xBAE6FDFF) + /// #7dd3fc + public let colorsSky300 = UIColor(hex: 0x7DD3FCFF) + /// #38bdf8 + public let colorsSky400 = UIColor(hex: 0x38BDF8FF) + /// #0ea5e9 + public let colorsSky500 = UIColor(hex: 0x0EA5E9FF) + /// #0284c7 + public let colorsSky600 = UIColor(hex: 0x0284C7FF) + /// #0369a1 + public let colorsSky700 = UIColor(hex: 0x0369A1FF) + /// #075985 + public let colorsSky800 = UIColor(hex: 0x075985FF) + /// #0c4a6e + public let colorsSky900 = UIColor(hex: 0x0C4A6EFF) + /// #eff6ff + public let colorsBlue50 = UIColor(hex: 0xEFF6FFFF) + /// #dbeafe + public let colorsBlue100 = UIColor(hex: 0xDBEAFEFF) + /// #bfdbfe + public let colorsBlue200 = UIColor(hex: 0xBFDBFEFF) + /// #93c5fd + public let colorsBlue300 = UIColor(hex: 0x93C5FDFF) + /// #60a5fa + public let colorsBlue400 = UIColor(hex: 0x60A5FAFF) + /// #3b82f6 + public let colorsBlue500 = UIColor(hex: 0x3B82F6FF) + /// #2563eb + public let colorsBlue600 = UIColor(hex: 0x2563EBFF) + /// #1d4ed8 + public let colorsBlue700 = UIColor(hex: 0x1D4ED8FF) + /// #1e40af + public let colorsBlue800 = UIColor(hex: 0x1E40AFFF) + /// #1e3a8a + public let colorsBlue900 = UIColor(hex: 0x1E3A8AFF) + /// #eef2ff + public let colorsIndigo50 = UIColor(hex: 0xEEF2FFFF) + /// #e0e7ff + public let colorsIndigo100 = UIColor(hex: 0xE0E7FFFF) + /// #c7d2fe + public let colorsIndigo200 = UIColor(hex: 0xC7D2FEFF) + /// #a5b4fc + public let colorsIndigo300 = UIColor(hex: 0xA5B4FCFF) + /// #818cf8 + public let colorsIndigo400 = UIColor(hex: 0x818CF8FF) + /// #6366f1 + public let colorsIndigo500 = UIColor(hex: 0x6366F1FF) + /// #4f46e5 + public let colorsIndigo600 = UIColor(hex: 0x4F46E5FF) + /// #4338ca + public let colorsIndigo700 = UIColor(hex: 0x4338CAFF) + /// #3730a3 + public let colorsIndigo800 = UIColor(hex: 0x3730A3FF) + /// #312e81 + public let colorsIndigo900 = UIColor(hex: 0x312E81FF) + /// #f5f3ff + public let colorsViolet50 = UIColor(hex: 0xF5F3FFFF) + /// #ede9fe + public let colorsViolet100 = UIColor(hex: 0xEDE9FEFF) + /// #ddd6fe + public let colorsViolet200 = UIColor(hex: 0xDDD6FEFF) + /// #c4b5fd + public let colorsViolet300 = UIColor(hex: 0xC4B5FDFF) + /// #a78bfa + public let colorsViolet400 = UIColor(hex: 0xA78BFAFF) + /// #8b5cf6 + public let colorsViolet500 = UIColor(hex: 0x8B5CF6FF) + /// #7c3aed + public let colorsViolet600 = UIColor(hex: 0x7C3AEDFF) + /// #6d28d9 + public let colorsViolet700 = UIColor(hex: 0x6D28D9FF) + /// #5b21b6 + public let colorsViolet800 = UIColor(hex: 0x5B21B6FF) + /// #4c1d95 + public let colorsViolet900 = UIColor(hex: 0x4C1D95FF) + /// #faf5ff + public let colorsPurple50 = UIColor(hex: 0xFAF5FFFF) + /// #f3e8ff + public let colorsPurple100 = UIColor(hex: 0xF3E8FFFF) + /// #e9d5ff + public let colorsPurple200 = UIColor(hex: 0xE9D5FFFF) + /// #d8b4fe + public let colorsPurple300 = UIColor(hex: 0xD8B4FEFF) + /// #c084fc + public let colorsPurple400 = UIColor(hex: 0xC084FCFF) + /// #a855f7 + public let colorsPurple500 = UIColor(hex: 0xA855F7FF) + /// #9333ea + public let colorsPurple600 = UIColor(hex: 0x9333EAFF) + /// #7e22ce + public let colorsPurple700 = UIColor(hex: 0x7E22CEFF) + /// #6b21a8 + public let colorsPurple800 = UIColor(hex: 0x6B21A8FF) + /// #581c87 + public let colorsPurple900 = UIColor(hex: 0x581C87FF) + /// #fdf4ff + public let colorsFuschia50 = UIColor(hex: 0xFDF4FFFF) + /// #fae8ff + public let colorsFuschia100 = UIColor(hex: 0xFAE8FFFF) + /// #f5d0fe + public let colorsFuschia200 = UIColor(hex: 0xF5D0FEFF) + /// #f0abfc + public let colorsFuschia300 = UIColor(hex: 0xF0ABFCFF) + /// #e879f9 + public let colorsFuschia400 = UIColor(hex: 0xE879F9FF) + /// #d946ef + public let colorsFuschia500 = UIColor(hex: 0xD946EFFF) + /// #c026d3 + public let colorsFuschia600 = UIColor(hex: 0xC026D3FF) + /// #a21caf + public let colorsFuschia700 = UIColor(hex: 0xA21CAFFF) + /// #86198f + public let colorsFuschia800 = UIColor(hex: 0x86198FFF) + /// #701a75 + public let colorsFuschia900 = UIColor(hex: 0x701A75FF) + /// #fdf2f8 + public let colorsPink50 = UIColor(hex: 0xFDF2F8FF) + /// #fce7f3 + public let colorsPink100 = UIColor(hex: 0xFCE7F3FF) + /// #fbcfe8 + public let colorsPink200 = UIColor(hex: 0xFBCFE8FF) + /// #f9a8d4 + public let colorsPink300 = UIColor(hex: 0xF9A8D4FF) + /// #f472b6 + public let colorsPink400 = UIColor(hex: 0xF472B6FF) + /// #ec4899 + public let colorsPink500 = UIColor(hex: 0xEC4899FF) + /// #db2777 + public let colorsPink600 = UIColor(hex: 0xDB2777FF) + /// #be185d + public let colorsPink700 = UIColor(hex: 0xBE185DFF) + /// #9d174d + public let colorsPink800 = UIColor(hex: 0x9D174DFF) + /// #831843 + public let colorsPink900 = UIColor(hex: 0x831843FF) + /// #fff1f2 + public let colorsRose50 = UIColor(hex: 0xFFF1F2FF) + /// #ffe4e6 + public let colorsRose100 = UIColor(hex: 0xFFE4E6FF) + /// #fecdd3 + public let colorsRose200 = UIColor(hex: 0xFECDD3FF) + /// #fda4af + public let colorsRose300 = UIColor(hex: 0xFDA4AFFF) + /// #fb7185 + public let colorsRose400 = UIColor(hex: 0xFB7185FF) + /// #f43f5e + public let colorsRose500 = UIColor(hex: 0xF43F5EFF) + /// #e11d48 + public let colorsRose600 = UIColor(hex: 0xE11D48FF) + /// #be123c + public let colorsRose700 = UIColor(hex: 0xBE123CFF) + /// #9f1239 + public let colorsRose800 = UIColor(hex: 0x9F1239FF) + /// #881337 + public let colorsRose900 = UIColor(hex: 0x881337FF) +} + +private extension UIColor { + + convenience init(hex: UInt32) { + let red = UInt8((hex >> 24) & 0xFF) + let green = UInt8((hex >> 16) & 0xFF) + let blue = UInt8((hex >> 8) & 0xFF) + let alpha = UInt8(hex & 0xFF) + + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: CGFloat(alpha) / 255.0 + ) + } +} diff --git a/Demo/FigmaGenDemo/Generated/BorderTokens.swift b/Demo/FigmaGenDemo/Generated/BorderTokens.swift new file mode 100644 index 0000000..93c9748 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/BorderTokens.swift @@ -0,0 +1,3 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen +// No border tokens found diff --git a/Demo/FigmaGenDemo/Generated/BoxShadowTokens.swift b/Demo/FigmaGenDemo/Generated/BoxShadowTokens.swift new file mode 100644 index 0000000..99d68fc --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/BoxShadowTokens.swift @@ -0,0 +1,105 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct BoxShadowTokens { + + /// - hh-day: + /// Offset: x 0; y 4 + /// Radius: 12 + /// Color: #7090b029 + /// - hh-night: + /// Offset: x 0; y 4 + /// Radius: 12 + /// Color: #7090b029 + public let level1: ShadowToken + + /// - hh-day: + /// Offset: x 0; y 8 + /// Radius: 16 + /// Color: #7090b03d + /// - hh-night: + /// Offset: x 0; y 8 + /// Radius: 16 + /// Color: #7090b03d + public let level2: ShadowToken + + /// - hh-day: + /// Offset: x 0; y 12 + /// Radius: 24 + /// Color: #7090b052 + /// - hh-night: + /// Offset: x 0; y 12 + /// Radius: 24 + /// Color: #7090b052 + public let level3: ShadowToken +} + +public struct ShadowToken: Equatable { + + // MARK: - Instance Properties + + public let offset: CGSize + public let radius: CGFloat + public let color: UIColor? + public let opacity: Float + + // MARK: - Initializers + + public init( + offset: CGSize = CGSize(width: 0, height: -3), + radius: CGFloat = 3.0, + color: UIColor? = .black, + opacity: Float = 0.0 + ) { + self.offset = offset + self.radius = radius + self.color = color + self.opacity = opacity + } +} + +public extension CALayer { + + // MARK: - Instance Properties + + var shadowToken: ShadowToken { + get { + ShadowToken( + offset: shadowOffset, + radius: shadowRadius, + color: shadowColor.map(UIColor.init(cgColor:)), + opacity: shadowOpacity + ) + } + + set { + shadowOffset = newValue.offset + shadowRadius = newValue.radius + shadowColor = newValue.color?.cgColor + shadowOpacity = newValue.opacity + } + } + + // MARK: - Initializers + + convenience init(shadowToken: ShadowToken) { + self.init() + + self.shadowToken = shadowToken + } +} + +public extension UIView { + + // MARK: - Instance Properties + + var shadowToken: ShadowToken { + get { layer.shadowToken } + set { layer.shadowToken = newValue } + } +} diff --git a/Demo/FigmaGenDemo/Generated/ColorStyle.swift b/Demo/FigmaGenDemo/Generated/ColorStyle.swift new file mode 100644 index 0000000..b0284ec --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/ColorStyle.swift @@ -0,0 +1,160 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct ColorStyle: Equatable { + + // MARK: - Type Properties + + /// Whisper + /// + /// Hex #E9E9E9FF; rgba 233 233 233, 100% + public static let whisper = ColorStyle( + red: 0.9137254953384399, + green: 0.9137254953384399, + blue: 0.9137254953384399, + alpha: 1.0 + ) + + /// Snow Drift + /// + /// Hex #DADAD9FF; rgba 218 218 217, 100% + public static let snowDrift = ColorStyle( + red: 0.8549019694328308, + green: 0.8549019694328308, + blue: 0.8509804010391235, + alpha: 1.0 + ) + + /// Submarine + /// + /// Hex #949798FF; rgba 148 151 152, 100% + public static let submarine = ColorStyle( + red: 0.5803921818733215, + green: 0.5921568870544434, + blue: 0.5960784554481506, + alpha: 1.0 + ) + + /// Eclipse + /// + /// Hex #393939FF; rgba 57 57 57, 100% + public static let eclipse = ColorStyle( + red: 0.2235294133424759, + green: 0.2235294133424759, + blue: 0.2235294133424759, + alpha: 1.0 + ) + + /// Lochinvar + /// + /// Hex #42967DFF; rgba 66 150 125, 100% + public static let lochinvar = ColorStyle( + red: 0.25882354378700256, + green: 0.5882353186607361, + blue: 0.4901960790157318, + alpha: 1.0 + ) + + /// Jelly Bean + /// + /// Hex #427D96FF; rgba 66 125 150, 100% + public static let jellyBean = ColorStyle( + red: 0.25882354378700256, + green: 0.4901960790157318, + blue: 0.5882353186607361, + alpha: 1.0 + ) + + /// Daisy Bush + /// + /// Hex #5B4296BF; rgba 91 66 150, 75% + public static let daisyBush = ColorStyle( + red: 0.35686275362968445, + green: 0.25882354378700256, + blue: 0.5882353186607361, + alpha: 0.75 + ) + + /// Razzmatazz + /// + /// Hex #E30B5CFF; rgba 227 11 92, 100% + public static let razzmatazz = ColorStyle( + red: 0.8901960849761963, + green: 0.04313725605607033, + blue: 0.3607843220233917, + alpha: 1.0 + ) + + // MARK: - Instance Properties + + public let red: CGFloat + public let green: CGFloat + public let blue: CGFloat + public let alpha: CGFloat + + public var color: UIColor { + return UIColor(style: self) + } + + // MARK: - Initializers + + public init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + } + + // MARK: - Instance Methods + + public func withRed(_ red: CGFloat) -> ColorStyle { + return ColorStyle( + red: red, + green: green, + blue: blue, + alpha: alpha + ) + } + + public func withGreen(_ green: CGFloat) -> ColorStyle { + return ColorStyle( + red: red, + green: green, + blue: blue, + alpha: alpha + ) + } + + public func withBlue(_ blue: CGFloat) -> ColorStyle { + return ColorStyle( + red: red, + green: green, + blue: blue, + alpha: alpha + ) + } + + public func withAlpha(_ alpha: CGFloat) -> ColorStyle { + return ColorStyle( + red: red, + green: green, + blue: blue, + alpha: alpha + ) + } +} + +public extension UIColor { + + // MARK: - Initializers + + convenience init(style: ColorStyle) { + self.init(red: style.red, green: style.green, blue: style.blue, alpha: style.alpha) + } +} diff --git a/Demo/FigmaGenDemo/Generated/ColorTokens.swift b/Demo/FigmaGenDemo/Generated/ColorTokens.swift new file mode 100644 index 0000000..5d84619 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/ColorTokens.swift @@ -0,0 +1,77 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct ColorTokens { + public struct Accent { + /// accent.bg + /// + /// hh-day: #c3dafe + /// hh-night: #434190 + public let bg: UIColor + /// accent.default + /// + /// hh-day: #7f9cf5 + /// hh-night: #5a67d8 + public let `default`: UIColor + /// accent.onAccent + /// + /// hh-day: #ffffff + /// hh-night: #ffffff + public let onAccent: UIColor + } + + public let accent: Accent + public struct Bg { + /// bg.default + /// + /// hh-day: #ffffff + /// hh-night: #1a202c + public let `default`: UIColor + /// bg.muted + /// + /// hh-day: #f7fafc + /// hh-night: #4a5568 + public let muted: UIColor + /// bg.subtle + /// + /// hh-day: #edf2f7 + /// hh-night: #718096 + public let subtle: UIColor + } + + public let bg: Bg + public struct Fg { + /// fg.default + /// + /// hh-day: #000000 + /// hh-night: #ffffff + public let `default`: UIColor + /// fg.muted + /// + /// hh-day: #4a5568 + /// hh-night: #e2e8f0 + public let muted: UIColor + /// fg.subtle + /// + /// hh-day: #a0aec0 + /// hh-night: #a0aec0 + public let subtle: UIColor + } + + public let fg: Fg + public struct Shadows { + /// shadows.default + /// + /// hh-day: #1a202c + /// hh-night: #00000000 + public let `default`: UIColor + } + + public let shadows: Shadows +} diff --git a/Demo/FigmaGenDemo/Generated/Colors.swift b/Demo/FigmaGenDemo/Generated/Colors.swift deleted file mode 100644 index 218f83f..0000000 --- a/Demo/FigmaGenDemo/Generated/Colors.swift +++ /dev/null @@ -1,83 +0,0 @@ -// swiftlint:disable all -import UIKit.UIColor - -public enum Colors { - - /// Gray - /// - /// Hex: #A9A9A9FF; rgba: 169 169 169, 100%. - public static let gray = UIColor(rgbaHex: 0xA9A9A9FF) - - /// Black - /// - /// Hex: #313033FF; rgba: 49 48 51, 100%. - public static let black = UIColor(rgbaHex: 0x313033FF) - - /// Slate Gray - /// - /// Hex: #708090FF; rgba: 112 128 144, 100%. - public static let slateGray = UIColor(rgbaHex: 0x708090FF) - - /// Dark Gray - /// - /// Hex: #696969FF; rgba: 105 105 105, 100%. - public static let darkGray = UIColor(rgbaHex: 0x696969FF) - - /// Light Gray - /// - /// Hex: #D3D3D3FF; rgba: 211 211 211, 100%. - public static let lightGray = UIColor(rgbaHex: 0xD3D3D3FF) - - /// White Smoke - /// - /// Hex: #F5F6F6FF; rgba: 245 246 246, 100%. - public static let whiteSmoke = UIColor(rgbaHex: 0xF5F6F6FF) - - /// White - /// - /// Hex: #FDFDFFFF; rgba: 253 253 255, 100%. - public static let white = UIColor(rgbaHex: 0xFDFDFFFF) - - /// Jungle - /// - /// Hex: #29AB87FF; rgba: 41 171 135, 100%. - public static let jungle = UIColor(rgbaHex: 0x29AB87FF) - - /// Fuchsia - /// - /// Hex: #E958A7FF; rgba: 233 88 167, 100%. - public static let fuchsia = UIColor(rgbaHex: 0xE958A7FF) - - /// Bumblebee - /// - /// Hex: #FCE205FF; rgba: 252 226 5, 100%. - public static let bumblebee = UIColor(rgbaHex: 0xFCE205FF) - - /// Electric - /// - /// Hex: #8F00FFFF; rgba: 143 0 255, 100%. - public static let electric = UIColor(rgbaHex: 0x8F00FFFF) - - /// Carolina - /// - /// Hex: #57A0D2FF; rgba: 87 160 210, 100%. - public static let carolina = UIColor(rgbaHex: 0x57A0D2FF) - - /// Imperial - /// - /// Hex: #ED2939FF; rgba: 237 41 57, 100%. - public static let imperial = UIColor(rgbaHex: 0xED2939FF) -} - -private extension UIColor { - - convenience init(rgbaHex: UInt32) { - self.init( - red: CGFloat((rgbaHex >> 24) & 0xFF) / 255.0, - green: CGFloat((rgbaHex >> 16) & 0xFF) / 255.0, - blue: CGFloat((rgbaHex >> 8) & 0xFF) / 255.0, - alpha: CGFloat(rgbaHex & 0xFF) / 255.0 - ) - } -} -// swiftlint:enable all diff --git a/Demo/FigmaGenDemo/Generated/FontFamilyTokens.swift b/Demo/FigmaGenDemo/Generated/FontFamilyTokens.swift new file mode 100644 index 0000000..2aff207 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/FontFamilyTokens.swift @@ -0,0 +1,21 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen +public struct FontFamilyTokens { + + public struct Heading { + public let headingRegular = "Inter-Regular" + public let headingBold = "Inter-Bold" + public let bodyRegular = "Inter-Regular" + public let bodyBold = "Inter-Bold" + } + + public struct Body { + public let headingRegular = "Roboto-Regular" + public let headingBold = "Roboto-Bold" + public let bodyRegular = "Roboto-Regular" + public let bodyBold = "Roboto-Bold" + } + + public let heading = Heading() + public let body = Body() +} diff --git a/Demo/FigmaGenDemo/Generated/Images.swift b/Demo/FigmaGenDemo/Generated/Images.swift new file mode 100644 index 0000000..07cf973 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/Images.swift @@ -0,0 +1,345 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public enum Images { + + // MARK: - Nested Types + + public enum ValidationError: Error, CustomStringConvertible { + case assetNotFound(name: String) + case resourceNotFound(name: String) + + public var description: String { + switch self { + case let .assetNotFound(name): + return "Image asset '\(name)' couldn't be loaded" + + case let .resourceNotFound(name): + return "Image resource file '\(name)' couldn't be loaded" + } + } + } + + public enum InterfaceEssentialBehance { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialBehanceStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialBehanceStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialBehanceStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialBehanceStyleOutlined")! + } + } + + public enum InterfaceEssentialDribbble { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialDribbbleStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialDribbbleStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialDribbbleStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialDribbbleStyleOutlined")! + } + } + + public enum InterfaceEssentialFacebook { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialFacebookStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialFacebookStyleFilled")! + } + + /// Outlined + /// + /// Asset: InterfaceEssentialFacebookOutlined + public static var outlined: UIImage { + return UIImage(named: "InterfaceEssentialFacebookOutlined")! + } + } + + public enum InterfaceEssentialFigma { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialFigmaStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialFigmaStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialFigmaStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialFigmaStyleOutlined")! + } + } + + public enum InterfaceEssentialGoogle { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialGoogleStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialGoogleStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialGoogleStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialGoogleStyleOutlined")! + } + } + + public enum InterfaceEssentialInstagram { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialInstagramStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialInstagramStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialInstagramStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialInstagramStyleOutlined")! + } + } + + public enum InterfaceEssentialLinkedin { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialLinkedinStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialLinkedinStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialLinkedinStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialLinkedinStyleOutlined")! + } + } + + public enum InterfaceEssentialTwitter { + + /// Style=Filled + /// + /// Asset: InterfaceEssentialTwitterStyleFilled + public static var styleFilled: UIImage { + return UIImage(named: "InterfaceEssentialTwitterStyleFilled")! + } + + /// Style=Outlined + /// + /// Asset: InterfaceEssentialTwitterStyleOutlined + public static var styleOutlined: UIImage { + return UIImage(named: "InterfaceEssentialTwitterStyleOutlined")! + } + } + + // MARK: - Type Properties + + /// Cloud + /// + /// Asset: Cloud + public static var cloud: UIImage { + return UIImage(named: "Cloud")! + } + + /// Geo + /// + /// Asset: Geo + public static var geo: UIImage { + return UIImage(named: "Geo")! + } + + /// Phone + /// + /// Asset: Phone + public static var phone: UIImage { + return UIImage(named: "Phone")! + } + + /// Share + /// + /// Asset: Share + public static var share: UIImage { + return UIImage(named: "Share")! + } + + /// Snapchat + /// + /// Asset: Snapchat + public static var snapchat: UIImage { + return UIImage(named: "Snapchat")! + } + + /// Star + /// + /// Asset: Star + public static var star: UIImage { + return UIImage(named: "Star")! + } + + /// Telegram + /// + /// Asset: Telegram + public static var telegram: UIImage { + return UIImage(named: "Telegram")! + } + + /// Viber + /// + /// Asset: Viber + public static var viber: UIImage { + return UIImage(named: "Viber")! + } + + /// WeChat + /// + /// Asset: WeChat + public static var weChat: UIImage { + return UIImage(named: "WeChat")! + } + + /// WhatsApp + /// + /// Asset: WhatsApp + public static var whatsApp: UIImage { + return UIImage(named: "WhatsApp")! + } + + // MARK: - Type Methods + + public static func validate() throws { + guard UIImage(named: "Cloud") != nil else { + throw ValidationError.assetNotFound(name: "Cloud") + } + + guard UIImage(named: "Geo") != nil else { + throw ValidationError.assetNotFound(name: "Geo") + } + + guard UIImage(named: "InterfaceEssentialBehanceStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialBehanceStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialBehanceStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialBehanceStyleOutlined") + } + + guard UIImage(named: "InterfaceEssentialDribbbleStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialDribbbleStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialDribbbleStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialDribbbleStyleOutlined") + } + + guard UIImage(named: "InterfaceEssentialFacebookStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialFacebookStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialFacebookOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialFacebookOutlined") + } + + guard UIImage(named: "InterfaceEssentialFigmaStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialFigmaStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialFigmaStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialFigmaStyleOutlined") + } + + guard UIImage(named: "InterfaceEssentialGoogleStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialGoogleStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialGoogleStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialGoogleStyleOutlined") + } + + guard UIImage(named: "InterfaceEssentialInstagramStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialInstagramStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialInstagramStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialInstagramStyleOutlined") + } + + guard UIImage(named: "InterfaceEssentialLinkedinStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialLinkedinStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialLinkedinStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialLinkedinStyleOutlined") + } + + guard UIImage(named: "InterfaceEssentialTwitterStyleFilled") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialTwitterStyleFilled") + } + + guard UIImage(named: "InterfaceEssentialTwitterStyleOutlined") != nil else { + throw ValidationError.assetNotFound(name: "InterfaceEssentialTwitterStyleOutlined") + } + + guard UIImage(named: "Phone") != nil else { + throw ValidationError.assetNotFound(name: "Phone") + } + + guard UIImage(named: "Share") != nil else { + throw ValidationError.assetNotFound(name: "Share") + } + + guard UIImage(named: "Snapchat") != nil else { + throw ValidationError.assetNotFound(name: "Snapchat") + } + + guard UIImage(named: "Star") != nil else { + throw ValidationError.assetNotFound(name: "Star") + } + + guard UIImage(named: "Telegram") != nil else { + throw ValidationError.assetNotFound(name: "Telegram") + } + + guard UIImage(named: "Viber") != nil else { + throw ValidationError.assetNotFound(name: "Viber") + } + + guard UIImage(named: "WeChat") != nil else { + throw ValidationError.assetNotFound(name: "WeChat") + } + + guard UIImage(named: "WhatsApp") != nil else { + throw ValidationError.assetNotFound(name: "WhatsApp") + } + + print("All images are valid") + } +} diff --git a/Demo/FigmaGenDemo/Generated/ShadowStyle.swift b/Demo/FigmaGenDemo/Generated/ShadowStyle.swift new file mode 100644 index 0000000..1e61170 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/ShadowStyle.swift @@ -0,0 +1,359 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct Shadow: Equatable { + + // MARK: - Type Properties + + public static let clear = Shadow() + + /// Ugly Shadow 1 + /// + /// Offset: x 2.0; y 2.0 + /// Radius: 8.0 + /// Color: hex #19193219; rgba 25 25 50, 10% + /// Opacity: 0.10000000149011612 + public static let uglyShadow1 = Shadow( + offset: CGSize(width: 2.0, height: 2.0), + radius: 8.0, + color: UIColor( + red: 0.09803921729326245, + green: 0.09803921729326245, + blue: 0.19607843458652496, + alpha: 1.0 + ), + opacity: 0.10000000149011612 + ) + + /// Card Shadow 1 + /// + /// Offset: x 2.0; y 8.0 + /// Radius: 12.0 + /// Color: hex #32324619; rgba 50 50 70, 10% + /// Opacity: 0.10000000149011612 + public static let cardShadow1 = Shadow( + offset: CGSize(width: 2.0, height: 8.0), + radius: 12.0, + color: UIColor( + red: 0.19607843458652496, + green: 0.19607843458652496, + blue: 0.27450981736183167, + alpha: 1.0 + ), + opacity: 0.10000000149011612 + ) + + /// Card Shadow 2 + /// + /// Offset: x 0.0; y 0.0 + /// Radius: 4.0 + /// Color: hex #19192319; rgba 25 25 35, 10% + /// Opacity: 0.10000000149011612 + public static let cardShadow2 = Shadow( + offset: CGSize(width: 0.0, height: 0.0), + radius: 4.0, + color: UIColor( + red: 0.09803921729326245, + green: 0.09803921729326245, + blue: 0.13725490868091583, + alpha: 1.0 + ), + opacity: 0.10000000149011612 + ) + + /// Thin Shadow + /// + /// Offset: x 0.0; y 1.0 + /// Radius: 4.0 + /// Color: hex #19193219; rgba 25 25 50, 10% + /// Opacity: 0.10000000149011612 + public static let thinShadow = Shadow( + offset: CGSize(width: 0.0, height: 1.0), + radius: 4.0, + color: UIColor( + red: 0.09803921729326245, + green: 0.09803921729326245, + blue: 0.19607843458652496, + alpha: 1.0 + ), + opacity: 0.10000000149011612 + ) + + // MARK: - Instance Properties + + public let offset: CGSize + public let radius: CGFloat + public let color: UIColor? + public let opacity: Float + + // MARK: - Initializers + + public init( + offset: CGSize = CGSize(width: 0, height: -3), + radius: CGFloat = 3.0, + color: UIColor? = .black, + opacity: Float = 0.0 + ) { + self.offset = offset + self.radius = radius + self.color = color + self.opacity = opacity + } +} + +public struct ShadowStyle { + + // MARK: - Type Properties + + public static let clear = ShadowStyle() + + /// Ugly Shadow + public static let uglyShadow = ShadowStyle( + shadows: [ + .uglyShadow1 + ] + ) + + /// Field Shadow + public static let fieldShadow = ShadowStyle( + shadows: [ + ] + ) + + /// Card Shadow + public static let cardShadow = ShadowStyle( + shadows: [ + .cardShadow1, + .cardShadow2 + ] + ) + + /// Thin Shadow + public static let thinShadow = ShadowStyle( + shadows: [ + .thinShadow + ] + ) + + // MARK: - Instance Properties + + public let shadows: [Shadow] + + // MARK: - Initializers + + public init(shadows: [Shadow] = []) { + self.shadows = shadows + } +} + +public extension CALayer { + + // MARK: - Instance Properties + + var shadow: Shadow { + get { + Shadow( + offset: shadowOffset, + radius: shadowRadius, + color: shadowColor.map(UIColor.init(cgColor:)), + opacity: shadowOpacity + ) + } + + set { + shadowOffset = newValue.offset + shadowRadius = newValue.radius + shadowColor = newValue.color?.cgColor + shadowOpacity = newValue.opacity + } + } + + // MARK: - Initializers + + convenience init(shadow: Shadow) { + self.init() + + self.shadow = shadow + } +} + +public extension UIView { + + // MARK: - Instance Properties + + var shadow: Shadow { + get { layer.shadow } + set { layer.shadow = newValue } + } +} + +private extension UIBezierPath { + + // MARK: - Initializers + + convenience init( + roundedRect rect: CGRect, + byRoundingCorners layerCorners: CACornerMask, + cornerRadii: CGSize + ) { + #if canImport(UIKit) + let cornerMaskMap: KeyValuePairs = [ + .layerMinXMinYCorner: .topLeft, + .layerMinXMaxYCorner: .bottomLeft, + .layerMaxXMinYCorner: .topRight, + .layerMaxXMaxYCorner: .bottomRight + ] + + let rectCorners = cornerMaskMap + .lazy + .filter { layerCorners.contains($0.key) } + .reduce(into: UIRectCorner()) { result, corner in + result.insert(corner.value) + } + + self.init( + roundedRect: rect, + byRoundingCorners: rectCorners, + cornerRadii: cornerRadii + ) + #else + self.init( + roundedRect: NSRectFromCGRect(rect), + xRadius: cornerRadii.width, + yRadius: cornerRadii.height + ) + #endif + } +} + +open class ShadowStyleLayer: CALayer { + + // MARK: - Instance Properties + + private var shadowLayers: [CALayer] = [] + private let backgroundLayer = CALayer() + + public var shadowStyle: ShadowStyle { + didSet { updateShadowLayers() } + } + + public override var backgroundColor: CGColor? { + get { backgroundLayer.backgroundColor } + set { backgroundLayer.backgroundColor = newValue } + } + + public override var cornerRadius: CGFloat { + didSet { backgroundLayer.cornerRadius = cornerRadius } + } + + public override var maskedCorners: CACornerMask { + didSet { backgroundLayer.maskedCorners = maskedCorners } + } + + // MARK: - Initializers + + public init(shadowStyle: ShadowStyle) { + self.shadowStyle = shadowStyle + + super.init() + + configureShadowLayers() + configureBackgroundLayer() + } + + public override convenience init() { + self.init(shadowStyle: .clear) + } + + public override convenience init(layer: Any) { + if let layer = layer as? ShadowStyleLayer { + self.init(shadowStyle: layer.shadowStyle) + } else { + self.init(shadowStyle: .clear) + } + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Instance Methods + + private func configureShadowLayers() { + shadowLayers = shadowStyle + .shadows + .map { CALayer(shadow: $0) } + + shadowLayers.reversed().forEach { shadowLayer in + insertSublayer(shadowLayer, at: 0) + } + } + + private func configureBackgroundLayer() { + backgroundLayer.masksToBounds = true + + addSublayer(backgroundLayer) + } + + private func updateShadowLayers() { + shadowLayers.forEach { $0.removeFromSuperlayer() } + + configureShadowLayers() + } + + private func layoutShadowLayers() { + shadowLayers.forEach { shadowLayer in + shadowLayer.frame = bounds + + shadowLayer.shadowPath = UIBezierPath( + roundedRect: bounds, + byRoundingCorners: maskedCorners, + cornerRadii: CGSize( + width: cornerRadius, + height: cornerRadius + ) + ).cgPath + } + } + + private func layoutBackgroundLayer() { + backgroundLayer.frame = bounds + } + + open override func layoutSublayers() { + super.layoutSublayers() + + layoutShadowLayers() + layoutBackgroundLayer() + } +} + +open class ShadowStyleView: UIView { + + // MARK: - Type Properties + + public override class var layerClass: AnyClass { + ShadowStyleLayer.self + } + + // MARK: - Instance Properties + + public var shadowStyleLayer: ShadowStyleLayer { + layer as! ShadowStyleLayer + } + + public var shadowStyle: ShadowStyle { + get { shadowStyleLayer.shadowStyle } + set { shadowStyleLayer.shadowStyle = newValue } + } + + public override var backgroundColor: UIColor? { + get { shadowStyleLayer.backgroundColor.map(UIColor.init(cgColor:)) } + set { shadowStyleLayer.backgroundColor = newValue?.cgColor } + } +} diff --git a/Demo/FigmaGenDemo/Generated/SpacingTokens.swift b/Demo/FigmaGenDemo/Generated/SpacingTokens.swift new file mode 100644 index 0000000..737eb8f --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/SpacingTokens.swift @@ -0,0 +1,132 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct SpacingTokens { + + // MARK: - Instance Properties + + /// spacing.0 + /// + /// Value: 0 + public var spacing0: CGFloat { + 0 + } + + /// spacing.1 + /// + /// Value: 8 + public var spacing1: CGFloat { + 8 + } + + /// spacing.2 + /// + /// Value: 16 + public var spacing2: CGFloat { + 16 + } + + /// spacing.3 + /// + /// Value: 24 + public var spacing3: CGFloat { + 24 + } + + /// spacing.4 + /// + /// Value: 32 + public var spacing4: CGFloat { + 32 + } + + /// spacing.5 + /// + /// Value: 40 + public var spacing5: CGFloat { + 40 + } + + /// spacing.6 + /// + /// Value: 48 + public var spacing6: CGFloat { + 48 + } + + /// spacing.7 + /// + /// Value: 56 + public var spacing7: CGFloat { + 56 + } + + /// spacing.8 + /// + /// Value: 64 + public var spacing8: CGFloat { + 64 + } + + /// spacing.9 + /// + /// Value: 72 + public var spacing9: CGFloat { + 72 + } + + /// spacing.10 + /// + /// Value: 80 + public var spacing10: CGFloat { + 80 + } + + /// spacing.xs + /// + /// Value: 4 + public var spacingXs: CGFloat { + 4 + } + + /// spacing.sm + /// + /// Value: 8 + public var spacingSm: CGFloat { + 8 + } + + /// spacing.md + /// + /// Value: 16 + public var spacingMd: CGFloat { + 16 + } + + /// spacing.lg + /// + /// Value: 32 + public var spacingLg: CGFloat { + 32 + } + + /// spacing.xl + /// + /// Value: 64 + public var spacingXl: CGFloat { + 64 + } + + /// spacing.multi-value + /// + /// Value: 8 64 + public var spacingMultiValue: UIEdgeInsets { + UIEdgeInsets(top: 8, left: 64, bottom: 8, right: 64) + } +} diff --git a/Demo/FigmaGenDemo/Generated/Spacings.swift b/Demo/FigmaGenDemo/Generated/Spacings.swift deleted file mode 100644 index adf72c4..0000000 --- a/Demo/FigmaGenDemo/Generated/Spacings.swift +++ /dev/null @@ -1,56 +0,0 @@ -// swiftlint:disable all -import UIKit - -public protocol Spacing { - init(_ value: Double) -} - -extension Int: Spacing { } -extension UInt: Spacing { } -extension Float: Spacing { } -extension Double: Spacing { } -extension CGFloat: Spacing { } - -extension Spacing { - - /// L - /// - /// Value: 24.0. - public static var l: Self { Self(24.0) } - - /// M - /// - /// Value: 16.0. - public static var m: Self { Self(16.0) } - - /// MPlus - /// - /// Value: 20.0. - public static var mplus: Self { Self(20.0) } - - /// S - /// - /// Value: 12.0. - public static var s: Self { Self(12.0) } - - /// XL - /// - /// Value: 32.0. - public static var xl: Self { Self(32.0) } - - /// XS - /// - /// Value: 8.0. - public static var xs: Self { Self(8.0) } - - /// XXS - /// - /// Value: 4.0. - public static var xxs: Self { Self(4.0) } - - /// XXXS - /// - /// Value: 1.0. - public static var xxxs: Self { Self(1.0) } -} -// swiftlint:enable all diff --git a/Demo/FigmaGenDemo/Generated/TextStyle.swift b/Demo/FigmaGenDemo/Generated/TextStyle.swift index 1702e66..c8ed850 100644 --- a/Demo/FigmaGenDemo/Generated/TextStyle.swift +++ b/Demo/FigmaGenDemo/Generated/TextStyle.swift @@ -1,99 +1,278 @@ // swiftlint:disable all -import Foundation +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) import UIKit +#else +import AppKit +#endif public struct TextStyle: Equatable { - public let font: UIFont - public let textColor: UIColor + // MARK: - Nested Types + + public enum ValidationError: Error, CustomStringConvertible { + case fontNotFound(name: String, size: Double) + + public var description: String { + switch self { + case let .fontNotFound(name, size): + return "Font '\(name) \(size)' couldn't be loaded" + } + } + } + + // MARK: - Type Properties + + /// Caption + /// + /// Font: SF Pro Display (SFProDisplay-Light); weight 300.0; size 13.0 + /// Color: Eclipse; hex #393939FF; rgba 57 57 57, 100% + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: default + /// Paragraph indent: default + /// Line height: 15.525 + /// Letter spacing: 0.0 + public static let caption = TextStyle( + font: UIFont(name: "SFProDisplay-Light", size: 13.0), + color: UIColor( + red: 0.2235294133424759, + green: 0.2235294133424759, + blue: 0.2235294133424759, + alpha: 1.0 + ), + strikethrough: false, + underline: false, + paragraphSpacing: nil, + paragraphIndent: nil, + lineHeight: 15.525, + letterSpacing: 0.0 + ) + + /// Body + /// + /// Font: SF Pro Display (SFProDisplay-Regular); weight 400.0; size 13.0 + /// Color: Eclipse; hex #393939FF; rgba 57 57 57, 100% + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: 4.0 + /// Paragraph indent: 16.0 + /// Line height: 16.0 + /// Letter spacing: 0.125 + public static let body = TextStyle( + font: UIFont(name: "SFProDisplay-Regular", size: 13.0), + color: UIColor( + red: 0.2235294133424759, + green: 0.2235294133424759, + blue: 0.2235294133424759, + alpha: 1.0 + ), + strikethrough: false, + underline: false, + paragraphSpacing: 4.0, + paragraphIndent: 16.0, + lineHeight: 16.0, + letterSpacing: 0.125 + ) + + /// Subtitle + /// + /// Font: SF Pro Display (SFProDisplay-Regular); weight 400.0; size 15.0 + /// Color: Eclipse; hex #393939FF; rgba 57 57 57, 100% + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: default + /// Paragraph indent: default + /// Line height: 17.9 + /// Letter spacing: 0.2 + public static let subtitle = TextStyle( + font: UIFont(name: "SFProDisplay-Regular", size: 15.0), + color: UIColor( + red: 0.2235294133424759, + green: 0.2235294133424759, + blue: 0.2235294133424759, + alpha: 1.0 + ), + strikethrough: false, + underline: false, + paragraphSpacing: nil, + paragraphIndent: nil, + lineHeight: 17.9, + letterSpacing: 0.2 + ) + + /// Title + /// + /// Font: SF Pro Display (SFProDisplay-Medium); weight 500.0; size 17.0 + /// Color: Eclipse; hex #393939FF; rgba 57 57 57, 100% + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: default + /// Paragraph indent: default + /// Line height: 20.275 + /// Letter spacing: 0.125 + public static let title = TextStyle( + font: UIFont(name: "SFProDisplay-Medium", size: 17.0), + color: UIColor( + red: 0.2235294133424759, + green: 0.2235294133424759, + blue: 0.2235294133424759, + alpha: 1.0 + ), + strikethrough: false, + underline: false, + paragraphSpacing: nil, + paragraphIndent: nil, + lineHeight: 20.275, + letterSpacing: 0.125 + ) + + /// Large Title + /// + /// Font: SF Pro Display (SFProDisplay-Bold); weight 700.0; size 34.0 + /// Color: Eclipse; hex #393939FF; rgba 57 57 57, 100% + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: default + /// Paragraph indent: default + /// Line height: 40.575 + /// Letter spacing: 0.1 + public static let largeTitle = TextStyle( + font: UIFont(name: "SFProDisplay-Bold", size: 34.0), + color: UIColor( + red: 0.2235294133424759, + green: 0.2235294133424759, + blue: 0.2235294133424759, + alpha: 1.0 + ), + strikethrough: false, + underline: false, + paragraphSpacing: nil, + paragraphIndent: nil, + lineHeight: 40.575, + letterSpacing: 0.1 + ) + + // MARK: - Type Methods + + public static func validate() throws { + guard UIFont(name: "SFProDisplay-Light", size: 13.0) != nil else { + throw ValidationError.fontNotFound(name: "SFProDisplay-Light", size: 13.0) + } + + guard UIFont(name: "SFProDisplay-Regular", size: 13.0) != nil else { + throw ValidationError.fontNotFound(name: "SFProDisplay-Regular", size: 13.0) + } + + guard UIFont(name: "SFProDisplay-Regular", size: 15.0) != nil else { + throw ValidationError.fontNotFound(name: "SFProDisplay-Regular", size: 15.0) + } + + guard UIFont(name: "SFProDisplay-Medium", size: 17.0) != nil else { + throw ValidationError.fontNotFound(name: "SFProDisplay-Medium", size: 17.0) + } + + guard UIFont(name: "SFProDisplay-Bold", size: 34.0) != nil else { + throw ValidationError.fontNotFound(name: "SFProDisplay-Bold", size: 34.0) + } + + print("All text styles are valid") + } + + // MARK: - Instance Properties + + public let font: UIFont? + public let color: UIColor? + public let backgroundColor: UIColor? + public let strikethrough: Bool + public let underline: Bool public let paragraphSpacing: CGFloat? public let paragraphIndent: CGFloat? public let lineHeight: CGFloat? public let letterSpacing: CGFloat? + public let lineBreakMode: NSLineBreakMode? + public let alignment: NSTextAlignment? - public var actualLineHeight: CGFloat { - return lineHeight ?? font.lineHeight - } + // MARK: - Initializers public init( - font: UIFont, - textColor: UIColor, + font: UIFont? = nil, + color: UIColor? = nil, + backgroundColor: UIColor? = nil, + strikethrough: Bool = false, + underline: Bool = false, paragraphSpacing: CGFloat? = nil, paragraphIndent: CGFloat? = nil, lineHeight: CGFloat? = nil, - letterSpacing: CGFloat? = nil + letterSpacing: CGFloat? = nil, + lineBreakMode: NSLineBreakMode? = nil, + alignment: NSTextAlignment? = nil ) { self.font = font - self.textColor = textColor + self.color = color + self.backgroundColor = backgroundColor + self.strikethrough = strikethrough + self.underline = underline self.paragraphSpacing = paragraphSpacing self.paragraphIndent = paragraphIndent self.lineHeight = lineHeight self.letterSpacing = letterSpacing + self.lineBreakMode = lineBreakMode + self.alignment = alignment } - public init( - fontName: String, - fontSize: CGFloat, - textColor: UIColor, - paragraphSpacing: CGFloat? = nil, - paragraphIndent: CGFloat? = nil, - lineHeight: CGFloat? = nil, - letterSpacing: CGFloat? = nil - ) { - self.init( - font: UIFont(name: fontName, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize), - textColor: textColor, - paragraphSpacing: paragraphSpacing, - paragraphIndent: paragraphIndent, - lineHeight: lineHeight, - letterSpacing: letterSpacing - ) - } + // MARK: - Instance Methods - public func withTextColor(_ textColor: UIColor) -> TextStyle { - return TextStyle( - font: font, - textColor: textColor, - paragraphSpacing: paragraphSpacing, - paragraphIndent: paragraphIndent, - lineHeight: lineHeight, - letterSpacing: letterSpacing - ) - } + private func attributes(paragraphStyle: NSParagraphStyle?) -> [NSAttributedString.Key: Any] { + var attributes: [NSAttributedString.Key: Any] = [:] - public func attributes( - textColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - alignment: NSTextAlignment? = nil, - lineBreakMode: NSLineBreakMode? = nil, - ignoringParagraphStyle: Bool = false - ) -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: textColor ?? self.textColor, - ] + if let paragraphStyle = paragraphStyle { + attributes[.paragraphStyle] = paragraphStyle + } + + if let font = font { + attributes[.font] = font + } + + if let color = color { + attributes[.foregroundColor] = color + } if let backgroundColor = backgroundColor { attributes[.backgroundColor] = backgroundColor } - if let letterSpacing = letterSpacing { - attributes[.kern] = NSNumber(value: Float(letterSpacing)) + if strikethrough { + attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue } - if ignoringParagraphStyle { - return attributes + if underline { + attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue } + if let letterSpacing = letterSpacing { + attributes[.kern] = letterSpacing + } + + return attributes + } + + // MARK: - + + public func paragraphStyle() -> NSParagraphStyle { let paragraphStyle = NSMutableParagraphStyle() if let lineHeight = lineHeight { - let paragraphLineSpacing = (lineHeight - font.lineHeight) / 2.0 - let paragraphLineHeight = lineHeight - paragraphLineSpacing + if let font = font { + paragraphStyle.lineSpacing = (lineHeight - font.lineHeight) * 0.5 + paragraphStyle.minimumLineHeight = lineHeight - paragraphStyle.lineSpacing + } else { + paragraphStyle.lineSpacing = 0.0 + paragraphStyle.minimumLineHeight = lineHeight + } - paragraphStyle.lineSpacing = paragraphLineSpacing - paragraphStyle.minimumLineHeight = paragraphLineHeight - paragraphStyle.maximumLineHeight = paragraphLineHeight + paragraphStyle.maximumLineHeight = paragraphStyle.minimumLineHeight } if let paragraphSpacing = paragraphSpacing { @@ -104,161 +283,216 @@ public struct TextStyle: Equatable { paragraphStyle.firstLineHeadIndent = paragraphIndent } + if let lineBreakMode = lineBreakMode { + paragraphStyle.lineBreakMode = lineBreakMode + } + if let alignment = alignment { paragraphStyle.alignment = alignment } - if let lineBreakMode = lineBreakMode { - paragraphStyle.lineBreakMode = lineBreakMode + return paragraphStyle + } + + public func attributes(includingParagraphStyle: Bool = true) -> [NSAttributedString.Key: Any] { + if includingParagraphStyle { + return attributes(paragraphStyle: paragraphStyle()) + } else { + return attributes(paragraphStyle: nil) } + } - attributes[.paragraphStyle] = paragraphStyle + public func attributedString( + _ string: String, + includingParagraphStyle: Bool = true + ) -> NSAttributedString { + return NSAttributedString(string: string, style: self, includingParagraphStyle: includingParagraphStyle) + } - return attributes + // MARK: - + + public func withFont(_ font: UIFont?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) } -} -public extension TextStyle { + public func withColor(_ color: UIColor?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - /// Body - /// - /// Font: SF Pro Display (SFProDisplay-Regular); weight 400.0; size 13.0 - /// Text color: Black; hex: #313033FF; rgba: 49 48 51, 100% - /// Paragraph spacing: default - /// Paragraph indent: default - /// Line height: 17.0 - /// Letter spacing: -0.0 - static let body = TextStyle( - fontName: "SFProDisplay-Regular", - fontSize: 13.0, - textColor: UIColor(rgbaHex: 0x313033FF), - paragraphSpacing: nil, - paragraphIndent: nil, - lineHeight: 17.0, - letterSpacing: -0.0 - ) + public func withBackgroundColor(_ backgroundColor: UIColor?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - /// Subtitle 2 - /// - /// Font: SF Pro Display (SFProDisplay-Regular); weight 400.0; size 11.0 - /// Text color: Black; hex: #313033FF; rgba: 49 48 51, 100% - /// Paragraph spacing: default - /// Paragraph indent: default - /// Line height: 14.0 - /// Letter spacing: 0.25 - static let subtitle2 = TextStyle( - fontName: "SFProDisplay-Regular", - fontSize: 11.0, - textColor: UIColor(rgbaHex: 0x313033FF), - paragraphSpacing: nil, - paragraphIndent: nil, - lineHeight: 14.0, - letterSpacing: 0.25 - ) + public func withStrikethrough(_ strikethrough: Bool) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - /// Subtitle 1 - /// - /// Font: SF Pro Display (SFProDisplay-Bold); weight 700.0; size 13.0 - /// Text color: Black; hex: #313033FF; rgba: 49 48 51, 100% - /// Paragraph spacing: default - /// Paragraph indent: default - /// Line height: 15.25 - /// Letter spacing: 0.35 - static let subtitle1 = TextStyle( - fontName: "SFProDisplay-Bold", - fontSize: 13.0, - textColor: UIColor(rgbaHex: 0x313033FF), - paragraphSpacing: nil, - paragraphIndent: nil, - lineHeight: 15.25, - letterSpacing: 0.35 - ) + public func withUnderline(_ underline: Bool) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - /// Title 2 - /// - /// Font: SF Pro Display (SFProDisplay-Semibold); weight 600.0; size 14.0 - /// Text color: Black; hex: #313033FF; rgba: 49 48 51, 100% - /// Paragraph spacing: default - /// Paragraph indent: default - /// Line height: 16.4 - /// Letter spacing: 0.0 - static let title2 = TextStyle( - fontName: "SFProDisplay-Semibold", - fontSize: 14.0, - textColor: UIColor(rgbaHex: 0x313033FF), - paragraphSpacing: nil, - paragraphIndent: nil, - lineHeight: 16.4, - letterSpacing: 0.0 - ) + public func withParagraphSpacing(_ paragraphSpacing: CGFloat?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - /// Title 1 - /// - /// Font: SF Pro Display (SFProDisplay-Bold); weight 700.0; size 17.0 - /// Text color: Black; hex: #313033FF; rgba: 49 48 51, 100% - /// Paragraph spacing: default - /// Paragraph indent: default - /// Line height: 19.9 - /// Letter spacing: 0.35 - static let title1 = TextStyle( - fontName: "SFProDisplay-Bold", - fontSize: 17.0, - textColor: UIColor(rgbaHex: 0x313033FF), - paragraphSpacing: nil, - paragraphIndent: nil, - lineHeight: 19.9, - letterSpacing: 0.35 - ) + public func withParagraphIndent(_ paragraphIndent: CGFloat?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - /// Large Title - /// - /// Font: SF Pro Display (SFProDisplay-Bold); weight 700.0; size 36.0 - /// Text color: Black; hex: #313033FF; rgba: 49 48 51, 100% - /// Paragraph spacing: default - /// Paragraph indent: default - /// Line height: 40.0 - /// Letter spacing: 0.25 - static let largeTitle = TextStyle( - fontName: "SFProDisplay-Bold", - fontSize: 36.0, - textColor: UIColor(rgbaHex: 0x313033FF), - paragraphSpacing: nil, - paragraphIndent: nil, - lineHeight: 40.0, - letterSpacing: 0.25 - ) -} + public func withLineHeight(_ lineHeight: CGFloat?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } -public extension String { + public func withLetterSpacing(_ letterSpacing: CGFloat?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } - func styled( - _ textStyle: TextStyle, - textColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - alignment: NSTextAlignment? = nil, - lineBreakMode: NSLineBreakMode? = nil - ) -> NSAttributedString { - return NSAttributedString( - string: self, - attributes: textStyle.attributes( - textColor: textColor, - backgroundColor: backgroundColor, - alignment: alignment, - lineBreakMode: lineBreakMode - ) + public func withLineBreakMode(_ lineBreakMode: NSLineBreakMode?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withAlignment(_ alignment: NSTextAlignment?) -> TextStyle { + return TextStyle( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment ) } } -private extension UIColor { +public extension NSAttributedString { - convenience init(rgbaHex: UInt32) { - self.init( - red: CGFloat((rgbaHex >> 24) & 0xFF) / 255.0, - green: CGFloat((rgbaHex >> 16) & 0xFF) / 255.0, - blue: CGFloat((rgbaHex >> 8) & 0xFF) / 255.0, - alpha: CGFloat(rgbaHex & 0xFF) / 255.0 - ) + // MARK: - Initializers + + convenience init(string: String, style: TextStyle, includingParagraphStyle: Bool = true) { + self.init(string: string, attributes: style.attributes(includingParagraphStyle: includingParagraphStyle)) } } -// swiftlint:enable all diff --git a/Demo/FigmaGenDemo/Generated/Theme.swift b/Demo/FigmaGenDemo/Generated/Theme.swift new file mode 100644 index 0000000..c159b81 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/Theme.swift @@ -0,0 +1,130 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct Theme { + + public let colors: ColorTokens + public let shadows: BoxShadowTokens + public let typographies: TypographyTokens + + init( + colors: ColorTokens, + shadows: BoxShadowTokens, + typographies: TypographyTokens = TypographyTokens() + ) { + self.colors = colors + self.shadows = shadows + self.typographies = typographies + } +} + +extension Theme { + + public static let hhDay = Self( + colors: ColorTokens( + accent: ColorTokens.Accent( + bg: UIColor(hex: 0xC3DAFEFF), + default: UIColor(hex: 0x7F9CF5FF), + onAccent: UIColor(hex: 0xFFFFFFFF) + ), + bg: ColorTokens.Bg( + default: UIColor(hex: 0xFFFFFFFF), + muted: UIColor(hex: 0xF7FAFCFF), + subtle: UIColor(hex: 0xEDF2F7FF) + ), + fg: ColorTokens.Fg( + default: UIColor(hex: 0x000000FF), + muted: UIColor(hex: 0x4A5568FF), + subtle: UIColor(hex: 0xA0AEC0FF) + ), + shadows: ColorTokens.Shadows( + default: UIColor(hex: 0x1A202CFF) + ) + ), + shadows: BoxShadowTokens( + level1: ShadowToken( + offset: CGSize(width: 0, height: 4), + radius: 12, + color: UIColor(hex: 0x7090B029), + opacity: 1.0 + ), + level2: ShadowToken( + offset: CGSize(width: 0, height: 8), + radius: 16, + color: UIColor(hex: 0x7090B03D), + opacity: 1.0 + ), + level3: ShadowToken( + offset: CGSize(width: 0, height: 12), + radius: 24, + color: UIColor(hex: 0x7090B052), + opacity: 1.0 + ) + ) + ) + public static let hhNight = Self( + colors: ColorTokens( + accent: ColorTokens.Accent( + bg: UIColor(hex: 0x434190FF), + default: UIColor(hex: 0x5A67D8FF), + onAccent: UIColor(hex: 0xFFFFFFFF) + ), + bg: ColorTokens.Bg( + default: UIColor(hex: 0x1A202CFF), + muted: UIColor(hex: 0x4A5568FF), + subtle: UIColor(hex: 0x718096FF) + ), + fg: ColorTokens.Fg( + default: UIColor(hex: 0xFFFFFFFF), + muted: UIColor(hex: 0xE2E8F0FF), + subtle: UIColor(hex: 0xA0AEC0FF) + ), + shadows: ColorTokens.Shadows( + default: UIColor(hex: 0x00000000) + ) + ), + shadows: BoxShadowTokens( + level1: ShadowToken( + offset: CGSize(width: 0, height: 4), + radius: 12, + color: UIColor(hex: 0x7090B029), + opacity: 1.0 + ), + level2: ShadowToken( + offset: CGSize(width: 0, height: 8), + radius: 16, + color: UIColor(hex: 0x7090B03D), + opacity: 1.0 + ), + level3: ShadowToken( + offset: CGSize(width: 0, height: 12), + radius: 24, + color: UIColor(hex: 0x7090B052), + opacity: 1.0 + ) + ) + ) +} + +private extension UIColor { + + convenience init(hex: UInt32) { + let red = UInt8((hex >> 24) & 0xFF) + let green = UInt8((hex >> 16) & 0xFF) + let blue = UInt8((hex >> 8) & 0xFF) + let alpha = UInt8(hex & 0xFF) + + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: CGFloat(alpha) / 255.0 + ) + } +} diff --git a/Demo/FigmaGenDemo/Generated/TypographyTokens.swift b/Demo/FigmaGenDemo/Generated/TypographyTokens.swift new file mode 100644 index 0000000..063a598 --- /dev/null +++ b/Demo/FigmaGenDemo/Generated/TypographyTokens.swift @@ -0,0 +1,460 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct TypographyTokens { + + /// typography H1 Bold + /// + /// Font: Inter Bold + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: 32 + /// Paragraph indent: default + /// Line height: 53.71 + /// Letter spacing: -2.44 + public let typographyH1Bold = Typography( + font: UIFont(name: "Inter-Bold", size: 48.828125), + color: .label, + strikethrough: false, + underline: false, + paragraphSpacing: 32, + paragraphIndent: nil, + lineHeight: 53.71, + letterSpacing: -2.44 + ) + + /// typography H1 Regular + /// + /// Font: Inter Regular + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: 32 + /// Paragraph indent: default + /// Line height: 53.71 + /// Letter spacing: -2.44 + public let typographyH1Regular = Typography( + font: UIFont(name: "Inter-Regular", size: 48.828125), + color: .label, + strikethrough: false, + underline: false, + paragraphSpacing: 32, + paragraphIndent: nil, + lineHeight: 53.71, + letterSpacing: -2.44 + ) + + /// typography H2 Bold + /// + /// Font: Inter Bold + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: 26 + /// Paragraph indent: default + /// Line height: 42.97 + /// Letter spacing: -1.95 + public let typographyH2Bold = Typography( + font: UIFont(name: "Inter-Bold", size: 39.0625), + color: .label, + strikethrough: false, + underline: false, + paragraphSpacing: 26, + paragraphIndent: nil, + lineHeight: 42.97, + letterSpacing: -1.95 + ) + + /// typography H2 Regular + /// + /// Font: Inter Regular + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: 26 + /// Paragraph indent: default + /// Line height: 42.97 + /// Letter spacing: -1.95 + public let typographyH2Regular = Typography( + font: UIFont(name: "Inter-Regular", size: 39.0625), + color: .label, + strikethrough: false, + underline: false, + paragraphSpacing: 26, + paragraphIndent: nil, + lineHeight: 42.97, + letterSpacing: -1.95 + ) + + /// typography Body + /// + /// Font: Roboto Regular + /// Strikethrough: false + /// Underline: false + /// Paragraph spacing: 26 + /// Paragraph indent: default + /// Line height: 17.6 + /// Letter spacing: default + public let typographyBody = Typography( + font: UIFont(name: "Roboto-Regular", size: 16), + color: .label, + strikethrough: false, + underline: false, + paragraphSpacing: 26, + paragraphIndent: nil, + lineHeight: 17.6, + letterSpacing: nil + ) +} + +public struct Typography: Equatable { + + public enum ValidationError: Error, CustomStringConvertible { + case fontNotFound(name: String, size: Double) + + public var description: String { + switch self { + case let .fontNotFound(name, size): + return "Font '\(name) \(size)' couldn't be loaded" + } + } + } + + public static func validate() throws { + guard UIFont(name: "Inter-Bold", size: 48.828125) != nil else { + throw ValidationError.fontNotFound(name: "Inter-Bold", size: 48.828125) + } + + guard UIFont(name: "Inter-Regular", size: 48.828125) != nil else { + throw ValidationError.fontNotFound(name: "Inter-Regular", size: 48.828125) + } + + guard UIFont(name: "Inter-Bold", size: 39.0625) != nil else { + throw ValidationError.fontNotFound(name: "Inter-Bold", size: 39.0625) + } + + guard UIFont(name: "Inter-Regular", size: 39.0625) != nil else { + throw ValidationError.fontNotFound(name: "Inter-Regular", size: 39.0625) + } + + guard UIFont(name: "Roboto-Regular", size: 16) != nil else { + throw ValidationError.fontNotFound(name: "Roboto-Regular", size: 16) + } + + print("All text styles are valid") + } + + public let font: UIFont? + public let color: UIColor? + public let backgroundColor: UIColor? + public let strikethrough: Bool + public let underline: Bool + public let paragraphSpacing: CGFloat? + public let paragraphIndent: CGFloat? + public let lineHeight: CGFloat? + public let letterSpacing: CGFloat? + public let lineBreakMode: NSLineBreakMode? + public let alignment: NSTextAlignment? + + public init( + font: UIFont? = nil, + color: UIColor? = nil, + backgroundColor: UIColor? = nil, + strikethrough: Bool = false, + underline: Bool = false, + paragraphSpacing: CGFloat? = nil, + paragraphIndent: CGFloat? = nil, + lineHeight: CGFloat? = nil, + letterSpacing: CGFloat? = nil, + lineBreakMode: NSLineBreakMode? = nil, + alignment: NSTextAlignment? = nil + ) { + self.font = font + self.color = color + self.backgroundColor = backgroundColor + self.strikethrough = strikethrough + self.underline = underline + self.paragraphSpacing = paragraphSpacing + self.paragraphIndent = paragraphIndent + self.lineHeight = lineHeight + self.letterSpacing = letterSpacing + self.lineBreakMode = lineBreakMode + self.alignment = alignment + } + + private func attributes(paragraphStyle: NSParagraphStyle?) -> [NSAttributedString.Key: Any] { + var attributes: [NSAttributedString.Key: Any] = [:] + + if let paragraphStyle = paragraphStyle { + attributes[.paragraphStyle] = paragraphStyle + } + + if let font = font { + attributes[.font] = font + } + + if let color = color { + attributes[.foregroundColor] = color + } + + if let backgroundColor = backgroundColor { + attributes[.backgroundColor] = backgroundColor + } + + if strikethrough { + attributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue + } + + if underline { + attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue + } + + if let letterSpacing = letterSpacing { + attributes[.kern] = letterSpacing + } + + return attributes + } + + public func paragraphStyle() -> NSParagraphStyle { + let paragraphStyle = NSMutableParagraphStyle() + + if let lineHeight = lineHeight { + if let font = font { + paragraphStyle.lineSpacing = (lineHeight - font.lineHeight) * 0.5 + paragraphStyle.minimumLineHeight = lineHeight - paragraphStyle.lineSpacing + } else { + paragraphStyle.lineSpacing = 0.0 + paragraphStyle.minimumLineHeight = lineHeight + } + + paragraphStyle.maximumLineHeight = paragraphStyle.minimumLineHeight + } + + if let paragraphSpacing = paragraphSpacing { + paragraphStyle.paragraphSpacing = paragraphSpacing + } + + if let paragraphIndent = paragraphIndent { + paragraphStyle.firstLineHeadIndent = paragraphIndent + } + + if let lineBreakMode = lineBreakMode { + paragraphStyle.lineBreakMode = lineBreakMode + } + + if let alignment = alignment { + paragraphStyle.alignment = alignment + } + + return paragraphStyle + } + + public func attributes(includingParagraphStyle: Bool = true) -> [NSAttributedString.Key: Any] { + if includingParagraphStyle { + return attributes(paragraphStyle: paragraphStyle()) + } else { + return attributes(paragraphStyle: nil) + } + } + + public func attributedString( + _ string: String, + includingParagraphStyle: Bool = true + ) -> NSAttributedString { + return NSAttributedString(string: string, style: self, includingParagraphStyle: includingParagraphStyle) + } + + public func withFont(_ font: UIFont?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withColor(_ color: UIColor?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withBackgroundColor(_ backgroundColor: UIColor?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withStrikethrough(_ strikethrough: Bool) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withUnderline(_ underline: Bool) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withParagraphSpacing(_ paragraphSpacing: CGFloat?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withParagraphIndent(_ paragraphIndent: CGFloat?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withLineHeight(_ lineHeight: CGFloat?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withLetterSpacing(_ letterSpacing: CGFloat?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withLineBreakMode(_ lineBreakMode: NSLineBreakMode?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } + + public func withAlignment(_ alignment: NSTextAlignment?) -> Typography { + return Typography( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } +} + +public extension NSAttributedString { + + convenience init(string: String, style: Typography, includingParagraphStyle: Bool = true) { + self.init(string: string, attributes: style.attributes(includingParagraphStyle: includingParagraphStyle)) + } +} + +public extension String { + + func styled(as typography: Typography) -> NSAttributedString { + NSAttributedString(string: self, style: typography) + } +} diff --git a/Demo/FigmaGenDemo/Info.plist b/Demo/FigmaGenDemo/Info.plist index b0f4e12..0070cfb 100644 --- a/Demo/FigmaGenDemo/Info.plist +++ b/Demo/FigmaGenDemo/Info.plist @@ -20,6 +20,17 @@ 1 LSRequiresIPhoneOS + UIAppFonts + + SF-Pro-Display-Bold.otf + SF-Pro-Display-Semibold.otf + SF-Pro-Display-Light.otf + SF-Pro-Display-Medium.otf + SF-Pro-Display-Regular.otf + Inter-Regular.otf + Inter-Bold.otf + Roboto-Regular.ttf + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -31,8 +42,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad @@ -41,13 +50,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIAppFonts - - SF-Pro-Display-Bold.otf - SF-Pro-Display-Semibold.otf - SF-Pro-Display-Light.otf - SF-Pro-Display-Medium.otf - SF-Pro-Display-Regular.otf - diff --git a/Demo/FigmaGenDemo/Resources/Base.lproj/LaunchScreen.storyboard b/Demo/FigmaGenDemo/Resources/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 865e932..0000000 --- a/Demo/FigmaGenDemo/Resources/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Demo/FigmaGenDemo/Resources/Base.lproj/Main.storyboard b/Demo/FigmaGenDemo/Resources/Base.lproj/Main.storyboard deleted file mode 100644 index a78d66a..0000000 --- a/Demo/FigmaGenDemo/Resources/Base.lproj/Main.storyboard +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/DaisyBush.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/DaisyBush.colorset/Contents.json new file mode 100644 index 0000000..023f364 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/DaisyBush.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.75", + "blue" : "0.5882353186607361", + "green" : "0.25882354378700256", + "red" : "0.35686275362968445" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/Eclipse.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Eclipse.colorset/Contents.json new file mode 100644 index 0000000..4e15a0f --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Eclipse.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.2235294133424759", + "green" : "0.2235294133424759", + "red" : "0.2235294133424759" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/JellyBean.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/JellyBean.colorset/Contents.json new file mode 100644 index 0000000..ede2d36 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/JellyBean.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.5882353186607361", + "green" : "0.4901960790157318", + "red" : "0.25882354378700256" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/Lochinvar.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Lochinvar.colorset/Contents.json new file mode 100644 index 0000000..ed5f229 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Lochinvar.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.4901960790157318", + "green" : "0.5882353186607361", + "red" : "0.25882354378700256" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/Razzmatazz.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Razzmatazz.colorset/Contents.json new file mode 100644 index 0000000..89cf210 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Razzmatazz.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.3607843220233917", + "green" : "0.04313725605607033", + "red" : "0.8901960849761963" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/SnowDrift.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/SnowDrift.colorset/Contents.json new file mode 100644 index 0000000..92e03d7 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/SnowDrift.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.8509804010391235", + "green" : "0.8549019694328308", + "red" : "0.8549019694328308" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/Submarine.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Submarine.colorset/Contents.json new file mode 100644 index 0000000..caa4f87 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Submarine.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.5960784554481506", + "green" : "0.5921568870544434", + "red" : "0.5803921818733215" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Colors.xcassets/Whisper.colorset/Contents.json b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Whisper.colorset/Contents.json new file mode 100644 index 0000000..bd3b527 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Colors.xcassets/Whisper.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.0", + "blue" : "0.9137254953384399", + "green" : "0.9137254953384399", + "red" : "0.9137254953384399" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Fonts/Inter-Bold.otf b/Demo/FigmaGenDemo/Resources/Fonts/Inter-Bold.otf new file mode 100755 index 0000000..c74cc0c Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Fonts/Inter-Bold.otf differ diff --git a/Demo/FigmaGenDemo/Resources/Fonts/Inter-Regular.otf b/Demo/FigmaGenDemo/Resources/Fonts/Inter-Regular.otf new file mode 100755 index 0000000..84e6a61 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Fonts/Inter-Regular.otf differ diff --git a/Demo/FigmaGenDemo/Resources/Fonts/Roboto-Regular.ttf b/Demo/FigmaGenDemo/Resources/Fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..f16ad7a Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Fonts/Roboto-Regular.ttf differ diff --git a/Demo/FigmaGenDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Demo/FigmaGenDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Demo/FigmaGenDemo/Resources/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Demo/FigmaGenDemo/Resources/Assets.xcassets/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Contents.json similarity index 100% rename from Demo/FigmaGenDemo/Resources/Assets.xcassets/Contents.json rename to Demo/FigmaGenDemo/Resources/Images.xcassets/Contents.json diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Cloud.imageset/Cloud.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Cloud.imageset/Cloud.pdf new file mode 100644 index 0000000..407cae7 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Cloud.imageset/Cloud.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Cloud.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Cloud.imageset/Contents.json new file mode 100644 index 0000000..30f7efc --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Cloud.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Cloud.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Geo.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Geo.imageset/Contents.json new file mode 100644 index 0000000..4019aa5 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Geo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Geo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Geo.imageset/Geo.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Geo.imageset/Geo.pdf new file mode 100644 index 0000000..74c668e Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Geo.imageset/Geo.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Phone.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Phone.imageset/Contents.json new file mode 100644 index 0000000..548d826 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Phone.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Phone.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Phone.imageset/Phone.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Phone.imageset/Phone.pdf new file mode 100644 index 0000000..315cf7b Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Phone.imageset/Phone.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Share.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Share.imageset/Contents.json new file mode 100644 index 0000000..6104ffb --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Share.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Share.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Share.imageset/Share.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Share.imageset/Share.pdf new file mode 100644 index 0000000..832da6c Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Share.imageset/Share.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Snapchat.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Snapchat.imageset/Contents.json new file mode 100644 index 0000000..b638a4d --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Snapchat.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Snapchat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Snapchat.imageset/Snapchat.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Snapchat.imageset/Snapchat.pdf new file mode 100644 index 0000000..867bfbe Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Snapchat.imageset/Snapchat.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Star.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Star.imageset/Contents.json new file mode 100644 index 0000000..a25926c --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Star.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Star.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Star.imageset/Star.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Star.imageset/Star.pdf new file mode 100644 index 0000000..874d71c Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Star.imageset/Star.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Telegram.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Telegram.imageset/Contents.json new file mode 100644 index 0000000..6e13865 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Telegram.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Telegram.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Telegram.imageset/Telegram.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Telegram.imageset/Telegram.pdf new file mode 100644 index 0000000..fcd5216 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Telegram.imageset/Telegram.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Viber.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Viber.imageset/Contents.json new file mode 100644 index 0000000..797325b --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Viber.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Viber.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Viber.imageset/Viber.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Viber.imageset/Viber.pdf new file mode 100644 index 0000000..5944614 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/Viber.imageset/Viber.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WeChat.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WeChat.imageset/Contents.json new file mode 100644 index 0000000..3c32904 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WeChat.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "WeChat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WeChat.imageset/WeChat.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WeChat.imageset/WeChat.pdf new file mode 100644 index 0000000..32e406f Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WeChat.imageset/WeChat.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WhatsApp.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WhatsApp.imageset/Contents.json new file mode 100644 index 0000000..7fc8745 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WhatsApp.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "WhatsApp.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WhatsApp.imageset/WhatsApp.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WhatsApp.imageset/WhatsApp.pdf new file mode 100644 index 0000000..726c054 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Icon/WhatsApp.imageset/WhatsApp.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..5346105 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialBehanceStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleFilled.imageset/InterfaceEssentialBehanceStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleFilled.imageset/InterfaceEssentialBehanceStyleFilled.pdf new file mode 100644 index 0000000..b204451 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleFilled.imageset/InterfaceEssentialBehanceStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..bd0f26f --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialBehanceStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleOutlined.imageset/InterfaceEssentialBehanceStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleOutlined.imageset/InterfaceEssentialBehanceStyleOutlined.pdf new file mode 100644 index 0000000..ebc0ff1 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialBehance/InterfaceEssentialBehanceStyleOutlined.imageset/InterfaceEssentialBehanceStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..8faeda0 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialDribbbleStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleFilled.imageset/InterfaceEssentialDribbbleStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleFilled.imageset/InterfaceEssentialDribbbleStyleFilled.pdf new file mode 100644 index 0000000..029d1a6 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleFilled.imageset/InterfaceEssentialDribbbleStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..a929d6d --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialDribbbleStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleOutlined.imageset/InterfaceEssentialDribbbleStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleOutlined.imageset/InterfaceEssentialDribbbleStyleOutlined.pdf new file mode 100644 index 0000000..ead62ce Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialDribbble/InterfaceEssentialDribbbleStyleOutlined.imageset/InterfaceEssentialDribbbleStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookOutlined.imageset/Contents.json new file mode 100644 index 0000000..197a1ab --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialFacebookOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookOutlined.imageset/InterfaceEssentialFacebookOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookOutlined.imageset/InterfaceEssentialFacebookOutlined.pdf new file mode 100644 index 0000000..7aaaf6e Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookOutlined.imageset/InterfaceEssentialFacebookOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..2e78276 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialFacebookStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookStyleFilled.imageset/InterfaceEssentialFacebookStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookStyleFilled.imageset/InterfaceEssentialFacebookStyleFilled.pdf new file mode 100644 index 0000000..3515883 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFacebook/InterfaceEssentialFacebookStyleFilled.imageset/InterfaceEssentialFacebookStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..556c5dd --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialFigmaStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleFilled.imageset/InterfaceEssentialFigmaStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleFilled.imageset/InterfaceEssentialFigmaStyleFilled.pdf new file mode 100644 index 0000000..47e4236 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleFilled.imageset/InterfaceEssentialFigmaStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..c8142e0 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialFigmaStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleOutlined.imageset/InterfaceEssentialFigmaStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleOutlined.imageset/InterfaceEssentialFigmaStyleOutlined.pdf new file mode 100644 index 0000000..bd46b51 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialFigma/InterfaceEssentialFigmaStyleOutlined.imageset/InterfaceEssentialFigmaStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..f54934c --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialGoogleStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleFilled.imageset/InterfaceEssentialGoogleStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleFilled.imageset/InterfaceEssentialGoogleStyleFilled.pdf new file mode 100644 index 0000000..65a433c Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleFilled.imageset/InterfaceEssentialGoogleStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..b5a12f5 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialGoogleStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleOutlined.imageset/InterfaceEssentialGoogleStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleOutlined.imageset/InterfaceEssentialGoogleStyleOutlined.pdf new file mode 100644 index 0000000..fc205ce Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialGoogle/InterfaceEssentialGoogleStyleOutlined.imageset/InterfaceEssentialGoogleStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..400c8d1 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialInstagramStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleFilled.imageset/InterfaceEssentialInstagramStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleFilled.imageset/InterfaceEssentialInstagramStyleFilled.pdf new file mode 100644 index 0000000..d2cc1c7 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleFilled.imageset/InterfaceEssentialInstagramStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..2f671e1 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialInstagramStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleOutlined.imageset/InterfaceEssentialInstagramStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleOutlined.imageset/InterfaceEssentialInstagramStyleOutlined.pdf new file mode 100644 index 0000000..7af15fc Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialInstagram/InterfaceEssentialInstagramStyleOutlined.imageset/InterfaceEssentialInstagramStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..60af618 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialLinkedinStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleFilled.imageset/InterfaceEssentialLinkedinStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleFilled.imageset/InterfaceEssentialLinkedinStyleFilled.pdf new file mode 100644 index 0000000..b307d47 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleFilled.imageset/InterfaceEssentialLinkedinStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..675ae28 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialLinkedinStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleOutlined.imageset/InterfaceEssentialLinkedinStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleOutlined.imageset/InterfaceEssentialLinkedinStyleOutlined.pdf new file mode 100644 index 0000000..b14dddf Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialLinkedin/InterfaceEssentialLinkedinStyleOutlined.imageset/InterfaceEssentialLinkedinStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/Contents.json new file mode 100644 index 0000000..a274db6 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "FigmaGen", + "version" : 1 + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleFilled.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleFilled.imageset/Contents.json new file mode 100644 index 0000000..977fa91 --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleFilled.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialTwitterStyleFilled.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleFilled.imageset/InterfaceEssentialTwitterStyleFilled.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleFilled.imageset/InterfaceEssentialTwitterStyleFilled.pdf new file mode 100644 index 0000000..7488b13 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleFilled.imageset/InterfaceEssentialTwitterStyleFilled.pdf differ diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleOutlined.imageset/Contents.json b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleOutlined.imageset/Contents.json new file mode 100644 index 0000000..5e6f3ec --- /dev/null +++ b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleOutlined.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "InterfaceEssentialTwitterStyleOutlined.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "FigmaGen", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : false + } +} \ No newline at end of file diff --git a/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleOutlined.imageset/InterfaceEssentialTwitterStyleOutlined.pdf b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleOutlined.imageset/InterfaceEssentialTwitterStyleOutlined.pdf new file mode 100644 index 0000000..a73f317 Binary files /dev/null and b/Demo/FigmaGenDemo/Resources/Images.xcassets/Generated/Service/InterfaceEssentialTwitter/InterfaceEssentialTwitterStyleOutlined.imageset/InterfaceEssentialTwitterStyleOutlined.pdf differ diff --git a/Demo/FigmaGenDemo/Storyboards/Base.lproj/LaunchScreen.storyboard b/Demo/FigmaGenDemo/Storyboards/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..3ab60af --- /dev/null +++ b/Demo/FigmaGenDemo/Storyboards/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/FigmaGenDemo/Storyboards/Base.lproj/Main.storyboard b/Demo/FigmaGenDemo/Storyboards/Base.lproj/Main.storyboard new file mode 100644 index 0000000..b8e041a --- /dev/null +++ b/Demo/FigmaGenDemo/Storyboards/Base.lproj/Main.storyboard @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/FigmaGenDemo/ViewController.swift b/Demo/FigmaGenDemo/ViewController.swift index 49946ea..87f5276 100644 --- a/Demo/FigmaGenDemo/ViewController.swift +++ b/Demo/FigmaGenDemo/ViewController.swift @@ -1,20 +1,37 @@ // -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence +// ViewController.swift +// FigmaGenDemo +// +// Created by Almaz Ibragimov on 16.11.2019. +// Copyright © 2019 Almaz Ibragimov. All rights reserved. // import UIKit class ViewController: UIViewController { - @IBOutlet private weak var titleLabel: UILabel! - @IBOutlet private weak var subtitleLabel: UILabel! + @IBOutlet private weak var cardView: ShadowStyleView! + @IBOutlet private weak var label: UILabel! + @IBOutlet private weak var imageView: UIImageView! override func viewDidLoad() { super.viewDidLoad() - titleLabel.attributedText = "Title".styled(.title2, textColor: Colors.black) - subtitleLabel.attributedText = "Subtitle".styled(.subtitle2, textColor: Colors.gray) + view.backgroundColor = ColorStyle.razzmatazz.color + view.backgroundColor = UIColor(style: .razzmatazz) + + label.attributedText = TextStyle.title.attributedString("Hello world") + label.attributedText = NSAttributedString(string: "Hello world", style: .title) + + label.attributedText = TextStyle + .title + .withColor(.white) + .withLineBreakMode(.byWordWrapping) + .attributedString("Hello world") + + imageView.image = Images.cloud + + cardView.shadowStyle = .cardShadow + label.shadow = .thinShadow } } diff --git a/Demo/FigmaGenDemoTests/FigmaGenDemoTests.swift b/Demo/FigmaGenDemoTests/FigmaGenDemoTests.swift new file mode 100644 index 0000000..d90e5ef --- /dev/null +++ b/Demo/FigmaGenDemoTests/FigmaGenDemoTests.swift @@ -0,0 +1,25 @@ +// +// FigmaGenDemoTests.swift +// FigmaGenDemoTests +// +// Created by Almaz Ibragimov on 08.02.2020. +// Copyright © 2020 Almaz Ibragimov. All rights reserved. +// + +import XCTest +import FigmaGenDemo + +class FigmaGenDemoTests: XCTestCase { + + func testImages() throws { + try Images.validate() + } + + func testTextStyles() throws { + try TextStyle.validate() + } + + func testTypographies() throws { + try Typography.validate() + } +} diff --git a/Tests/Info.plist b/Demo/FigmaGenDemoTests/Info.plist old mode 100755 new mode 100644 similarity index 93% rename from Tests/Info.plist rename to Demo/FigmaGenDemoTests/Info.plist index 6c40a6c..64d65ca --- a/Tests/Info.plist +++ b/Demo/FigmaGenDemoTests/Info.plist @@ -13,7 +13,7 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - BNDL + $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion diff --git a/Demo/Podfile b/Demo/Podfile new file mode 100644 index 0000000..5003a7a --- /dev/null +++ b/Demo/Podfile @@ -0,0 +1,7 @@ +platform :ios, '13.0' + +target 'FigmaGenDemo' do + use_frameworks! + + pod 'FigmaGen' +end diff --git a/Demo/Podfile.lock b/Demo/Podfile.lock new file mode 100644 index 0000000..cbef881 --- /dev/null +++ b/Demo/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - FigmaGen (1.0.0) + +DEPENDENCIES: + - FigmaGen + +SPEC REPOS: + trunk: + - FigmaGen + +SPEC CHECKSUMS: + FigmaGen: 92645f7e4362261825c151dee0d714061fb134f0 + +PODFILE CHECKSUM: 89af93ce0bdc260ae0acfd95cc62f262401aecb3 + +COCOAPODS: 1.12.1 diff --git a/Demo/Templates/Colors.stencil b/Demo/Templates/Colors.stencil deleted file mode 100644 index 8116df8..0000000 --- a/Demo/Templates/Colors.stencil +++ /dev/null @@ -1,35 +0,0 @@ -// swiftlint:disable all -{% if colors %} -{% macro rgbaHex color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %} -{% macro rgbHexString color %}#{{color.red}}{{color.green}}{{color.blue}}{% endmacro %} -{% macro rgbaHexString color %}{% call rgbHexString color %}{{color.alpha}}{% endmacro %} -{% macro rgbString color %}{{color.red|hexToInt}} {{color.green|hexToInt}} {{color.blue|hexToInt}}{% endmacro %} -{% macro rgbaString color %}{% call rgbString color %}, {{color.alpha|hexToInt|int255toFloat|percent}}{% endmacro %} -{% macro colorName color %}{{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endmacro %} -import UIKit.UIColor - -public enum Colors { -{% for color in colors %} - - /// {{color.name}} - /// - /// Hex: {% call rgbaHexString color %}; rgba: {% call rgbaString color %}. - public static let {% call colorName color %} = UIColor(rgbaHex: {% call rgbaHex color %}) -{% endfor %} -} - -private extension UIColor { - - convenience init(rgbaHex: UInt32) { - self.init( - red: CGFloat((rgbaHex >> 24) & 0xFF) / 255.0, - green: CGFloat((rgbaHex >> 16) & 0xFF) / 255.0, - blue: CGFloat((rgbaHex >> 8) & 0xFF) / 255.0, - alpha: CGFloat(rgbaHex & 0xFF) / 255.0 - ) - } -} -{% else %} -// No color found -{% endif %} -// swiftlint:enable all diff --git a/Demo/Templates/Spacings.stencil b/Demo/Templates/Spacings.stencil deleted file mode 100644 index 5a78d6f..0000000 --- a/Demo/Templates/Spacings.stencil +++ /dev/null @@ -1,27 +0,0 @@ -// swiftlint:disable all -{% if spacings %} -import UIKit - -public protocol Spacing { - init(_ value: Double) -} - -extension Int: Spacing { } -extension UInt: Spacing { } -extension Float: Spacing { } -extension Double: Spacing { } -extension CGFloat: Spacing { } - -extension Spacing { -{% for spacing in spacings %} - - /// {{ spacing.name }} - /// - /// Value: {{ spacing.value }}. - public static var {{ spacing.name|lowercase }}: Self { Self({{ spacing.value }}) } -{% endfor %} -} -{% else %} -// No spacings found -{% endif %} -// swiftlint:enable all diff --git a/Demo/Templates/TextStyles.stencil b/Demo/Templates/TextStyles.stencil deleted file mode 100644 index 0c36035..0000000 --- a/Demo/Templates/TextStyles.stencil +++ /dev/null @@ -1,193 +0,0 @@ -// swiftlint:disable all -{% if textStyles %} -{% macro rgbaHex color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %} -{% macro rgbHexString color %}#{{color.red}}{{color.green}}{{color.blue}}{% endmacro %} -{% macro rgbaHexString color %}{% call rgbHexString color %}{{color.alpha}}{% endmacro %} -{% macro rgbString color %}{{color.red|hexToInt}} {{color.green|hexToInt}} {{color.blue|hexToInt}}{% endmacro %} -{% macro rgbaString color %}{% call rgbString color %}, {{color.alpha|hexToInt|int255toFloat|percent}}{% endmacro %} -{% macro colorValue color %}hex: {% call rgbaHexString color %}; rgba: {% call rgbaString color %}{% endmacro %} -{% macro colorStyle color %}{% if color.name %}{{color.name}}; {% endif %}{% endmacro %} -{% macro colorDescription color %}{% call colorStyle color %}{% call colorValue color %}{% endmacro %} -{% macro fontName textStyle %}{{textStyle.fontFamily}} ({{textStyle.fontPostScriptName}}); {% endmacro %} -{% macro fontWeight textStyle %}weight {{textStyle.fontWeight}}; {% endmacro %} -{% macro fontSize textStyle %}size {{textStyle.fontSize}}{% endmacro %} -{% macro fontDescription textStyle %}{% call fontName textStyle %}{% call fontWeight textStyle %}{% call fontSize textStyle %}{% endmacro %} -{% macro textStyleName textStyle %}{{textStyle.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endmacro %} -import Foundation -import UIKit - -public struct TextStyle: Equatable { - - public let font: UIFont - public let textColor: UIColor - public let paragraphSpacing: CGFloat? - public let paragraphIndent: CGFloat? - public let lineHeight: CGFloat? - public let letterSpacing: CGFloat? - - public var actualLineHeight: CGFloat { - return lineHeight ?? font.lineHeight - } - - public init( - font: UIFont, - textColor: UIColor, - paragraphSpacing: CGFloat? = nil, - paragraphIndent: CGFloat? = nil, - lineHeight: CGFloat? = nil, - letterSpacing: CGFloat? = nil - ) { - self.font = font - self.textColor = textColor - self.paragraphSpacing = paragraphSpacing - self.paragraphIndent = paragraphIndent - self.lineHeight = lineHeight - self.letterSpacing = letterSpacing - } - - public init( - fontName: String, - fontSize: CGFloat, - textColor: UIColor, - paragraphSpacing: CGFloat? = nil, - paragraphIndent: CGFloat? = nil, - lineHeight: CGFloat? = nil, - letterSpacing: CGFloat? = nil - ) { - self.init( - font: UIFont(name: fontName, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize), - textColor: textColor, - paragraphSpacing: paragraphSpacing, - paragraphIndent: paragraphIndent, - lineHeight: lineHeight, - letterSpacing: letterSpacing - ) - } - - public func withTextColor(_ textColor: UIColor) -> TextStyle { - return TextStyle( - font: font, - textColor: textColor, - paragraphSpacing: paragraphSpacing, - paragraphIndent: paragraphIndent, - lineHeight: lineHeight, - letterSpacing: letterSpacing - ) - } - - public func attributes( - textColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - alignment: NSTextAlignment? = nil, - lineBreakMode: NSLineBreakMode? = nil, - ignoringParagraphStyle: Bool = false - ) -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: textColor ?? self.textColor, - ] - - if let backgroundColor = backgroundColor { - attributes[.backgroundColor] = backgroundColor - } - - if let letterSpacing = letterSpacing { - attributes[.kern] = NSNumber(value: Float(letterSpacing)) - } - - if ignoringParagraphStyle { - return attributes - } - - let paragraphStyle = NSMutableParagraphStyle() - - if let lineHeight = lineHeight { - let paragraphLineSpacing = (lineHeight - font.lineHeight) / 2.0 - let paragraphLineHeight = lineHeight - paragraphLineSpacing - - paragraphStyle.lineSpacing = paragraphLineSpacing - paragraphStyle.minimumLineHeight = paragraphLineHeight - paragraphStyle.maximumLineHeight = paragraphLineHeight - } - - if let paragraphSpacing = paragraphSpacing { - paragraphStyle.paragraphSpacing = paragraphSpacing - } - - if let paragraphIndent = paragraphIndent { - paragraphStyle.firstLineHeadIndent = paragraphIndent - } - - if let alignment = alignment { - paragraphStyle.alignment = alignment - } - - if let lineBreakMode = lineBreakMode { - paragraphStyle.lineBreakMode = lineBreakMode - } - - attributes[.paragraphStyle] = paragraphStyle - - return attributes - } -} - -public extension TextStyle { -{% for textStyle in textStyles %} - - /// {{textStyle.name}} - /// - /// Font: {% call fontDescription textStyle %} - /// Text color: {% call colorDescription textStyle.textColor %} - /// Paragraph spacing: {{textStyle.paragraphSpacing|default:"default"}} - /// Paragraph indent: {{textStyle.paragraphIndent|default:"default"}} - /// Line height: {{textStyle.lineHeight|default:"default"}} - /// Letter spacing: {{textStyle.letterSpacing|default:"default"}} - static let {% call textStyleName textStyle %} = TextStyle( - fontName: "{{textStyle.fontPostScriptName}}", - fontSize: {{textStyle.fontSize}}, - textColor: UIColor(rgbaHex: {% call rgbaHex textStyle.textColor %}), - paragraphSpacing: {{textStyle.paragraphSpacing|default:"nil"}}, - paragraphIndent: {{textStyle.paragraphIndent|default:"nil"}}, - lineHeight: {{textStyle.lineHeight|default:"nil"}}, - letterSpacing: {{textStyle.letterSpacing|default:"nil"}} - ) -{% endfor %} -} - -public extension String { - - func styled( - _ textStyle: TextStyle, - textColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - alignment: NSTextAlignment? = nil, - lineBreakMode: NSLineBreakMode? = nil - ) -> NSAttributedString { - return NSAttributedString( - string: self, - attributes: textStyle.attributes( - textColor: textColor, - backgroundColor: backgroundColor, - alignment: alignment, - lineBreakMode: lineBreakMode - ) - ) - } -} - -private extension UIColor { - - convenience init(rgbaHex: UInt32) { - self.init( - red: CGFloat((rgbaHex >> 24) & 0xFF) / 255.0, - green: CGFloat((rgbaHex >> 16) & 0xFF) / 255.0, - blue: CGFloat((rgbaHex >> 8) & 0xFF) / 255.0, - alpha: CGFloat(rgbaHex & 0xFF) / 255.0 - ) - } -} -{% else %} -// No text style found -{% endif %} -// swiftlint:enable all diff --git a/Demo/figmagen b/Demo/figmagen deleted file mode 100755 index b97ccf1..0000000 Binary files a/Demo/figmagen and /dev/null differ diff --git a/Docs/AccessToken.png b/Docs/AccessToken.png new file mode 100644 index 0000000..7dbc675 Binary files /dev/null and b/Docs/AccessToken.png differ diff --git a/Docs/Choose_and_Edit_Scheme.png b/Docs/Choose_and_Edit_Scheme.png new file mode 100644 index 0000000..57e515a Binary files /dev/null and b/Docs/Choose_and_Edit_Scheme.png differ diff --git a/Docs/FileURL.png b/Docs/FileURL.png new file mode 100644 index 0000000..19d4dd2 Binary files /dev/null and b/Docs/FileURL.png differ diff --git a/Docs/Generate_from_config.png b/Docs/Generate_from_config.png new file mode 100644 index 0000000..e2d4149 Binary files /dev/null and b/Docs/Generate_from_config.png differ diff --git a/Docs/Generate_tokens_from_Figma.png b/Docs/Generate_tokens_from_Figma.png new file mode 100644 index 0000000..db55ed9 Binary files /dev/null and b/Docs/Generate_tokens_from_Figma.png differ diff --git a/Docs/Generate_tokens_from_GitHub_file.png b/Docs/Generate_tokens_from_GitHub_file.png new file mode 100644 index 0000000..7f3fa60 Binary files /dev/null and b/Docs/Generate_tokens_from_GitHub_file.png differ diff --git a/Docs/PlayVideo.png b/Docs/PlayVideo.png new file mode 100644 index 0000000..514ee8d Binary files /dev/null and b/Docs/PlayVideo.png differ diff --git a/FigmaGen.podspec b/FigmaGen.podspec index c2ef4df..985aa2f 100644 --- a/FigmaGen.podspec +++ b/FigmaGen.podspec @@ -1,12 +1,11 @@ Pod::Spec.new do |spec| spec.name = 'FigmaGen' spec.version = `make version` - spec.summary = 'A tool to automate resources using the Figma API.' - spec.osx.deployment_target = '10.12' + spec.summary = 'Swift code & resources generator for your Figma files.' spec.homepage = 'https://github.com/hhru/FigmaGen' spec.license = { :type => 'MIT', :file => 'LICENSE' } - spec.author = { 'HeadHunter iOS Team' => 'https://hh.ru' } + spec.author = { "Almaz Ibragimov" => "almazrafi@gmail.com" } spec.source = { http: "https://github.com/hhru/FigmaGen/releases/download/#{spec.version}/figmagen-#{spec.version}.zip" @@ -14,4 +13,9 @@ Pod::Spec.new do |spec| spec.preserve_paths = '*' spec.exclude_files = '**/file.zip' + + spec.ios.deployment_target = '11.0' + spec.osx.deployment_target = '10.13' + spec.watchos.deployment_target = '4.0' + spec.tvos.deployment_target = '11.0' end diff --git a/FigmaGen.xcodeproj/project.pbxproj b/FigmaGen.xcodeproj/project.pbxproj deleted file mode 100644 index 83f9317..0000000 --- a/FigmaGen.xcodeproj/project.pbxproj +++ /dev/null @@ -1,1152 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 52; - objects = { - -/* Begin PBXBuildFile section */ - 0CEFBD150631EBC2D1E8D5C9 /* Pods_FigmaGen.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7CCE646935D17668E376318 /* Pods_FigmaGen.framework */; }; - 275066ECF0967BD89155A3A8 /* Pods_FigmaGenTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1FBD58D183E7F79857C9AD1 /* Pods_FigmaGenTests.framework */; }; - C00380B02358929700E57CCE /* FigmaAPIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C00380AF2358929700E57CCE /* FigmaAPIProvider.swift */; }; - C05FD336235CBBE90042552E /* UnkeyedDecodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05FD32F235CBBE90042552E /* UnkeyedDecodingContainer+Extensions.swift */; }; - C05FD33A235CBBE90042552E /* SingleValueDecodingContainer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05FD333235CBBE90042552E /* SingleValueDecodingContainer+Extensions.swift */; }; - C05FD33C235CBBE90042552E /* KeyedDecodingContainerProtocol+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05FD335235CBBE90042552E /* KeyedDecodingContainerProtocol+Extensions.swift */; }; - C05FD353235CC33E0042552E /* FigmaAPIFileRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B985842358EFBA00D8B21D /* FigmaAPIFileRoute.swift */; }; - C06B03CC236302C900DC2754 /* ProcessInfo+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06B03CB236302C900DC2754 /* ProcessInfo+Extensions.swift */; }; - C06B03CE2363301F00DC2754 /* ColorsServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06B03CD2363301F00DC2754 /* ColorsServices.swift */; }; - C06B03D223633B0400DC2754 /* Routable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06B03D123633B0400DC2754 /* Routable+Extensions.swift */; }; - C06B053023633E9200DC2754 /* Services.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06B052F23633E9200DC2754 /* Services.swift */; }; - C06EC14E2371AF790086B2D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06EC14D2371AF790086B2D1 /* Configuration.swift */; }; - C06EC1512371B0CA0086B2D1 /* StepConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06EC14F2371AFCF0086B2D1 /* StepConfiguration.swift */; }; - C06EC1572371B4850086B2D1 /* TextStylesServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F5D23686EFE00BE7DE8 /* TextStylesServices.swift */; }; - C06EC15B2371C6820086B2D1 /* ColorsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06EC15A2371C6820086B2D1 /* ColorsGenerator.swift */; }; - C06EC1612371C8CA0086B2D1 /* GenerateCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06EC1602371C8CA0086B2D1 /* GenerateCommand.swift */; }; - C0778EA02361CAAC00D020FB /* ColorsRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0778E9C2361CAAC00D020FB /* ColorsRenderer.swift */; }; - C0778EA12361CAAC00D020FB /* DefaultColorsRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0778E9D2361CAAC00D020FB /* DefaultColorsRenderer.swift */; }; - C0778EA62362060100D020FB /* Path+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0778EA52362060100D020FB /* Path+Extensions.swift */; }; - C0778EA823620A2300D020FB /* Templates in Resources */ = {isa = PBXBuildFile; fileRef = C0778EA723620A2300D020FB /* Templates */; }; - C0778EAB2362E8D200D020FB /* ColorsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0778EAA2362E8D200D020FB /* ColorsCommand.swift */; }; - C0778EAD2362F21C00D020FB /* TemplateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0778EAC2362F21C00D020FB /* TemplateType.swift */; }; - C0A4A160235F5A390063FBE4 /* FigmaExportSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A12D235F5A390063FBE4 /* FigmaExportSetting.swift */; }; - C0A4A161235F5A390063FBE4 /* FigmaStrokeAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A12E235F5A390063FBE4 /* FigmaStrokeAlignment.swift */; }; - C0A4A162235F5A390063FBE4 /* FigmaStyleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A12F235F5A390063FBE4 /* FigmaStyleType.swift */; }; - C0A4A163235F5A390063FBE4 /* FigmaTextVerticalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A130235F5A390063FBE4 /* FigmaTextVerticalAlignment.swift */; }; - C0A4A164235F5A390063FBE4 /* FigmaPaint.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A131235F5A390063FBE4 /* FigmaPaint.swift */; }; - C0A4A165235F5A390063FBE4 /* FigmaFrameOffset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A132235F5A390063FBE4 /* FigmaFrameOffset.swift */; }; - C0A4A166235F5A390063FBE4 /* FigmaFrameNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A134235F5A390063FBE4 /* FigmaFrameNodeInfo.swift */; }; - C0A4A167235F5A390063FBE4 /* FigmaTextNodePayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A135235F5A390063FBE4 /* FigmaTextNodePayload.swift */; }; - C0A4A168235F5A390063FBE4 /* FigmaNodeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A136235F5A390063FBE4 /* FigmaNodeType.swift */; }; - C0A4A169235F5A390063FBE4 /* FigmaDocumentNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A137235F5A390063FBE4 /* FigmaDocumentNodeInfo.swift */; }; - C0A4A16B235F5A390063FBE4 /* FigmaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A139235F5A390063FBE4 /* FigmaFile.swift */; }; - C0A4A16C235F5A390063FBE4 /* FigmaRectangleNodePayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A13A235F5A390063FBE4 /* FigmaRectangleNodePayload.swift */; }; - C0A4A16E235F5A390063FBE4 /* FigmaCanvasNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A13C235F5A390063FBE4 /* FigmaCanvasNodeInfo.swift */; }; - C0A4A16F235F5A390063FBE4 /* FigmaSliceNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A13D235F5A390063FBE4 /* FigmaSliceNodeInfo.swift */; }; - C0A4A170235F5A390063FBE4 /* FigmaInstanceNodePayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A13E235F5A390063FBE4 /* FigmaInstanceNodePayload.swift */; }; - C0A4A171235F5A390063FBE4 /* FigmaNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A13F235F5A390063FBE4 /* FigmaNode.swift */; }; - C0A4A172235F5A390063FBE4 /* FigmaVectorNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A140235F5A390063FBE4 /* FigmaVectorNodeInfo.swift */; }; - C0A4A173235F5A390063FBE4 /* FigmaBooleanOperationNodePayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A141235F5A390063FBE4 /* FigmaBooleanOperationNodePayload.swift */; }; - C0A4A174235F5A390063FBE4 /* FigmaLayoutGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A142235F5A390063FBE4 /* FigmaLayoutGrid.swift */; }; - C0A4A175235F5A390063FBE4 /* FigmaLineHeightUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A143235F5A390063FBE4 /* FigmaLineHeightUnit.swift */; }; - C0A4A176235F5A390063FBE4 /* FigmaTypeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A144235F5A390063FBE4 /* FigmaTypeStyle.swift */; }; - C0A4A177235F5A390063FBE4 /* FigmaScaleMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A145235F5A390063FBE4 /* FigmaScaleMode.swift */; }; - C0A4A178235F5A390063FBE4 /* FigmaEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A146235F5A390063FBE4 /* FigmaEffect.swift */; }; - C0A4A179235F5A390063FBE4 /* FigmaBlendMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A147235F5A390063FBE4 /* FigmaBlendMode.swift */; }; - C0A4A17A235F5A390063FBE4 /* FigmaLayoutVerticalConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A148235F5A390063FBE4 /* FigmaLayoutVerticalConstraint.swift */; }; - C0A4A17B235F5A390063FBE4 /* FigmaColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A149235F5A390063FBE4 /* FigmaColor.swift */; }; - C0A4A17C235F5A390063FBE4 /* FigmaVector.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A14A235F5A390063FBE4 /* FigmaVector.swift */; }; - C0A4A17D235F5A390063FBE4 /* FigmaLayoutGridAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A14B235F5A390063FBE4 /* FigmaLayoutGridAlignment.swift */; }; - C0A4A17E235F5A390063FBE4 /* FigmaStrokeJoin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A14C235F5A390063FBE4 /* FigmaStrokeJoin.swift */; }; - C0A4A17F235F5A390063FBE4 /* FigmaLayoutHorizontalConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A14D235F5A390063FBE4 /* FigmaLayoutHorizontalConstraint.swift */; }; - C0A4A180235F5A390063FBE4 /* FigmaComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A14E235F5A390063FBE4 /* FigmaComponent.swift */; }; - C0A4A181235F5A390063FBE4 /* FigmaEasingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A14F235F5A390063FBE4 /* FigmaEasingType.swift */; }; - C0A4A182235F5A390063FBE4 /* FigmaColorStop.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A150235F5A390063FBE4 /* FigmaColorStop.swift */; }; - C0A4A183235F5A390063FBE4 /* FigmaStrokeCap.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A151235F5A390063FBE4 /* FigmaStrokeCap.swift */; }; - C0A4A184235F5A390063FBE4 /* FigmaLayoutGridPattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A152235F5A390063FBE4 /* FigmaLayoutGridPattern.swift */; }; - C0A4A185235F5A390063FBE4 /* FigmaLayoutConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A153235F5A390063FBE4 /* FigmaLayoutConstraint.swift */; }; - C0A4A186235F5A390063FBE4 /* FigmaTextHorizontalAlignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A154235F5A390063FBE4 /* FigmaTextHorizontalAlignment.swift */; }; - C0A4A187235F5A390063FBE4 /* FigmaTextCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A155235F5A390063FBE4 /* FigmaTextCase.swift */; }; - C0A4A188235F5A390063FBE4 /* FigmaConstraintType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A156235F5A390063FBE4 /* FigmaConstraintType.swift */; }; - C0A4A189235F5A390063FBE4 /* FigmaTextDecoration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A157235F5A390063FBE4 /* FigmaTextDecoration.swift */; }; - C0A4A18A235F5A390063FBE4 /* FigmaStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A158235F5A390063FBE4 /* FigmaStyle.swift */; }; - C0A4A18B235F5A390063FBE4 /* FigmaEffectType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A159235F5A390063FBE4 /* FigmaEffectType.swift */; }; - C0A4A18C235F5A390063FBE4 /* FigmaImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A15A235F5A390063FBE4 /* FigmaImageFormat.swift */; }; - C0A4A18D235F5A390063FBE4 /* FigmaRectangle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A15B235F5A390063FBE4 /* FigmaRectangle.swift */; }; - C0A4A18E235F5A390063FBE4 /* FigmaPaintType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A15C235F5A390063FBE4 /* FigmaPaintType.swift */; }; - C0A4A18F235F5A390063FBE4 /* FigmaBooleanOperationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A15D235F5A390063FBE4 /* FigmaBooleanOperationType.swift */; }; - C0A4A190235F5A390063FBE4 /* FigmaConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A15E235F5A390063FBE4 /* FigmaConstraint.swift */; }; - C0A4A191235F5A390063FBE4 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A15F235F5A390063FBE4 /* Color.swift */; }; - C0A4A197235F5AC40063FBE4 /* ColorsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A193235F5AC40063FBE4 /* ColorsProvider.swift */; }; - C0A4A199235F5AC40063FBE4 /* DefaultColorsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A195235F5AC40063FBE4 /* DefaultColorsProvider.swift */; }; - C0A4A19C235F5ACA0063FBE4 /* DefaultFigmaAPIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A19B235F5ACA0063FBE4 /* DefaultFigmaAPIProvider.swift */; }; - C0A4A19E235F6ABA0063FBE4 /* ColorsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A19D235F6ABA0063FBE4 /* ColorsError.swift */; }; - C0A4A1A3235FA7880063FBE4 /* Sequence+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A4A1A2235FA7880063FBE4 /* Sequence+Extensions.swift */; }; - C0A7A15F236874360087A369 /* DefaultTextStylesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A7A15E236874360087A369 /* DefaultTextStylesProvider.swift */; }; - C0A7A16123687A410087A369 /* DefaultTextStylesRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A7A16023687A410087A369 /* DefaultTextStylesRenderer.swift */; }; - C0A7A163236891E70087A369 /* TextStylesError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A7A162236891E70087A369 /* TextStylesError.swift */; }; - C0AF9F5223685F2600BE7DE8 /* NodesExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F5123685F2600BE7DE8 /* NodesExtractor.swift */; }; - C0AF9F5423685F4100BE7DE8 /* DefaultNodesExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F5323685F4100BE7DE8 /* DefaultNodesExtractor.swift */; }; - C0AF9F5823686E5200BE7DE8 /* TextStylesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F5723686E5200BE7DE8 /* TextStylesProvider.swift */; }; - C0AF9F5A23686E7700BE7DE8 /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F5923686E7700BE7DE8 /* TextStyle.swift */; }; - C0AF9F6023686F1900BE7DE8 /* TextStylesCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F5F23686F1900BE7DE8 /* TextStylesCommand.swift */; }; - C0AF9F62236870D200BE7DE8 /* TextStylesRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0AF9F61236870D200BE7DE8 /* TextStylesRenderer.swift */; }; - C0B5CA632372D3F7007A4B83 /* TextStylesGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B5CA622372D3F7007A4B83 /* TextStylesGenerator.swift */; }; - C0E034E52372EDFC004BE218 /* GenerateServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E034E42372EDFC004BE218 /* GenerateServices.swift */; }; - C0E034F123730607004BE218 /* BaseConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E034F023730607004BE218 /* BaseConfiguration.swift */; }; - C0E034F723730E53004BE218 /* ConfigurationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E034EA237302D4004BE218 /* ConfigurationError.swift */; }; - C0E0CA34236C7F5E004BA240 /* Colors.stencil in Resources */ = {isa = PBXBuildFile; fileRef = C0778D502360A0C100D020FB /* Colors.stencil */; }; - C0E0CA35236C7F61004BA240 /* TextStyles.stencil in Resources */ = {isa = PBXBuildFile; fileRef = C0268E33236B18FE00900532 /* TextStyles.stencil */; }; - C0E0CB6B236C8D6A004BA240 /* FloatingPoint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0E0CB6A236C8D6A004BA240 /* FloatingPoint+Extensions.swift */; }; - C0F194832356117200A24BDE /* FigmaGenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F194822356117200A24BDE /* FigmaGenTests.swift */; }; - C0F196012356260100A24BDE /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F194752356112F00A24BDE /* main.swift */; }; - C0F610FE236341B200960CD8 /* CLI+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F610FD236341B200960CD8 /* CLI+Extensions.swift */; }; - C0F775AE235846CC00389DD2 /* FigmaAPIRoute.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F775AD235846CC00389DD2 /* FigmaAPIRoute.swift */; }; - C0F775B0235846E400389DD2 /* FigmaAPIVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F775AF235846E400389DD2 /* FigmaAPIVersion.swift */; }; - C0F775B22358473A00389DD2 /* FigmaAPIHTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F775B12358473A00389DD2 /* FigmaAPIHTTPMethod.swift */; }; - C0FE20712395E0DE003A6701 /* NodesError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FE20702395E0DE003A6701 /* NodesError.swift */; }; - DBCFD32124BCAC660061478A /* Spacings.stencil in Resources */ = {isa = PBXBuildFile; fileRef = DBCFD32024BCAC660061478A /* Spacings.stencil */; }; - DBCFD32424BCAE310061478A /* SpacingsServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD32324BCAE310061478A /* SpacingsServices.swift */; }; - DBCFD32724BCAF0F0061478A /* SpacingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD32624BCAF0F0061478A /* SpacingsProvider.swift */; }; - DBCFD32924BCAF400061478A /* Spacing.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD32824BCAF400061478A /* Spacing.swift */; }; - DBCFD32B24BCAF7A0061478A /* SpacingsRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD32A24BCAF7A0061478A /* SpacingsRenderer.swift */; }; - DBCFD32D24BCB06B0061478A /* DefaultSpacingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD32C24BCB06B0061478A /* DefaultSpacingsProvider.swift */; }; - DBCFD32F24BCB0E10061478A /* DefaultSpacingsRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD32E24BCB0E10061478A /* DefaultSpacingsRenderer.swift */; }; - DBCFD33124BCB74C0061478A /* SpacingsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD33024BCB74C0061478A /* SpacingsError.swift */; }; - DBCFD33324BCBAD80061478A /* SpacingsGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD33224BCBAD80061478A /* SpacingsGenerator.swift */; }; - DBCFD33524BCBBB70061478A /* SpacingsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCFD33424BCBBB70061478A /* SpacingsCommand.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - C085A5AA2356328C00C56581 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C0F1946A2356112F00A24BDE /* Project object */; - proxyType = 1; - remoteGlobalIDString = C0F195F22356256A00A24BDE; - remoteInfo = FigmaGen; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 2EEDD6551CD0211C1EF2367B /* Pods-FigmaGenTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FigmaGenTests.debug.xcconfig"; path = "Target Support Files/Pods-FigmaGenTests/Pods-FigmaGenTests.debug.xcconfig"; sourceTree = ""; }; - 79A9EBE6548AF631B5702EDE /* Pods-FigmaGen.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FigmaGen.release.xcconfig"; path = "Target Support Files/Pods-FigmaGen/Pods-FigmaGen.release.xcconfig"; sourceTree = ""; }; - 8E7C17A850D86C8D1A110F2C /* Pods-FigmaGenTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FigmaGenTests.release.xcconfig"; path = "Target Support Files/Pods-FigmaGenTests/Pods-FigmaGenTests.release.xcconfig"; sourceTree = ""; }; - A1FBD58D183E7F79857C9AD1 /* Pods_FigmaGenTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FigmaGenTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - A7CCE646935D17668E376318 /* Pods_FigmaGen.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FigmaGen.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C00380AF2358929700E57CCE /* FigmaAPIProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FigmaAPIProvider.swift; sourceTree = ""; }; - C0268E33236B18FE00900532 /* TextStyles.stencil */ = {isa = PBXFileReference; lastKnownFileType = text; path = TextStyles.stencil; sourceTree = ""; }; - C05FD32F235CBBE90042552E /* UnkeyedDecodingContainer+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UnkeyedDecodingContainer+Extensions.swift"; sourceTree = ""; }; - C05FD333235CBBE90042552E /* SingleValueDecodingContainer+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SingleValueDecodingContainer+Extensions.swift"; sourceTree = ""; }; - C05FD335235CBBE90042552E /* KeyedDecodingContainerProtocol+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KeyedDecodingContainerProtocol+Extensions.swift"; sourceTree = ""; }; - C06B03CB236302C900DC2754 /* ProcessInfo+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+Extensions.swift"; sourceTree = ""; }; - C06B03CD2363301F00DC2754 /* ColorsServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsServices.swift; sourceTree = ""; }; - C06B03D123633B0400DC2754 /* Routable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Routable+Extensions.swift"; sourceTree = ""; }; - C06B052F23633E9200DC2754 /* Services.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Services.swift; sourceTree = ""; }; - C06EC14D2371AF790086B2D1 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; - C06EC14F2371AFCF0086B2D1 /* StepConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepConfiguration.swift; sourceTree = ""; }; - C06EC15A2371C6820086B2D1 /* ColorsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsGenerator.swift; sourceTree = ""; }; - C06EC1602371C8CA0086B2D1 /* GenerateCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateCommand.swift; sourceTree = ""; }; - C0778D502360A0C100D020FB /* Colors.stencil */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Colors.stencil; sourceTree = ""; }; - C0778E9C2361CAAC00D020FB /* ColorsRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorsRenderer.swift; sourceTree = ""; }; - C0778E9D2361CAAC00D020FB /* DefaultColorsRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultColorsRenderer.swift; sourceTree = ""; }; - C0778EA52362060100D020FB /* Path+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Path+Extensions.swift"; sourceTree = ""; }; - C0778EA723620A2300D020FB /* Templates */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Templates; sourceTree = SOURCE_ROOT; }; - C0778EAA2362E8D200D020FB /* ColorsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsCommand.swift; sourceTree = ""; }; - C0778EAC2362F21C00D020FB /* TemplateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateType.swift; sourceTree = ""; }; - C085A5AC2356350F00C56581 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Makefile; sourceTree = SOURCE_ROOT; }; - C085A5AD2356350F00C56581 /* Gemfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Gemfile; sourceTree = SOURCE_ROOT; }; - C085A5AE2356351000C56581 /* FigmaGen.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = FigmaGen.podspec; sourceTree = SOURCE_ROOT; }; - C085A5AF2356351000C56581 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; - C085A5B02356351000C56581 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = SOURCE_ROOT; }; - C085A5B12356351000C56581 /* Package.resources */ = {isa = PBXFileReference; lastKnownFileType = text; path = Package.resources; sourceTree = SOURCE_ROOT; }; - C085A5B22356351000C56581 /* Podfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Podfile; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - C085A5B32356351000C56581 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; - C0A4A12D235F5A390063FBE4 /* FigmaExportSetting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaExportSetting.swift; sourceTree = ""; }; - C0A4A12E235F5A390063FBE4 /* FigmaStrokeAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaStrokeAlignment.swift; sourceTree = ""; }; - C0A4A12F235F5A390063FBE4 /* FigmaStyleType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaStyleType.swift; sourceTree = ""; }; - C0A4A130235F5A390063FBE4 /* FigmaTextVerticalAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaTextVerticalAlignment.swift; sourceTree = ""; }; - C0A4A131235F5A390063FBE4 /* FigmaPaint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaPaint.swift; sourceTree = ""; }; - C0A4A132235F5A390063FBE4 /* FigmaFrameOffset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaFrameOffset.swift; sourceTree = ""; }; - C0A4A134235F5A390063FBE4 /* FigmaFrameNodeInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaFrameNodeInfo.swift; sourceTree = ""; }; - C0A4A135235F5A390063FBE4 /* FigmaTextNodePayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaTextNodePayload.swift; sourceTree = ""; }; - C0A4A136235F5A390063FBE4 /* FigmaNodeType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaNodeType.swift; sourceTree = ""; }; - C0A4A137235F5A390063FBE4 /* FigmaDocumentNodeInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaDocumentNodeInfo.swift; sourceTree = ""; }; - C0A4A139235F5A390063FBE4 /* FigmaFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaFile.swift; sourceTree = ""; }; - C0A4A13A235F5A390063FBE4 /* FigmaRectangleNodePayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaRectangleNodePayload.swift; sourceTree = ""; }; - C0A4A13C235F5A390063FBE4 /* FigmaCanvasNodeInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaCanvasNodeInfo.swift; sourceTree = ""; }; - C0A4A13D235F5A390063FBE4 /* FigmaSliceNodeInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaSliceNodeInfo.swift; sourceTree = ""; }; - C0A4A13E235F5A390063FBE4 /* FigmaInstanceNodePayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaInstanceNodePayload.swift; sourceTree = ""; }; - C0A4A13F235F5A390063FBE4 /* FigmaNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaNode.swift; sourceTree = ""; }; - C0A4A140235F5A390063FBE4 /* FigmaVectorNodeInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaVectorNodeInfo.swift; sourceTree = ""; }; - C0A4A141235F5A390063FBE4 /* FigmaBooleanOperationNodePayload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaBooleanOperationNodePayload.swift; sourceTree = ""; }; - C0A4A142235F5A390063FBE4 /* FigmaLayoutGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLayoutGrid.swift; sourceTree = ""; }; - C0A4A143235F5A390063FBE4 /* FigmaLineHeightUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLineHeightUnit.swift; sourceTree = ""; }; - C0A4A144235F5A390063FBE4 /* FigmaTypeStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaTypeStyle.swift; sourceTree = ""; }; - C0A4A145235F5A390063FBE4 /* FigmaScaleMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaScaleMode.swift; sourceTree = ""; }; - C0A4A146235F5A390063FBE4 /* FigmaEffect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaEffect.swift; sourceTree = ""; }; - C0A4A147235F5A390063FBE4 /* FigmaBlendMode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaBlendMode.swift; sourceTree = ""; }; - C0A4A148235F5A390063FBE4 /* FigmaLayoutVerticalConstraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLayoutVerticalConstraint.swift; sourceTree = ""; }; - C0A4A149235F5A390063FBE4 /* FigmaColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaColor.swift; sourceTree = ""; }; - C0A4A14A235F5A390063FBE4 /* FigmaVector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaVector.swift; sourceTree = ""; }; - C0A4A14B235F5A390063FBE4 /* FigmaLayoutGridAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLayoutGridAlignment.swift; sourceTree = ""; }; - C0A4A14C235F5A390063FBE4 /* FigmaStrokeJoin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaStrokeJoin.swift; sourceTree = ""; }; - C0A4A14D235F5A390063FBE4 /* FigmaLayoutHorizontalConstraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLayoutHorizontalConstraint.swift; sourceTree = ""; }; - C0A4A14E235F5A390063FBE4 /* FigmaComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaComponent.swift; sourceTree = ""; }; - C0A4A14F235F5A390063FBE4 /* FigmaEasingType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaEasingType.swift; sourceTree = ""; }; - C0A4A150235F5A390063FBE4 /* FigmaColorStop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaColorStop.swift; sourceTree = ""; }; - C0A4A151235F5A390063FBE4 /* FigmaStrokeCap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaStrokeCap.swift; sourceTree = ""; }; - C0A4A152235F5A390063FBE4 /* FigmaLayoutGridPattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLayoutGridPattern.swift; sourceTree = ""; }; - C0A4A153235F5A390063FBE4 /* FigmaLayoutConstraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaLayoutConstraint.swift; sourceTree = ""; }; - C0A4A154235F5A390063FBE4 /* FigmaTextHorizontalAlignment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaTextHorizontalAlignment.swift; sourceTree = ""; }; - C0A4A155235F5A390063FBE4 /* FigmaTextCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaTextCase.swift; sourceTree = ""; }; - C0A4A156235F5A390063FBE4 /* FigmaConstraintType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaConstraintType.swift; sourceTree = ""; }; - C0A4A157235F5A390063FBE4 /* FigmaTextDecoration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaTextDecoration.swift; sourceTree = ""; }; - C0A4A158235F5A390063FBE4 /* FigmaStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaStyle.swift; sourceTree = ""; }; - C0A4A159235F5A390063FBE4 /* FigmaEffectType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaEffectType.swift; sourceTree = ""; }; - C0A4A15A235F5A390063FBE4 /* FigmaImageFormat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaImageFormat.swift; sourceTree = ""; }; - C0A4A15B235F5A390063FBE4 /* FigmaRectangle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaRectangle.swift; sourceTree = ""; }; - C0A4A15C235F5A390063FBE4 /* FigmaPaintType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaPaintType.swift; sourceTree = ""; }; - C0A4A15D235F5A390063FBE4 /* FigmaBooleanOperationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaBooleanOperationType.swift; sourceTree = ""; }; - C0A4A15E235F5A390063FBE4 /* FigmaConstraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FigmaConstraint.swift; sourceTree = ""; }; - C0A4A15F235F5A390063FBE4 /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; - C0A4A193235F5AC40063FBE4 /* ColorsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorsProvider.swift; sourceTree = ""; }; - C0A4A195235F5AC40063FBE4 /* DefaultColorsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultColorsProvider.swift; sourceTree = ""; }; - C0A4A19B235F5ACA0063FBE4 /* DefaultFigmaAPIProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultFigmaAPIProvider.swift; sourceTree = ""; }; - C0A4A19D235F6ABA0063FBE4 /* ColorsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsError.swift; sourceTree = ""; }; - C0A4A1A2235FA7880063FBE4 /* Sequence+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Extensions.swift"; sourceTree = ""; }; - C0A7A15E236874360087A369 /* DefaultTextStylesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextStylesProvider.swift; sourceTree = ""; }; - C0A7A16023687A410087A369 /* DefaultTextStylesRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextStylesRenderer.swift; sourceTree = ""; }; - C0A7A162236891E70087A369 /* TextStylesError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStylesError.swift; sourceTree = ""; }; - C0AF9F5123685F2600BE7DE8 /* NodesExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesExtractor.swift; sourceTree = ""; }; - C0AF9F5323685F4100BE7DE8 /* DefaultNodesExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNodesExtractor.swift; sourceTree = ""; }; - C0AF9F5723686E5200BE7DE8 /* TextStylesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStylesProvider.swift; sourceTree = ""; }; - C0AF9F5923686E7700BE7DE8 /* TextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; - C0AF9F5D23686EFE00BE7DE8 /* TextStylesServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStylesServices.swift; sourceTree = ""; }; - C0AF9F5F23686F1900BE7DE8 /* TextStylesCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStylesCommand.swift; sourceTree = ""; }; - C0AF9F61236870D200BE7DE8 /* TextStylesRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStylesRenderer.swift; sourceTree = ""; }; - C0B5CA622372D3F7007A4B83 /* TextStylesGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStylesGenerator.swift; sourceTree = ""; }; - C0B985842358EFBA00D8B21D /* FigmaAPIFileRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FigmaAPIFileRoute.swift; sourceTree = ""; }; - C0E034E42372EDFC004BE218 /* GenerateServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateServices.swift; sourceTree = ""; }; - C0E034EA237302D4004BE218 /* ConfigurationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationError.swift; sourceTree = ""; }; - C0E034F023730607004BE218 /* BaseConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseConfiguration.swift; sourceTree = ""; }; - C0E0CB6A236C8D6A004BA240 /* FloatingPoint+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FloatingPoint+Extensions.swift"; sourceTree = ""; }; - C0F194752356112F00A24BDE /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - C0F194802356117200A24BDE /* FigmaGenTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FigmaGenTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - C0F194822356117200A24BDE /* FigmaGenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FigmaGenTests.swift; sourceTree = ""; }; - C0F194842356117200A24BDE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C0F195EE235624E800A24BDE /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C0F195F32356256A00A24BDE /* FigmaGen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FigmaGen.app; sourceTree = BUILT_PRODUCTS_DIR; }; - C0F610FD236341B200960CD8 /* CLI+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CLI+Extensions.swift"; sourceTree = ""; }; - C0F775AD235846CC00389DD2 /* FigmaAPIRoute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FigmaAPIRoute.swift; sourceTree = ""; }; - C0F775AF235846E400389DD2 /* FigmaAPIVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FigmaAPIVersion.swift; sourceTree = ""; }; - C0F775B12358473A00389DD2 /* FigmaAPIHTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FigmaAPIHTTPMethod.swift; sourceTree = ""; }; - C0FE20702395E0DE003A6701 /* NodesError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesError.swift; sourceTree = ""; }; - DBCFD32024BCAC660061478A /* Spacings.stencil */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Spacings.stencil; sourceTree = ""; }; - DBCFD32324BCAE310061478A /* SpacingsServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingsServices.swift; sourceTree = ""; }; - DBCFD32624BCAF0F0061478A /* SpacingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingsProvider.swift; sourceTree = ""; }; - DBCFD32824BCAF400061478A /* Spacing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spacing.swift; sourceTree = ""; }; - DBCFD32A24BCAF7A0061478A /* SpacingsRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingsRenderer.swift; sourceTree = ""; }; - DBCFD32C24BCB06B0061478A /* DefaultSpacingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSpacingsProvider.swift; sourceTree = ""; }; - DBCFD32E24BCB0E10061478A /* DefaultSpacingsRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultSpacingsRenderer.swift; sourceTree = ""; }; - DBCFD33024BCB74C0061478A /* SpacingsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingsError.swift; sourceTree = ""; }; - DBCFD33224BCBAD80061478A /* SpacingsGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingsGenerator.swift; sourceTree = ""; }; - DBCFD33424BCBBB70061478A /* SpacingsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpacingsCommand.swift; sourceTree = ""; }; - EE68917070188EC570F805C2 /* Pods-FigmaGen.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FigmaGen.debug.xcconfig"; path = "Target Support Files/Pods-FigmaGen/Pods-FigmaGen.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - C0F1947D2356117200A24BDE /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 275066ECF0967BD89155A3A8 /* Pods_FigmaGenTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C0F195F02356256A00A24BDE /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 0CEFBD150631EBC2D1E8D5C9 /* Pods_FigmaGen.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 4222F9882A886CE6ECBECAD3 /* Frameworks */ = { - isa = PBXGroup; - children = ( - A7CCE646935D17668E376318 /* Pods_FigmaGen.framework */, - A1FBD58D183E7F79857C9AD1 /* Pods_FigmaGenTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 58A78BD9A4D6AF1F5C96B87D /* Pods */ = { - isa = PBXGroup; - children = ( - EE68917070188EC570F805C2 /* Pods-FigmaGen.debug.xcconfig */, - 79A9EBE6548AF631B5702EDE /* Pods-FigmaGen.release.xcconfig */, - 2EEDD6551CD0211C1EF2367B /* Pods-FigmaGenTests.debug.xcconfig */, - 8E7C17A850D86C8D1A110F2C /* Pods-FigmaGenTests.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - C05FD32D235CBBB00042552E /* Tools */ = { - isa = PBXGroup; - children = ( - C05FD32E235CBBBB0042552E /* Extensions */, - ); - path = Tools; - sourceTree = ""; - }; - C05FD32E235CBBBB0042552E /* Extensions */ = { - isa = PBXGroup; - children = ( - C0F610FD236341B200960CD8 /* CLI+Extensions.swift */, - C05FD335235CBBE90042552E /* KeyedDecodingContainerProtocol+Extensions.swift */, - C0778EA52362060100D020FB /* Path+Extensions.swift */, - C06B03CB236302C900DC2754 /* ProcessInfo+Extensions.swift */, - C06B03D123633B0400DC2754 /* Routable+Extensions.swift */, - C0A4A1A2235FA7880063FBE4 /* Sequence+Extensions.swift */, - C05FD333235CBBE90042552E /* SingleValueDecodingContainer+Extensions.swift */, - C05FD32F235CBBE90042552E /* UnkeyedDecodingContainer+Extensions.swift */, - C0E0CB6A236C8D6A004BA240 /* FloatingPoint+Extensions.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - C0778D4F2360A08800D020FB /* Templates */ = { - isa = PBXGroup; - children = ( - DBCFD32024BCAC660061478A /* Spacings.stencil */, - C0778D502360A0C100D020FB /* Colors.stencil */, - C0268E33236B18FE00900532 /* TextStyles.stencil */, - ); - path = Templates; - sourceTree = ""; - }; - C0778EA92362E8C500D020FB /* Commands */ = { - isa = PBXGroup; - children = ( - DBCFD32224BCAE1F0061478A /* Spacings */, - C09E94302372D91B0098FEDD /* Colors */, - C09E94312372E88C0098FEDD /* Generate */, - C09E942F2372D6980098FEDD /* TextStyles */, - ); - path = Commands; - sourceTree = ""; - }; - C07B8DB02359D28200AF750F /* Models */ = { - isa = PBXGroup; - children = ( - C0E034E82372FC0B004BE218 /* Configuration */, - C0A4A12C235F5A390063FBE4 /* Figma */, - C0A4A15F235F5A390063FBE4 /* Color.swift */, - C0AF9F5923686E7700BE7DE8 /* TextStyle.swift */, - C0778EAC2362F21C00D020FB /* TemplateType.swift */, - DBCFD32824BCAF400061478A /* Spacing.swift */, - ); - path = Models; - sourceTree = ""; - }; - C09E942F2372D6980098FEDD /* TextStyles */ = { - isa = PBXGroup; - children = ( - C0B5CA622372D3F7007A4B83 /* TextStylesGenerator.swift */, - C0AF9F5D23686EFE00BE7DE8 /* TextStylesServices.swift */, - C0AF9F5F23686F1900BE7DE8 /* TextStylesCommand.swift */, - ); - path = TextStyles; - sourceTree = ""; - }; - C09E94302372D91B0098FEDD /* Colors */ = { - isa = PBXGroup; - children = ( - C06B03CD2363301F00DC2754 /* ColorsServices.swift */, - C06EC15A2371C6820086B2D1 /* ColorsGenerator.swift */, - C0778EAA2362E8D200D020FB /* ColorsCommand.swift */, - ); - path = Colors; - sourceTree = ""; - }; - C09E94312372E88C0098FEDD /* Generate */ = { - isa = PBXGroup; - children = ( - C06EC1602371C8CA0086B2D1 /* GenerateCommand.swift */, - C0E034E42372EDFC004BE218 /* GenerateServices.swift */, - ); - path = Generate; - sourceTree = ""; - }; - C0A4A12C235F5A390063FBE4 /* Figma */ = { - isa = PBXGroup; - children = ( - C0A4A133235F5A390063FBE4 /* Nodes */, - C0A4A147235F5A390063FBE4 /* FigmaBlendMode.swift */, - C0A4A15D235F5A390063FBE4 /* FigmaBooleanOperationType.swift */, - C0A4A149235F5A390063FBE4 /* FigmaColor.swift */, - C0A4A150235F5A390063FBE4 /* FigmaColorStop.swift */, - C0A4A14E235F5A390063FBE4 /* FigmaComponent.swift */, - C0A4A15E235F5A390063FBE4 /* FigmaConstraint.swift */, - C0A4A156235F5A390063FBE4 /* FigmaConstraintType.swift */, - C0A4A14F235F5A390063FBE4 /* FigmaEasingType.swift */, - C0A4A146235F5A390063FBE4 /* FigmaEffect.swift */, - C0A4A159235F5A390063FBE4 /* FigmaEffectType.swift */, - C0A4A12D235F5A390063FBE4 /* FigmaExportSetting.swift */, - C0A4A132235F5A390063FBE4 /* FigmaFrameOffset.swift */, - C0A4A15A235F5A390063FBE4 /* FigmaImageFormat.swift */, - C0A4A153235F5A390063FBE4 /* FigmaLayoutConstraint.swift */, - C0A4A142235F5A390063FBE4 /* FigmaLayoutGrid.swift */, - C0A4A14B235F5A390063FBE4 /* FigmaLayoutGridAlignment.swift */, - C0A4A152235F5A390063FBE4 /* FigmaLayoutGridPattern.swift */, - C0A4A14D235F5A390063FBE4 /* FigmaLayoutHorizontalConstraint.swift */, - C0A4A148235F5A390063FBE4 /* FigmaLayoutVerticalConstraint.swift */, - C0A4A143235F5A390063FBE4 /* FigmaLineHeightUnit.swift */, - C0A4A131235F5A390063FBE4 /* FigmaPaint.swift */, - C0A4A15C235F5A390063FBE4 /* FigmaPaintType.swift */, - C0A4A15B235F5A390063FBE4 /* FigmaRectangle.swift */, - C0A4A145235F5A390063FBE4 /* FigmaScaleMode.swift */, - C0A4A12E235F5A390063FBE4 /* FigmaStrokeAlignment.swift */, - C0A4A151235F5A390063FBE4 /* FigmaStrokeCap.swift */, - C0A4A14C235F5A390063FBE4 /* FigmaStrokeJoin.swift */, - C0A4A158235F5A390063FBE4 /* FigmaStyle.swift */, - C0A4A12F235F5A390063FBE4 /* FigmaStyleType.swift */, - C0A4A155235F5A390063FBE4 /* FigmaTextCase.swift */, - C0A4A157235F5A390063FBE4 /* FigmaTextDecoration.swift */, - C0A4A154235F5A390063FBE4 /* FigmaTextHorizontalAlignment.swift */, - C0A4A130235F5A390063FBE4 /* FigmaTextVerticalAlignment.swift */, - C0A4A144235F5A390063FBE4 /* FigmaTypeStyle.swift */, - C0A4A14A235F5A390063FBE4 /* FigmaVector.swift */, - ); - path = Figma; - sourceTree = ""; - }; - C0A4A133235F5A390063FBE4 /* Nodes */ = { - isa = PBXGroup; - children = ( - C0A4A141235F5A390063FBE4 /* FigmaBooleanOperationNodePayload.swift */, - C0A4A13C235F5A390063FBE4 /* FigmaCanvasNodeInfo.swift */, - C0A4A137235F5A390063FBE4 /* FigmaDocumentNodeInfo.swift */, - C0A4A139235F5A390063FBE4 /* FigmaFile.swift */, - C0A4A134235F5A390063FBE4 /* FigmaFrameNodeInfo.swift */, - C0A4A13E235F5A390063FBE4 /* FigmaInstanceNodePayload.swift */, - C0A4A13F235F5A390063FBE4 /* FigmaNode.swift */, - C0A4A136235F5A390063FBE4 /* FigmaNodeType.swift */, - C0A4A13A235F5A390063FBE4 /* FigmaRectangleNodePayload.swift */, - C0A4A13D235F5A390063FBE4 /* FigmaSliceNodeInfo.swift */, - C0A4A135235F5A390063FBE4 /* FigmaTextNodePayload.swift */, - C0A4A140235F5A390063FBE4 /* FigmaVectorNodeInfo.swift */, - ); - path = Nodes; - sourceTree = ""; - }; - C0A4A192235F5AC40063FBE4 /* Colors */ = { - isa = PBXGroup; - children = ( - C0A4A19D235F6ABA0063FBE4 /* ColorsError.swift */, - C0A4A193235F5AC40063FBE4 /* ColorsProvider.swift */, - C0778E9C2361CAAC00D020FB /* ColorsRenderer.swift */, - C0A4A195235F5AC40063FBE4 /* DefaultColorsProvider.swift */, - C0778E9D2361CAAC00D020FB /* DefaultColorsRenderer.swift */, - ); - path = Colors; - sourceTree = ""; - }; - C0AF9F5023685AA400BE7DE8 /* Nodes */ = { - isa = PBXGroup; - children = ( - C0AF9F5323685F4100BE7DE8 /* DefaultNodesExtractor.swift */, - C0FE20702395E0DE003A6701 /* NodesError.swift */, - C0AF9F5123685F2600BE7DE8 /* NodesExtractor.swift */, - ); - path = Nodes; - sourceTree = ""; - }; - C0AF9F5623686E2800BE7DE8 /* TextStyles */ = { - isa = PBXGroup; - children = ( - C0A7A15E236874360087A369 /* DefaultTextStylesProvider.swift */, - C0A7A16023687A410087A369 /* DefaultTextStylesRenderer.swift */, - C0A7A162236891E70087A369 /* TextStylesError.swift */, - C0AF9F5723686E5200BE7DE8 /* TextStylesProvider.swift */, - C0AF9F61236870D200BE7DE8 /* TextStylesRenderer.swift */, - ); - path = TextStyles; - sourceTree = ""; - }; - C0B985812358EF1F00D8B21D /* Routes */ = { - isa = PBXGroup; - children = ( - C0B985842358EFBA00D8B21D /* FigmaAPIFileRoute.swift */, - ); - path = Routes; - sourceTree = ""; - }; - C0B985822358EF8300D8B21D /* API */ = { - isa = PBXGroup; - children = ( - C0B985812358EF1F00D8B21D /* Routes */, - C0A4A19B235F5ACA0063FBE4 /* DefaultFigmaAPIProvider.swift */, - C0F775B12358473A00389DD2 /* FigmaAPIHTTPMethod.swift */, - C00380AF2358929700E57CCE /* FigmaAPIProvider.swift */, - C0F775AD235846CC00389DD2 /* FigmaAPIRoute.swift */, - C0F775AF235846E400389DD2 /* FigmaAPIVersion.swift */, - ); - path = API; - sourceTree = ""; - }; - C0E034E82372FC0B004BE218 /* Configuration */ = { - isa = PBXGroup; - children = ( - C0E034F023730607004BE218 /* BaseConfiguration.swift */, - C06EC14D2371AF790086B2D1 /* Configuration.swift */, - C0E034EA237302D4004BE218 /* ConfigurationError.swift */, - C06EC14F2371AFCF0086B2D1 /* StepConfiguration.swift */, - ); - path = Configuration; - sourceTree = ""; - }; - C0F194692356112F00A24BDE = { - isa = PBXGroup; - children = ( - C0F194742356112F00A24BDE /* Sources */, - C0F195ED235624E800A24BDE /* Resources */, - C0778D4F2360A08800D020FB /* Templates */, - C0F194812356117200A24BDE /* Tests */, - 58A78BD9A4D6AF1F5C96B87D /* Pods */, - C0F194732356112F00A24BDE /* Products */, - 4222F9882A886CE6ECBECAD3 /* Frameworks */, - C085A5AE2356351000C56581 /* FigmaGen.podspec */, - C085A5AD2356350F00C56581 /* Gemfile */, - C085A5B02356351000C56581 /* LICENSE */, - C085A5AC2356350F00C56581 /* Makefile */, - C085A5B12356351000C56581 /* Package.resources */, - C085A5B32356351000C56581 /* Package.swift */, - C085A5B22356351000C56581 /* Podfile */, - C085A5AF2356351000C56581 /* README.md */, - ); - sourceTree = ""; - }; - C0F194732356112F00A24BDE /* Products */ = { - isa = PBXGroup; - children = ( - C0F194802356117200A24BDE /* FigmaGenTests.xctest */, - C0F195F32356256A00A24BDE /* FigmaGen.app */, - ); - name = Products; - sourceTree = ""; - }; - C0F194742356112F00A24BDE /* Sources */ = { - isa = PBXGroup; - children = ( - C0778EA92362E8C500D020FB /* Commands */, - C07B8DB02359D28200AF750F /* Models */, - C0F775A9235761CA00389DD2 /* Services */, - C05FD32D235CBBB00042552E /* Tools */, - C0F194752356112F00A24BDE /* main.swift */, - C06B052F23633E9200DC2754 /* Services.swift */, - ); - path = Sources; - sourceTree = ""; - }; - C0F194812356117200A24BDE /* Tests */ = { - isa = PBXGroup; - children = ( - C0F194822356117200A24BDE /* FigmaGenTests.swift */, - C0F194842356117200A24BDE /* Info.plist */, - ); - path = Tests; - sourceTree = ""; - }; - C0F195ED235624E800A24BDE /* Resources */ = { - isa = PBXGroup; - children = ( - C0778EA723620A2300D020FB /* Templates */, - C0F195EE235624E800A24BDE /* Info.plist */, - ); - path = Resources; - sourceTree = ""; - }; - C0F775A9235761CA00389DD2 /* Services */ = { - isa = PBXGroup; - children = ( - DBCFD32524BCAE6E0061478A /* Spacings */, - C0B985822358EF8300D8B21D /* API */, - C0A4A192235F5AC40063FBE4 /* Colors */, - C0AF9F5023685AA400BE7DE8 /* Nodes */, - C0AF9F5623686E2800BE7DE8 /* TextStyles */, - ); - path = Services; - sourceTree = ""; - }; - DBCFD32224BCAE1F0061478A /* Spacings */ = { - isa = PBXGroup; - children = ( - DBCFD32324BCAE310061478A /* SpacingsServices.swift */, - DBCFD33224BCBAD80061478A /* SpacingsGenerator.swift */, - DBCFD33424BCBBB70061478A /* SpacingsCommand.swift */, - ); - path = Spacings; - sourceTree = ""; - }; - DBCFD32524BCAE6E0061478A /* Spacings */ = { - isa = PBXGroup; - children = ( - DBCFD32624BCAF0F0061478A /* SpacingsProvider.swift */, - DBCFD32A24BCAF7A0061478A /* SpacingsRenderer.swift */, - DBCFD32C24BCB06B0061478A /* DefaultSpacingsProvider.swift */, - DBCFD32E24BCB0E10061478A /* DefaultSpacingsRenderer.swift */, - DBCFD33024BCB74C0061478A /* SpacingsError.swift */, - ); - path = Spacings; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - C0F1947F2356117200A24BDE /* FigmaGenTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = C0F194872356117200A24BDE /* Build configuration list for PBXNativeTarget "FigmaGenTests" */; - buildPhases = ( - 9B23ECA6DC2B9C50594433B3 /* [CP] Check Pods Manifest.lock */, - C0F1947C2356117200A24BDE /* Sources */, - C0F1947D2356117200A24BDE /* Frameworks */, - C0F1947E2356117200A24BDE /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - C085A5AB2356328C00C56581 /* PBXTargetDependency */, - ); - name = FigmaGenTests; - productName = FigmaGenTests; - productReference = C0F194802356117200A24BDE /* FigmaGenTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - C0F195F22356256A00A24BDE /* FigmaGen */ = { - isa = PBXNativeTarget; - buildConfigurationList = C0F195FE2356256C00A24BDE /* Build configuration list for PBXNativeTarget "FigmaGen" */; - buildPhases = ( - 81F63CC1931A9DBF2F49D195 /* [CP] Check Pods Manifest.lock */, - C0F195EF2356256A00A24BDE /* Sources */, - C0F195F02356256A00A24BDE /* Frameworks */, - C0F195F12356256A00A24BDE /* Resources */, - DF0FDF8C68F3D10D53E3B490 /* [CP] Embed Pods Frameworks */, - C085A5A923562B7300C56581 /* SwiftLint */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = FigmaGen; - productName = FigmaGen; - productReference = C0F195F32356256A00A24BDE /* FigmaGen.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - C0F1946A2356112F00A24BDE /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1100; - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = HeadHunter; - TargetAttributes = { - C0F1947F2356117200A24BDE = { - CreatedOnToolsVersion = 11.0; - }; - C0F195F22356256A00A24BDE = { - CreatedOnToolsVersion = 11.0; - }; - }; - }; - buildConfigurationList = C0F1946D2356112F00A24BDE /* Build configuration list for PBXProject "FigmaGen" */; - compatibilityVersion = "Xcode 11.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = C0F194692356112F00A24BDE; - productRefGroup = C0F194732356112F00A24BDE /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - C0F195F22356256A00A24BDE /* FigmaGen */, - C0F1947F2356117200A24BDE /* FigmaGenTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - C0F1947E2356117200A24BDE /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C0F195F12356256A00A24BDE /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DBCFD32124BCAC660061478A /* Spacings.stencil in Resources */, - C0E0CA34236C7F5E004BA240 /* Colors.stencil in Resources */, - C0E0CA35236C7F61004BA240 /* TextStyles.stencil in Resources */, - C0778EA823620A2300D020FB /* Templates in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 81F63CC1931A9DBF2F49D195 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-FigmaGen-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9B23ECA6DC2B9C50594433B3 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-FigmaGenTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - C085A5A923562B7300C56581 /* SwiftLint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = SwiftLint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [[ ! $CI && ! $SKIP_SWIFTLINT ]]; then\n \"${PODS_ROOT}/SwiftLint/swiftlint\" --no-cache\nfi\n"; - }; - DF0FDF8C68F3D10D53E3B490 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FigmaGen/Pods-FigmaGen-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-FigmaGen/Pods-FigmaGen-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FigmaGen/Pods-FigmaGen-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - C0F1947C2356117200A24BDE /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - C0F194832356117200A24BDE /* FigmaGenTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - C0F195EF2356256A00A24BDE /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DBCFD32D24BCB06B0061478A /* DefaultSpacingsProvider.swift in Sources */, - DBCFD32B24BCAF7A0061478A /* SpacingsRenderer.swift in Sources */, - C0A4A161235F5A390063FBE4 /* FigmaStrokeAlignment.swift in Sources */, - C0A4A177235F5A390063FBE4 /* FigmaScaleMode.swift in Sources */, - C0A4A18B235F5A390063FBE4 /* FigmaEffectType.swift in Sources */, - C05FD33A235CBBE90042552E /* SingleValueDecodingContainer+Extensions.swift in Sources */, - C0A4A169235F5A390063FBE4 /* FigmaDocumentNodeInfo.swift in Sources */, - C05FD336235CBBE90042552E /* UnkeyedDecodingContainer+Extensions.swift in Sources */, - C0A4A19C235F5ACA0063FBE4 /* DefaultFigmaAPIProvider.swift in Sources */, - C05FD33C235CBBE90042552E /* KeyedDecodingContainerProtocol+Extensions.swift in Sources */, - C0AF9F5823686E5200BE7DE8 /* TextStylesProvider.swift in Sources */, - C0A4A18C235F5A390063FBE4 /* FigmaImageFormat.swift in Sources */, - C0778EAD2362F21C00D020FB /* TemplateType.swift in Sources */, - C0A4A1A3235FA7880063FBE4 /* Sequence+Extensions.swift in Sources */, - C0A4A173235F5A390063FBE4 /* FigmaBooleanOperationNodePayload.swift in Sources */, - C0E0CB6B236C8D6A004BA240 /* FloatingPoint+Extensions.swift in Sources */, - C0A4A183235F5A390063FBE4 /* FigmaStrokeCap.swift in Sources */, - C0778EA12361CAAC00D020FB /* DefaultColorsRenderer.swift in Sources */, - C0A4A17A235F5A390063FBE4 /* FigmaLayoutVerticalConstraint.swift in Sources */, - C0AF9F5A23686E7700BE7DE8 /* TextStyle.swift in Sources */, - C0AF9F6023686F1900BE7DE8 /* TextStylesCommand.swift in Sources */, - C0A4A19E235F6ABA0063FBE4 /* ColorsError.swift in Sources */, - C0A4A176235F5A390063FBE4 /* FigmaTypeStyle.swift in Sources */, - C0778EA02361CAAC00D020FB /* ColorsRenderer.swift in Sources */, - C0A4A197235F5AC40063FBE4 /* ColorsProvider.swift in Sources */, - C0A4A181235F5A390063FBE4 /* FigmaEasingType.swift in Sources */, - C0A4A172235F5A390063FBE4 /* FigmaVectorNodeInfo.swift in Sources */, - C0A4A166235F5A390063FBE4 /* FigmaFrameNodeInfo.swift in Sources */, - C06B03D223633B0400DC2754 /* Routable+Extensions.swift in Sources */, - C0A4A182235F5A390063FBE4 /* FigmaColorStop.swift in Sources */, - C06B03CC236302C900DC2754 /* ProcessInfo+Extensions.swift in Sources */, - C06EC14E2371AF790086B2D1 /* Configuration.swift in Sources */, - C0A4A17D235F5A390063FBE4 /* FigmaLayoutGridAlignment.swift in Sources */, - C0E034E52372EDFC004BE218 /* GenerateServices.swift in Sources */, - C0A4A170235F5A390063FBE4 /* FigmaInstanceNodePayload.swift in Sources */, - C0A4A16E235F5A390063FBE4 /* FigmaCanvasNodeInfo.swift in Sources */, - C0A7A16123687A410087A369 /* DefaultTextStylesRenderer.swift in Sources */, - C0A7A15F236874360087A369 /* DefaultTextStylesProvider.swift in Sources */, - C0A4A174235F5A390063FBE4 /* FigmaLayoutGrid.swift in Sources */, - C0A4A187235F5A390063FBE4 /* FigmaTextCase.swift in Sources */, - C0A4A171235F5A390063FBE4 /* FigmaNode.swift in Sources */, - C0A4A18A235F5A390063FBE4 /* FigmaStyle.swift in Sources */, - C0AF9F5423685F4100BE7DE8 /* DefaultNodesExtractor.swift in Sources */, - C0A4A190235F5A390063FBE4 /* FigmaConstraint.swift in Sources */, - C0A4A184235F5A390063FBE4 /* FigmaLayoutGridPattern.swift in Sources */, - C0A4A189235F5A390063FBE4 /* FigmaTextDecoration.swift in Sources */, - DBCFD32F24BCB0E10061478A /* DefaultSpacingsRenderer.swift in Sources */, - C0A4A160235F5A390063FBE4 /* FigmaExportSetting.swift in Sources */, - C0A4A164235F5A390063FBE4 /* FigmaPaint.swift in Sources */, - DBCFD32424BCAE310061478A /* SpacingsServices.swift in Sources */, - C0F610FE236341B200960CD8 /* CLI+Extensions.swift in Sources */, - C0F775AE235846CC00389DD2 /* FigmaAPIRoute.swift in Sources */, - C00380B02358929700E57CCE /* FigmaAPIProvider.swift in Sources */, - C0B5CA632372D3F7007A4B83 /* TextStylesGenerator.swift in Sources */, - C06B03CE2363301F00DC2754 /* ColorsServices.swift in Sources */, - C0A4A188235F5A390063FBE4 /* FigmaConstraintType.swift in Sources */, - C0E034F123730607004BE218 /* BaseConfiguration.swift in Sources */, - C0A4A18D235F5A390063FBE4 /* FigmaRectangle.swift in Sources */, - C0A4A17B235F5A390063FBE4 /* FigmaColor.swift in Sources */, - C0F196012356260100A24BDE /* main.swift in Sources */, - C0A4A191235F5A390063FBE4 /* Color.swift in Sources */, - C0778EA62362060100D020FB /* Path+Extensions.swift in Sources */, - C0A7A163236891E70087A369 /* TextStylesError.swift in Sources */, - DBCFD33124BCB74C0061478A /* SpacingsError.swift in Sources */, - C0A4A18E235F5A390063FBE4 /* FigmaPaintType.swift in Sources */, - C0778EAB2362E8D200D020FB /* ColorsCommand.swift in Sources */, - C0A4A178235F5A390063FBE4 /* FigmaEffect.swift in Sources */, - C0A4A16C235F5A390063FBE4 /* FigmaRectangleNodePayload.swift in Sources */, - C0A4A17C235F5A390063FBE4 /* FigmaVector.swift in Sources */, - C0F775B0235846E400389DD2 /* FigmaAPIVersion.swift in Sources */, - C06EC1612371C8CA0086B2D1 /* GenerateCommand.swift in Sources */, - C0A4A165235F5A390063FBE4 /* FigmaFrameOffset.swift in Sources */, - C0A4A175235F5A390063FBE4 /* FigmaLineHeightUnit.swift in Sources */, - C0E034F723730E53004BE218 /* ConfigurationError.swift in Sources */, - C06EC1512371B0CA0086B2D1 /* StepConfiguration.swift in Sources */, - C0A4A185235F5A390063FBE4 /* FigmaLayoutConstraint.swift in Sources */, - DBCFD32924BCAF400061478A /* Spacing.swift in Sources */, - C0AF9F62236870D200BE7DE8 /* TextStylesRenderer.swift in Sources */, - C0A4A179235F5A390063FBE4 /* FigmaBlendMode.swift in Sources */, - C0A4A199235F5AC40063FBE4 /* DefaultColorsProvider.swift in Sources */, - C06EC15B2371C6820086B2D1 /* ColorsGenerator.swift in Sources */, - C0A4A17F235F5A390063FBE4 /* FigmaLayoutHorizontalConstraint.swift in Sources */, - DBCFD33524BCBBB70061478A /* SpacingsCommand.swift in Sources */, - C0A4A163235F5A390063FBE4 /* FigmaTextVerticalAlignment.swift in Sources */, - C0A4A16B235F5A390063FBE4 /* FigmaFile.swift in Sources */, - C0FE20712395E0DE003A6701 /* NodesError.swift in Sources */, - DBCFD32724BCAF0F0061478A /* SpacingsProvider.swift in Sources */, - C06B053023633E9200DC2754 /* Services.swift in Sources */, - C0A4A180235F5A390063FBE4 /* FigmaComponent.swift in Sources */, - C05FD353235CC33E0042552E /* FigmaAPIFileRoute.swift in Sources */, - C0A4A167235F5A390063FBE4 /* FigmaTextNodePayload.swift in Sources */, - C0A4A162235F5A390063FBE4 /* FigmaStyleType.swift in Sources */, - C0A4A18F235F5A390063FBE4 /* FigmaBooleanOperationType.swift in Sources */, - C0A4A186235F5A390063FBE4 /* FigmaTextHorizontalAlignment.swift in Sources */, - C0AF9F5223685F2600BE7DE8 /* NodesExtractor.swift in Sources */, - C0A4A17E235F5A390063FBE4 /* FigmaStrokeJoin.swift in Sources */, - C0A4A168235F5A390063FBE4 /* FigmaNodeType.swift in Sources */, - C0F775B22358473A00389DD2 /* FigmaAPIHTTPMethod.swift in Sources */, - DBCFD33324BCBAD80061478A /* SpacingsGenerator.swift in Sources */, - C06EC1572371B4850086B2D1 /* TextStylesServices.swift in Sources */, - C0A4A16F235F5A390063FBE4 /* FigmaSliceNodeInfo.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - C085A5AB2356328C00C56581 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C0F195F22356256A00A24BDE /* FigmaGen */; - targetProxy = C085A5AA2356328C00C56581 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - C0F194772356112F00A24BDE /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_LABEL = YES; - GCC_WARN_UNUSED_PARAMETER = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - C0F194782356112F00A24BDE /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_LABEL = YES; - GCC_WARN_UNUSED_PARAMETER = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - C0F194852356117200A24BDE /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2EEDD6551CD0211C1EF2367B /* Pods-FigmaGenTests.debug.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = ru.hh.FigmaGenTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.1; - }; - name = Debug; - }; - C0F194862356117200A24BDE /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 8E7C17A850D86C8D1A110F2C /* Pods-FigmaGenTests.release.xcconfig */; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Tests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - "@loader_path/../Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = ru.hh.FigmaGenTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.1; - }; - name = Release; - }; - C0F195FF2356256C00A24BDE /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = EE68917070188EC570F805C2 /* Pods-FigmaGen.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_SUSPICIOUS_MOVES = YES; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - INFOPLIST_FILE = Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = ru.hh.FigmaGen; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 5.1; - }; - name = Debug; - }; - C0F196002356256C00A24BDE /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 79A9EBE6548AF631B5702EDE /* Pods-FigmaGen.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_SUSPICIOUS_MOVES = YES; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - INFOPLIST_FILE = Resources/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MACOSX_DEPLOYMENT_TARGET = 10.12; - MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = ru.hh.FigmaGen; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.1; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - C0F1946D2356112F00A24BDE /* Build configuration list for PBXProject "FigmaGen" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C0F194772356112F00A24BDE /* Debug */, - C0F194782356112F00A24BDE /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C0F194872356117200A24BDE /* Build configuration list for PBXNativeTarget "FigmaGenTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C0F194852356117200A24BDE /* Debug */, - C0F194862356117200A24BDE /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - C0F195FE2356256C00A24BDE /* Build configuration list for PBXNativeTarget "FigmaGen" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - C0F195FF2356256C00A24BDE /* Debug */, - C0F196002356256C00A24BDE /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = C0F1946A2356112F00A24BDE /* Project object */; -} diff --git a/FigmaGen.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/FigmaGen.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 65c9109..0000000 --- a/FigmaGen.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/FigmaGen.xcodeproj/xcshareddata/IDETemplateMacros.plist b/FigmaGen.xcodeproj/xcshareddata/IDETemplateMacros.plist deleted file mode 100644 index 91ef30b..0000000 --- a/FigmaGen.xcodeproj/xcshareddata/IDETemplateMacros.plist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - FILEHEADER - -// ___PROJECTNAME___ -// Copyright © ___YEAR___ ___ORGANIZATIONNAME___ -// MIT Licence -// - - diff --git a/FigmaGen.xcodeproj/xcshareddata/xcschemes/FigmaGen Help.xcscheme b/FigmaGen.xcodeproj/xcshareddata/xcschemes/FigmaGen Help.xcscheme deleted file mode 100644 index 21879c9..0000000 --- a/FigmaGen.xcodeproj/xcshareddata/xcschemes/FigmaGen Help.xcscheme +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FigmaGen.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/FigmaGen.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/FigmaGen.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Gemfile b/Gemfile index a3f7980..01e1ea0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,10 @@ source "https://rubygems.org" +gem 'xcode-install' + gem 'cocoapods' -gem 'danger' -gem 'danger-swiftlint' \ No newline at end of file +gem 'xcpretty' +gem 'xcpretty-json-formatter' + +gem "danger" +gem 'danger-swiftlint' diff --git a/Gemfile.lock b/Gemfile.lock index b07a689..bd04b0e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,124 +1,322 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.1) - activesupport (4.2.11.1) - i18n (~> 0.7) - minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) - tzinfo (~> 1.1) - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - algoliasearch (1.27.1) + CFPropertyList (3.0.6) + rexml + activesupport (7.0.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) + algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + artifactory (3.0.15) atomos (0.1.3) - claide (1.0.3) + aws-eventstream (1.2.0) + aws-partitions (1.781.0) + aws-sdk-core (3.175.0) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.5) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.67.0) + aws-sdk-core (~> 3, >= 3.174.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.126.0) + aws-sdk-core (~> 3, >= 3.174.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sigv4 (1.5.2) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + claide (1.1.0) claide-plugins (0.9.2) cork nap open4 (~> 1.3) - cocoapods (1.8.3) - activesupport (>= 4.0.2, < 5) + cocoapods (1.12.1) + addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.8.3) + cocoapods-core (= 1.12.1) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.2.2, < 2.0) + cocoapods-downloader (>= 1.6.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) - cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.4.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) fourflusher (>= 2.3.0, < 3.0) gh_inspector (~> 1.0) - molinillo (~> 0.6.6) + molinillo (~> 0.8.0) nap (~> 1.0) - ruby-macho (~> 1.4) - xcodeproj (>= 1.11.1, < 2.0) - cocoapods-core (1.8.3) - activesupport (>= 4.0.2, < 6) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.21.0, < 2.0) + cocoapods-core (1.12.1) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) algoliasearch (~> 1.0) concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) - cocoapods-deintegrate (1.0.4) - cocoapods-downloader (1.2.2) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (1.6.3) cocoapods-plugins (1.0.0) nap - cocoapods-search (1.0.0) - cocoapods-stats (1.1.0) - cocoapods-trunk (1.4.1) + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) nap (>= 0.8, < 2.0) netrc (~> 0.11) - cocoapods-try (1.1.0) + cocoapods-try (1.2.0) + colored (1.2) colored2 (3.1.2) - concurrent-ruby (1.1.5) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.2.2) cork (0.3.0) colored2 (~> 3.1) - danger (6.1.0) + danger (9.3.1) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) cork (~> 0.1) - faraday (~> 0.9) + faraday (>= 0.9.0, < 3.0) faraday-http-cache (~> 2.0) - git (~> 1.5) - kramdown (~> 2.0) + git (~> 1.13) + kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) no_proxy_fix - octokit (~> 4.7) - terminal-table (~> 1) - danger-swiftlint (0.23.0) + octokit (~> 6.0) + terminal-table (>= 1, < 4) + danger-swiftlint (0.33.0) danger rake (> 10) thor (~> 0.19) + declarative (0.0.20) + digest-crc (0.6.4) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.5.20190701) + unf (>= 0.0.5, < 1.0.0) + dotenv (2.8.1) + emoji_regex (3.2.3) escape (0.0.4) - faraday (0.17.0) - multipart-post (>= 1.2, < 3) - faraday-http-cache (2.0.0) - faraday (~> 0.8) + ethon (0.16.0) + ffi (>= 1.15.0) + excon (0.100.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-http-cache (2.5.0) + faraday (>= 0.8) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.0) + faraday (~> 1.0) + fastimage (2.2.7) + fastlane (2.213.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + ffi (1.15.5) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - git (1.5.0) + git (1.18.0) + addressable (~> 2.8) + rchardet (~> 1.8) + google-apis-androidpublisher_v3 (0.44.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.0) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.19.0) + google-apis-core (>= 0.9.0, < 2.a) + google-cloud-core (1.6.0) + google-cloud-env (~> 1.0) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.3.1) + google-cloud-storage (1.44.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.19.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.6.0) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + memoist (~> 0.16) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + domain_name (~> 0.5) httpclient (2.8.3) - i18n (0.9.5) + i18n (1.14.1) concurrent-ruby (~> 1.0) - json (2.2.0) - kramdown (2.1.0) + jmespath (1.6.2) + json (2.6.3) + jwt (2.7.1) + kramdown (2.4.0) + rexml kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) - minitest (5.12.2) - molinillo (0.6.6) - multipart-post (2.1.1) - nanaimo (0.2.6) + memoist (0.16.2) + mini_magick (4.12.0) + mini_mime (1.1.2) + minitest (5.18.1) + molinillo (0.8.0) + multi_json (1.15.0) + multipart-post (2.3.0) + nanaimo (0.3.0) nap (1.1.0) + naturally (2.2.1) netrc (0.11.0) no_proxy_fix (0.1.2) - octokit (4.14.0) - sawyer (~> 0.8.0, >= 0.5.3) + octokit (6.1.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) open4 (1.3.4) - public_suffix (4.0.1) - rake (13.0.0) - ruby-macho (1.4.0) - sawyer (0.8.2) + optparse (0.1.1) + os (1.1.4) + plist (3.7.0) + public_suffix (4.0.7) + rake (13.0.6) + rchardet (1.8.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.5) + rouge (2.0.7) + ruby-macho (2.5.1) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + sawyer (0.9.2) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) + faraday (>= 0.17.3, < 3) + security (0.1.3) + signet (0.17.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) thor (0.20.3) - thread_safe (0.3.6) - tzinfo (1.2.5) - thread_safe (~> 0.1) - unicode-display_width (1.6.0) - xcodeproj (1.12.0) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.1) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.2) + unicode-display_width (1.8.0) + webrick (1.8.1) + word_wrap (1.0.0) + xcode-install (2.8.1) + claide (>= 0.9.1) + fastlane (>= 2.1.0, < 3.0.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.6) + nanaimo (~> 0.3.0) + rexml (~> 3.2.4) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-json-formatter (0.1.1) + xcpretty (~> 0.2, >= 0.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) PLATFORMS ruby @@ -127,6 +325,9 @@ DEPENDENCIES cocoapods danger danger-swiftlint + xcode-install + xcpretty + xcpretty-json-formatter BUNDLED WITH - 2.0.2 + 2.4.1 diff --git a/LICENSE b/LICENSE index db4087a..1c139d7 100755 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 HeadHunter +Copyright (c) 2019 Almaz Ibragimov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 6500e57..30113c6 100644 --- a/Makefile +++ b/Makefile @@ -1,49 +1,93 @@ -PRODUCT_NAME=figmagen -PRODUCT_VERSION=1.0.0 +PREFIX?=/usr/local +PRODUCT_NAME=figmagen +PRODUCT_VERSION=2.0.0-beta.24 TEMPLATES_NAME=Templates README_NAME=README.md LICENSE_NAME=LICENSE +MAKEFILE_NAME=Makefile + +SOURCES_MAIN_PATH=Sources/FigmaGen/main.swift -RELEASE_PATH=.build/release/$(PRODUCT_NAME)-$(PRODUCT_VERSION) -RELEASE_ZIP_PATH = ./$(PRODUCT_NAME)-$(PRODUCT_VERSION).zip -PRODUCT_PATH=.build/release/$(PRODUCT_NAME) +BUILD_PATH=.build +RELEASE_PATH=$(BUILD_PATH)/release/$(PRODUCT_NAME)-$(PRODUCT_VERSION) +RELEASE_ZIP_PATH=$(PRODUCT_NAME)-$(PRODUCT_VERSION).zip +PRODUCT_PATH=$(BUILD_PATH)/release/$(PRODUCT_NAME) TEMPLATES_PATH=$(TEMPLATES_NAME) + +DEMO_PATH=Demo +DEMO_WORKSPACE=FigmaGenDemo.xcworkspace +DEMO_TEST_SCHEME=FigmaGenDemo +DEMO_TEST_DESTINATION=OS=17.2,name=iPhone 15 +DEMO_TEST_LOG_PATH=$(BUILD_PATH)/demo_test.json + README_PATH=$(README_NAME) LICENSE_PATH=$(LICENSE_NAME) - -PREFIX = /usr/local +MAKEFILE_PATH=$(MAKEFILE_NAME) BIN_PATH=$(PREFIX)/bin BIN_PRODUCT_PATH=$(BIN_PATH)/$(PRODUCT_NAME) SHARE_PRODUCT_PATH=$(PREFIX)/share/$(PRODUCT_NAME) -.PHONY: all version build install uninstall release lint +.PHONY: all version bootstrap lint build test test_demo install uninstall update_version release version: @echo $(PRODUCT_VERSION) +bootstrap: + Scripts/bootstrap.sh + +lint: + Scripts/swiftlint.sh + build: + swift package clean swift build --disable-sandbox -c release +test: + swift package clean + swift test + +test_demo: build + cp -f $(PRODUCT_PATH) $(DEMO_PATH) + cp -r $(TEMPLATES_PATH) $(DEMO_PATH) + + set -euo pipefail; \ + cd $(DEMO_PATH); \ + ./figmagen generate; \ + bundle exec pod install; \ + xcodebuild clean build test -workspace "$(DEMO_WORKSPACE)" -scheme "$(DEMO_TEST_SCHEME)" -destination "$(DEMO_TEST_DESTINATION)" | XCPRETTY_JSON_FILE_OUTPUT="../$(DEMO_TEST_LOG_PATH)" xcpretty -f `xcpretty-json-formatter` + install: build mkdir -p $(BIN_PATH) cp -f $(PRODUCT_PATH) $(BIN_PRODUCT_PATH) mkdir -p $(SHARE_PRODUCT_PATH) - cp -R $(TEMPLATES_PATH)/. $(SHARE_PRODUCT_PATH) + cp -r $(TEMPLATES_PATH)/. $(SHARE_PRODUCT_PATH) + +install_release: + mkdir -p $(BIN_PATH) + cp -f $(PRODUCT_NAME) $(BIN_PRODUCT_PATH) + + mkdir -p $(SHARE_PRODUCT_PATH) + cp -r $(TEMPLATES_PATH)/. $(SHARE_PRODUCT_PATH) uninstall: rm -rf $(BIN_PRODUCT_PATH) rm -rf $(SHARE_PRODUCT_PATH) -release: build +update_version: + sed -i '' 's|\(let version = "\)\(.*\)\("\)|\1$(PRODUCT_VERSION)\3|' $(SOURCES_MAIN_PATH) + sed -i '' 's|\(pod '\''FigmaGen'\'', '\''~> \)\(.*\)\('\''\)|\1$(PRODUCT_VERSION)\3|' $(README_PATH) + sed -i '' 's|\($ mint install hhru/FigmaGen@\)\(.*\)|\1$(PRODUCT_VERSION)|' $(README_PATH) + +release: update_version build mkdir -p $(RELEASE_PATH) + cp -f $(PRODUCT_PATH) $(RELEASE_PATH) cp -r $(TEMPLATES_PATH) $(RELEASE_PATH) cp -f $(README_PATH) $(RELEASE_PATH) cp -f $(LICENSE_PATH) $(RELEASE_PATH) - (cd $(RELEASE_PATH); zip -yr - $(PRODUCT_NAME) $(TEMPLATES_NAME) $(README_NAME) $(LICENSE_NAME)) > $(RELEASE_ZIP_PATH) + cp -f $(MAKEFILE_PATH) $(RELEASE_PATH) -lint: - swiftlint lint --quiet + (cd $(RELEASE_PATH); zip -yr - $(PRODUCT_NAME) $(TEMPLATES_NAME) $(README_NAME) $(LICENSE_NAME) $(MAKEFILE_NAME)) > $(RELEASE_ZIP_PATH) diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..891d859 --- /dev/null +++ b/Mintfile @@ -0,0 +1 @@ +realm/SwiftLint@0.52.2 diff --git a/Package.resolved b/Package.resolved index 655e055..0d278ad 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,88 +1,131 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "c1d14588e5558a3669fd03510d135d88c5109069", - "version": "5.0.0-rc.2" - } - }, - { - "package": "PathKit", - "repositoryURL": "https://github.com/kylef/PathKit.git", - "state": { - "branch": null, - "revision": "e2f5be30e4c8f531c9c1e8765aa7b71c0a45d7a0", - "version": "0.9.2" - } - }, - { - "package": "PromiseKit", - "repositoryURL": "https://github.com/mxcl/PromiseKit.git", - "state": { - "branch": null, - "revision": "4d8d1287d2e50c53a9f8430ffe88925292838c57", - "version": "6.11.0" - } - }, - { - "package": "Rainbow", - "repositoryURL": "https://github.com/onevcat/Rainbow.git", - "state": { - "branch": null, - "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", - "version": "3.1.5" - } - }, - { - "package": "Spectre", - "repositoryURL": "https://github.com/kylef/Spectre.git", - "state": { - "branch": null, - "revision": "f14ff47f45642aa5703900980b014c2e9394b6e5", - "version": "0.9.0" - } - }, - { - "package": "Stencil", - "repositoryURL": "https://github.com/kylef/Stencil.git", - "state": { - "branch": null, - "revision": "0e9a78d6584e3812cd9c09494d5c7b483e8f533c", - "version": "0.13.1" - } - }, - { - "package": "StencilSwiftKit", - "repositoryURL": "https://github.com/SwiftGen/StencilSwiftKit.git", - "state": { - "branch": null, - "revision": "dbf02bd6afe71b65ead2bd250aaf974abc688094", - "version": "2.7.2" - } - }, - { - "package": "SwiftCLI", - "repositoryURL": "https://github.com/jakeheis/SwiftCLI", - "state": { - "branch": null, - "revision": "df4d5b3ec2c1421a58d71010ff43528db5b19e04", - "version": "5.3.3" - } - }, - { - "package": "Yams", - "repositoryURL": "https://github.com/jpsim/Yams.git", - "state": { - "branch": null, - "revision": "c947a306d2e80ecb2c0859047b35c73b8e1ca27f", - "version": "2.0.0" - } + "pins" : [ + { + "identity" : "dictionarycoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/almazrafi/DictionaryCoder.git", + "state" : { + "revision" : "c340c90eeb4ff318ccea992e57e9ace8aaf3357d", + "version" : "1.1.0" } - ] - }, - "version": 1 + }, + { + "identity" : "expression", + "kind" : "remoteSourceControl", + "location" : "https://github.com/nicklockwood/Expression.git", + "state" : { + "revision" : "bd35e1919e7c596b45869a3704dc2a15fab648c4", + "version" : "0.13.7" + } + }, + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess.git", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, + { + "identity" : "komondor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/Komondor.git", + "state" : { + "revision" : "90b087b1e39069684b1ff4bf915c2aae594f2d60", + "version" : "1.1.3" + } + }, + { + "identity" : "packageconfig", + "kind" : "remoteSourceControl", + "location" : "https://github.com/shibapm/PackageConfig.git", + "state" : { + "revision" : "58523193c26fb821ed1720dcd8a21009055c7cdb", + "version" : "1.1.3" + } + }, + { + "identity" : "pathkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/PathKit.git", + "state" : { + "revision" : "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version" : "1.0.1" + } + }, + { + "identity" : "promisekit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mxcl/PromiseKit.git", + "state" : { + "revision" : "143091e1b8a0e186b8eba5aa2a38f4dc49ec73ac", + "version" : "8.0.0" + } + }, + { + "identity" : "rainbow", + "kind" : "remoteSourceControl", + "location" : "https://github.com/onevcat/Rainbow.git", + "state" : { + "revision" : "e0dada9cd44e3fa7ec3b867e49a8ddbf543e3df3", + "version" : "4.0.1" + } + }, + { + "identity" : "shellout", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/ShellOut.git", + "state" : { + "revision" : "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version" : "2.3.0" + } + }, + { + "identity" : "spectre", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kylef/Spectre.git", + "state" : { + "revision" : "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version" : "0.10.1" + } + }, + { + "identity" : "stencil", + "kind" : "remoteSourceControl", + "location" : "https://github.com/stencilproject/Stencil.git", + "state" : { + "revision" : "4f222ac85d673f35df29962fc4c36ccfdaf9da5b", + "version" : "0.15.1" + } + }, + { + "identity" : "stencilswiftkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/SwiftGen/StencilSwiftKit.git", + "state" : { + "revision" : "20e2de5322c83df005939d9d9300fab130b49f97", + "version" : "2.10.1" + } + }, + { + "identity" : "swiftcli", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jakeheis/SwiftCLI", + "state" : { + "revision" : "2e949055d9797c1a6bddcda0e58dada16cc8e970", + "version" : "6.0.3" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "0d9ee7ea8c4ebd4a489ad7a73d5c6cad55d6fed3", + "version" : "5.0.6" + } + } + ], + "version" : 2 } diff --git a/Package.resources b/Package.resources index b0d3b6d..1e1408c 100755 --- a/Package.resources +++ b/Package.resources @@ -1,2 +1 @@ Templates -Resources \ No newline at end of file diff --git a/Package.swift b/Package.swift index 8a725b8..21da2cd 100755 --- a/Package.swift +++ b/Package.swift @@ -1,46 +1,73 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.6 import PackageDescription let package = Package( name: "FigmaGen", platforms: [ - .macOS(.v10_12) + .macOS(.v12) ], products: [ - .executable( - name: "figmagen", - targets: ["FigmaGen"] - ) + .executable(name: "figmagen", targets: ["FigmaGen"]), + .library(name: "FigmaGenTools", targets: ["FigmaGenTools"]) ], dependencies: [ - .package(url: "https://github.com/jakeheis/SwiftCLI", from: "5.0.0"), - .package(url: "https://github.com/onevcat/Rainbow.git", from: "3.0.0"), - .package(url: "https://github.com/jpsim/Yams.git", from: "2.0.0"), - .package(url: "https://github.com/kylef/PathKit.git", from: "0.9.2"), - .package(url: "https://github.com/kylef/Stencil.git", from: "0.13.0"), + .package(url: "https://github.com/jakeheis/SwiftCLI", from: "6.0.3"), + .package(url: "https://github.com/onevcat/Rainbow.git", from: "4.0.0"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6"), + .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.1"), + .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.13.0"), .package(url: "https://github.com/SwiftGen/StencilSwiftKit.git", from: "2.7.2"), - .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0-rc.2"), - .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.8.0") + .package(url: "https://github.com/mxcl/PromiseKit.git", from: "8.0.0"), + .package(url: "https://github.com/almazrafi/DictionaryCoder.git", from: "1.0.0"), + .package(url: "https://github.com/nicklockwood/Expression.git", from: "0.13.0"), + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2") ], targets: [ - .target( + .executableTarget( name: "FigmaGen", dependencies: [ + "Yams", "SwiftCLI", "Rainbow", - "Yams", "PathKit", "Stencil", "StencilSwiftKit", - "Alamofire", - "PromiseKit" + "PromiseKit", + "DictionaryCoder", + "FigmaGenTools", + "Expression", + .product( + name: "KeychainAccess", + package: "KeychainAccess", + condition: .when(platforms: [.macOS]) + ) ], - path: "Sources" + path: "Sources/FigmaGen" + ), + .target( + name: "FigmaGenTools", + dependencies: [ + "SwiftCLI", + "PathKit", + "PromiseKit", + .product( + name: "KeychainAccess", + package: "KeychainAccess", + condition: .when(platforms: [.macOS]) + ) + ], + path: "Sources/FigmaGenTools" ), .testTarget( name: "FigmaGenTests", dependencies: ["FigmaGen"], - path: "Tests" + path: "Tests/FigmaGenTests" + ), + .testTarget( + name: "FigmaGenToolsTests", + dependencies: ["FigmaGenTools"], + path: "Tests/FigmaGenToolsTests" ) - ] + ], + swiftLanguageVersions: [.v5] ) diff --git a/Podfile b/Podfile deleted file mode 100644 index 443aeba..0000000 --- a/Podfile +++ /dev/null @@ -1,30 +0,0 @@ -using_bundler = defined? Bundler - -unless using_bundler - puts "\nPlease re-run using:".red - puts " bundle exec pod install\n\n" - - exit(1) -end - -platform :macos, '10.12' -inhibit_all_warnings! -use_frameworks! - -source 'https://cdn.cocoapods.org/' - -target 'FigmaGen' do - pod 'SwiftLint' - pod 'SwiftCLI' - pod 'RainbowSwift' - pod 'Yams' - pod 'PathKit' - pod 'Stencil' - pod 'StencilSwiftKit' - pod 'Alamofire', '~> 5.0.0-rc.2' - pod 'PromiseKit' -end - -target 'FigmaGenTests' do - inherit! :search_paths -end diff --git a/Podfile.lock b/Podfile.lock deleted file mode 100644 index 61eb41b..0000000 --- a/Podfile.lock +++ /dev/null @@ -1,56 +0,0 @@ -PODS: - - Alamofire (5.0.0-rc.2) - - PathKit (0.9.2) - - PromiseKit (6.11.0): - - PromiseKit/CorePromise (= 6.11.0) - - PromiseKit/Foundation (= 6.11.0) - - PromiseKit/UIKit (= 6.11.0) - - PromiseKit/CorePromise (6.11.0) - - PromiseKit/Foundation (6.11.0): - - PromiseKit/CorePromise - - RainbowSwift (3.1.5) - - Stencil (0.13.1): - - PathKit (~> 0.9.0) - - StencilSwiftKit (2.7.2): - - Stencil (~> 0.13.1) - - SwiftCLI (5.2.1) - - SwiftLint (0.36.0) - - Yams (2.0.0) - -DEPENDENCIES: - - Alamofire (~> 5.0.0-rc.2) - - PathKit - - PromiseKit - - RainbowSwift - - Stencil - - StencilSwiftKit - - SwiftCLI - - SwiftLint - - Yams - -SPEC REPOS: - trunk: - - Alamofire - - PathKit - - PromiseKit - - RainbowSwift - - Stencil - - StencilSwiftKit - - SwiftCLI - - SwiftLint - - Yams - -SPEC CHECKSUMS: - Alamofire: f9450d3c7f6bea2ad62e7a541c3e9b186c7991d6 - PathKit: 273f59a38e3218eb95abd9f6a61730a8bcfd2f06 - PromiseKit: e4863d06976e7dee5e41c04fc7371c16b3600292 - RainbowSwift: 25d1e4ae8da8360816cc21d7c753df48ec078f71 - Stencil: b5128a0a43ece9225db5cbd94d6569fe6fba609b - StencilSwiftKit: 9ebd42556b6c7396440f286b6c9aca1549c963ff - SwiftCLI: abd45071fc69b26861fd17f4f7e40f1416358e0c - SwiftLint: fc9859e4e1752340664851f667bb1898b9c90114 - Yams: cb96472112d99e4b368f8dae9ab62e8a3eb8a3f9 - -PODFILE CHECKSUM: ba07d1d6aace079717e40009fb2a157e039100fd - -COCOAPODS: 1.8.3 diff --git a/README-ru.md b/README-ru.md deleted file mode 100755 index c23eeff..0000000 --- a/README-ru.md +++ /dev/null @@ -1,177 +0,0 @@ -# FigmaGen -[![Travis CI](https://travis-ci.com/hhru/FigmaGen.svg?branch=master)](https://travis-ci.com/hhru/FigmaGen) -[![Version](https://img.shields.io/github/v/release/hhru/FigmaGen)](https://github.com/hhru/FigmaGen/releases) -[![Xcode](https://img.shields.io/badge/Xcode-11-blue.svg)](https://developer.apple.com/xcode) -[![Swift](https://img.shields.io/badge/Swift-5.1-orange.svg)](https://swift.org) -[![License](https://img.shields.io/github/license/hhru/FigmaGen.svg?style=flat)](https://opensource.org/licenses/MIT) - -FigmaGen - инструмент командной строки для генерации кода на основе библиотеки компонентов в Figma. - -Поддерживает генерацию: -- цветовых стилей -- текстовых стилей - -#### Навигация -- [Установка](#конфигурация) - - [CocoaPods](#cocoapods) - - [Homebrew](#homebrew) - - [Вручную](#вручную) -- [Использование](#использование) -- [Конфигурация](#конфигурация) - - [Базовые параметры](#базовые-параметры) - - [Токен доступа Figma](#токен-доступа-figma) - - [Файл Figma](#файл-figma) - - [Настройки генерации](#настройки-генерации) -- [Цвета](#цвета) -- [Текстовые стили](#текстовые-стили) -- [Лицензия](#лицензия) - -## Установка -### CocoaPods -Для установки FigmaGen с помощью менеджера зависимостей [CocoaPods](http://cocoapods.org) добавьте строку в `Podfile`:  -```ruby -pod 'FigmaGen', '~> 1.0.0' -``` - -Затем выполните команду: -```sh -$ pod install --repo-update -``` - -В случае установки FigmaGen посредством CocoaPods команда генерации должна включать относительный путь к папке `Pods/FigmaGen`: -```sh -$ Pods/FigmaGen/figmagen generate -``` - -### Homebrew -Для установки FigmaGen средствами менеджера пакетов [Homebrew](https://brew.sh) выполните команду: -```sh -$ brew install hhru/tap/FigmaGen -``` - -### Вручную -- Откройте [страницу релизов репозитория](https://github.com/hhru/FigmaGen/releases). -- Загрузить архив 'figmagen-x.y.z.zip', прикреплённый к последнему релизу. -- Распакуйте архив в удобную папку проекта. - -**Важно**: в случае такой установки команда генерации должна включать относительный путь к папке с содержимым ZIP-архива, -например: -```sh -$ MyFolder/figmagen generate -``` - -## Использование -Для генерации кода необходимо вызвать команду: -```sh -$ figmagen generate -``` -Результатом её выполнения являются файлы исходного кода, согласно конфигурации (см. [Конфигурация](#конфигурация)), -которая по умолчанию должна находиться в файле `.figmagen.yml`. - -При желании можно использовать другой путь к конфигурации, передав его в параметре `--config`, например: -```sh -$ figmagen generate --config 'Folder/figmagen.yml' -``` - -Получаемый код может быть легко кастомизирован, -благодаря [Stencil-шаблонам](https://github.com/stencilproject/Stencil). -Если возможностей стандартных шаблонов недостаточно, можно использовать собственный, -указав путь к нему в [конфигурации](#конфигурация). - -FigmaGen запрашивает данные файлов, используя [Figma API](https://www.figma.com/developers/api), -поэтому для генерации кода требуется интернет-соединение. - -## Конфигурация -Для конфигурации FigmaGen используется файл в формате [YAML](https://yaml.org). -В нем должны быть определены все обязательные параметры генерации кода. - -Конфигурация делится на несколько разделов: -- `base`: базовые параметры, используемые всеми подкомандами генерации (см. [Базовые параметры](#базовые-параметры)). -- `colors`: конфигурация подкоманды генерации цветовых стилей (см. [Цветовые стили](#цветовые-стили)). -- `textStyles`: конфигурация подкоманды генерации текстовых стилей (см. [Текстовые стили](#текстовые-стили)). - -### Базовые параметры -Каждая подкоманда генерации использует следующие базовые параметры: -- `accessToken`: строка с токеном доступа, необходимый для выполнения запросов Figma API -(см. [Токен доступа Figma](#токен-доступа-figma)). -- `fileKey`: идентификатор файла Figma, данные которого будут использованы для генерации кода -(см. [Файл Figma](#файл-figma)). - -Пример: -```yaml -base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc -... -``` - -Если какой-либо из базовых параметров отсутствует и в разделе подкоманды генерации, и в разделе `base`, -то в результате выполнения команды `figmagen generate` будет получена соответствующая ошибка. - -### Токен доступа Figma -Для получения файлов Figma необходима авторизация, которая реализуется путём передачи персонального токена доступа. -Такой токен может быть создан в несколько простых шагов: -1. Открыть [настройки аккаунта](https://www.figma.com/settings) в Figma. -2. Нажать кнопку "Create a new personal access token" в разделе "Personal Access Tokens". -3. Ввести описание для создаваемого токена (например, "FigmaGen"). -4. Скопировать созданный токен в буфер обмена. -5. Вставить токен доступа в поле `accessToken` конфигурации - -### Файл Figma -FigmaGen запрашивает файл Figma по его стандартному идентификатору, -который может быть получен из URL в адресной строке браузера. -В общем виде этот URL имеет следующий формат: -``` -https://www.figma.com/file/<идентификатор>/<название>?node-id=<идентификатор выделенного узла> -``` - -Получив идентификатор файла, его необходимо указать в поле `fileKey` конфигурации. - -### Настройки генерации -Для каждой подкоманды генерации необходимо определить следующие параметры: -- `destinationPath`: путь для сохранения сгенерированного файла. -- `templatePath`: путь к файлу Stencil-шаблона. -Если параметр пропущен, то будет использован стандартный шаблон. -- `includingNodes`: массив строк с идентификаторами узлов, которые должны быть использованы при генерации кода. -Если этот параметр пропущен, то будут использованы все узлы файла. -- `excludingNodes`: массив строк с идентификаторами узлов, которые должны быть проигнорированы при генерации кода. -Если этот параметр пропущен, то будут использованы все узлы файла, указанные в поле `includingNodes`. - ---- - -## Цвета -Пример конфигурации: -```yaml -base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc -colors: - destinationPath: Sources/Generated/Colors.swift - includingNodes: - - 7:24 -``` - -Пример использования сгенерированного кода: -```swift -view.backgroundColor = Colors.carolina -``` - -## Текстовые стили -Пример конфигурации: -```yaml -base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc -textStyles: - destinationPath: Sources/Generated/TextStyle.swift - includingNodes: - - 3:19 -``` - -Пример использования сгенерированного кода: -```swift -label.attributedText = "Hello World".styled(.title1, textColor: Colors.black) -``` - -## Лицензия -FigmaGen доступен для использования под лицензией MIT (см. [LICENSE](LICENSE)). diff --git a/README.md b/README.md index 4a226aa..3b1edb6 100755 --- a/README.md +++ b/README.md @@ -1,171 +1,612 @@ # FigmaGen -[![Travis CI](https://travis-ci.com/hhru/FigmaGen.svg?branch=master)](https://travis-ci.com/hhru/FigmaGen) +[![Build Status](https://github.com/hhru/FigmaGen/workflows/CI/badge.svg?event=push)](https://github.com/hhru/FigmaGen/actions?query=event%3Apush) [![Version](https://img.shields.io/github/v/release/hhru/FigmaGen)](https://github.com/hhru/FigmaGen/releases) -[![Xcode](https://img.shields.io/badge/Xcode-11-blue.svg)](https://developer.apple.com/xcode) -[![Swift](https://img.shields.io/badge/Swift-5.1-orange.svg)](https://swift.org) -[![License](https://img.shields.io/github/license/hhru/FigmaGen.svg?style=flat)](https://opensource.org/licenses/MIT) +[![Xcode](https://img.shields.io/badge/Xcode-13-blue.svg)](https://developer.apple.com/xcode) +[![Swift](https://img.shields.io/badge/Swift-5.5-orange.svg)](https://swift.org) +[![Platforms](https://img.shields.io/badge/platforms-macOS%20%7C%20Linux-lightgrey)](https://swift.org/about/#platform-support) +[![License](https://img.shields.io/github/license/hhru/FigmaGen.svg)](https://github.com/hhru/FigmaGen/blob/master/LICENSE) -FigmaGen - a command line tool to generate code for UI styles using Figma components. +FigmaGen is a command line tool for exporting resources and generating code from your [Figma](http://figma.com/) files. -It can generate: -- color styles -- text styles +Currently, FigmaGen supports the following entities: +- ✅ Color styles +- ✅ Text styles +- ✅ Shadow styles +- ✅ Images -[README на русском языке](README-ru.md) +#### Watch the video +[![Watch the video](Docs/PlayVideo.png)](https://youtu.be/SfZb8iu2bWY) -#### Navigation +#### Table of context - [Installation](#installation) - [CocoaPods](#cocoapods) - [Homebrew](#homebrew) - - [Manually](#manually) + - [Mint](#mint) + - [ZIP archive](#zip-archive) - [Usage](#usage) - [Configuration](#configuration) - [Base parameters](#base-parameters) - [Figma access token](#figma-access-token) - [Figma file](#figma-file) - - [Generation settings](#generation-settings) -- [Colors](#color-styles) + - [Generation configuration](#generation-configuration) +- [Color styles](#color-styles) - [Text styles](#text-styles) +- [Shadow styles](#shadow-styles) +- [Images](#images) +- [Tokens](#tokens) +- [Working on FigmaGen](#Working-on-FigmaGen) +- [Communication](#communication) - [License](#license) -## Installation + +## Installation: ### CocoaPods -To install FigmaGen using [CocoaPods](http://cocoapods.org) add the following line to your `Podfile`:  +To install FigmaGen using [CocoaPods](http://cocoapods.org) dependency manager, add this line to your `Podfile`: ```ruby -pod 'FigmaGen', '~> 1.0.0' +pod 'FigmaGen', '~> 2.0.0-beta.24' ``` -Then run in Terminal: +Then run this command: ```sh $ pod install --repo-update ``` -If FigmaGen installed using CocoaPods then one should use the relative path to `Pods/FigmaGen` folder while using `generate` command: + +If installing using CocoaPods, the generate command should include a relative path to the `Pods/FigmaGen` folder: ```sh $ Pods/FigmaGen/figmagen generate ``` +> Installation via CocoaPods is recommended, as it allows to use the fixed version on all team members machines +> and automate the update process. + ### Homebrew -To install FigmaGen using [Homebrew](https://brew.sh) run: +For [Homebrew](https://brew.sh) dependency manager installation, run: +```sh +$ brew install hhru/tap/figmagen +``` + +> It's impossible to set a specific package version via Homebrew. +> If you chose this method, make sure all team members use the same version of FigmaGen. + +### Mint + +For [Mint](https://github.com/yonaskolb/mint) package manager installation, run: + ```sh -$ brew install hhru/tap/FigmaGen +$ mint install hhru/FigmaGen@2.0.0-beta.24 ``` -### Manually -- Go to [releases page](https://github.com/hhru/FigmaGen/releases). -- Download the latest release `figmagen-x.y.z.zip` -- Unzip the archive +### ZIP archive + +Every release in the repo has a ZIP archive which can be used to integrate FigmaGen into a project. +To use that method, the following steps should be taken: +- Open [repository release page](https://github.com/hhru/FigmaGen/releases). +- Download the 'figmagen-x.y.z.zip' archive attached to the latest release. +- Unarchive files into a convenient project folder -**Important**: in this case one should use the relative path to the archive content folder while using `generate` command: +If this integration method was chosen, +the generation command should include a relative path to the folder with the ZIP-archive content, for example: ```sh -$ MyFolder/figmagen generate +$ FigmaGen/figmagen generate ``` +> It's recommended to include unarchived files into the Git index (`git add FigmaGen`). +> It will guarantee that all team members are using the same version of FigmaGen for the project. + + ## Usage -To generate the code run in Terminal: +FigmaGen provides a simple command for code generation: ```sh $ figmagen generate ``` -The command generates source code files according to the configuration (see [Configuration](#configuration)), described in `.figmagen.yml` file. +As the result, the source code files will be received according to the configuration (see [Configuration](#configuration)), +which by default should be placed to `.figmagen.yml` file. -One can use another configuration file passing its path in `--config` parameter: +If you need, you can use a specific path to the configuration, just pass it in the `--config` parameter. For example: ```sh $ figmagen generate --config 'Folder/figmagen.yml' ``` -The generated source code can be customised using [Stencil-templates](https://github.com/stencilproject/Stencil). -If the standard templates do not fit your needs then use your own one, passing its path in the [configuration](#configuration). +FigmaGen requests files data using [Figma API](https://www.figma.com/developers/api), +so make sure you have the internet connection on while generating the code. + +The resulting code could be easily customized with [Stencil-templates](https://github.com/stencilproject/Stencil). +If standard templates are not enough, a custom template could be used. Just +specify its path in the [configuration](#configuration). + +### Integration +There is no point to run `figmagen generate` at the build stage, as FigmaGen doesn't work with local resources, +which can be changed during development. All data for code generation is in Figma. +Also, there won't be any merge conflicts, if you use design versioning. + +So, it is much better to generate code just once and keep it in the Git index. +Also run `figmagen generate` for the following reasons: +- to upgrade to a new design version in Figma +- after updating FigmaGen version + +There are also some recommendations on integration based on technologies used in a project. +All of them are listed in this section and will be updated as feedback is received. + +In case you have any problems or suggestions related to integration, +please open the corresponding request in [Issues](https://github.com/hhru/FigmaGen/issues). + +#### CocoaPods +If you are using [CocoaPods](http://cocoapods.org) dependency manager, +run code generating command from `pre-install` event handler in `Podfile`: +```ruby +pre_install do |installer| + system "Pods/FigmaGen/figmagen generate" +end +``` + +That will allow connecting the code generation command with updating FigmaGen version +and will reduce the number of commands executed while cloning the project. + +🚨 If you want to keep the generated files in the Development Pod, this integration method is ideal. +In this case the generation should be run after loading FigmaGen and before installing all Pods. +Otherwise, new files will not be indexed on time and won't get included into the Xcode-project. -FigmaGen uses [Figma API](https://www.figma.com/developers/api), thus it needs an internet connection. ## Configuration -FigmaGen can be configured using a file in [YAML](https://yaml.org) format. -This file should contain all the required parameters. +[YAML](https://yaml.org) file is used for FigmaGen configuration. +Define all necessary parameters of code generation in this file. -The configuration file structured into several sections: -- `base`: base parameters that being used by all the commands (см. [Base parameters](#base-parameters)). -- `colors`: color styles generation configuration (см. [Color styles](#color-styles)). -- `textStyles`: text styles generation configuration (см. [Text styles](#text-styles)). +Configuration is divided into several sections: +- `base`: base parameters that are actual for all other configuration sections (see [Base parameters](#base-parameters)) +- `colorStyles`: parameters of color styles generation step (see [Color styles](#color-styles)). +- `textStyles`: parameters of text styles generation step (see [Text styles](#text-styles)). +- `images`: parameters of the step of loading and generating code for images (see [Images](#images)). + +Any parameter from `base` section will be inherited and could be overwritten in the section of the particular generation step. +If some section of the generation step is missing in the configuration, +it will be skipped during `figmagen generate` command execution. ### Base parameters -All the generation commands use the following base parameters: -- `accessToken`: access token needed to perform requests to Figma API -(see [Figma access token](#figma-access-token)). -- `fileKey`: identifier of a Figma file that will be used for code generation -(see [Figma file](#figma-file)). +Each step of generation is using the following base parameters: +- `accessToken`: an access token string that is needed to execute Figma API requests (see [Figma access token](#figma-access-token)). +- `file`: URL of a Figma file, which data will be used to generate code (see [Figma file](#figma-file)). -Example: +In order not to duplicate these fields in the configuration, you can specify them in the `base` section. +They will be used by those generation steps for which these parameters are not defined. +For example: ```yaml base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc -... + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + +colorStyles: { } + +textStyles: + file: https://www.figma.com/file/SSeboI2x0jmeG4QO8iBMqX/FigmaGen-Demo-iOS ``` -If a base parameter is missing both in `base` section and in a concrete section (`colors` or `textStyles`) then one'll receive an error after running `figmagen generate` command. +If a base parameter is missing for both the generation step section and in the `base` section, +then as a result of the execution `figmagen generate` command, the corresponding error will be received. ### Figma access token -In order to get access to Figma files one have to be authorized via a personal access token. -How to obtain a token: -1. Open Figma [account settings](https://www.figma.com/settings). -2. Press "Create a new personal access token" in "Personal Access Tokens" section. -3. Input the token description ("FigmaGen", for example). -4. Copy the token to the clipboard. -5. Paste the token to `accessToken` field in the configuration file +Authorization is required to receive Figma files. +The authorization is implemented by transferring a personal access token. +This token could be created in a few simple steps: +1. Open [account settings]((https://www.figma.com/settings)) in Figma. +2. Press "Create a new personal access token" button in the "Personal Access Tokens" section. +3. Enter a description for the token (for instance, "FigmaGen"). +4. Copy the created token to the clipboard. + +![](Docs/AccessToken.png) + +Then paste the received access token in the `accessToken` field of the configuration. +It is enough to define it only in the `base` section if this token allows access to all Figma files, +which appear in the configuration. +For example: +```yaml +base: + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 +... +``` + +You can also set the name of the environment variable in the `env` field instead of the access token value itself. +For example: +```yaml +base: + accessToken: + env: FIGMAGEN_ACCESS_TOKEN +... +``` + +If for a certain file you need to use a different access token, +it should be specified in the corresponding section of the configuration (see [Base parameters](#base-parameters)). ### Figma file -FigmaGen requests the Figma file by its identifier, that can be copied from the file URL: -This URL has the following format: +FigmaGen requests Figma file by using the identifier from its URL. This URL should be placed in the `file` field of the configuration. +For example: +```yaml +base: + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo +... +``` + +In addition to the file identifier itself, the URL could also contain additional parameters +and generally has the following format: +```url +https://www.figma.com/file/:id/:title?version-id=:version-id&node-id=:node-id ``` -https://www.figma.com/file//?node-id= + +To get the file, the following parameters are used: +- `id`: the identifier of the file. +- `version-id`: the identifier of the file version. +If this parameter is omitted, the current version will be used. +- `node-id`: identifier of the selected frame. +If this parameter is present, then only the data from this frame will be used. + +The URL of the Figma file opened in the browser can be easily obtained from the address bar. + +![](Docs/FileURL.png) + +🚨 Be careful with the `node-id` parameter, as in Figma the wrong frame could be selected. +Then the wrong data will be used for generation. + +#### Alternative configuration +Sometimes using the file URL is not flexible enough. +In this case you can define an object with the following fields instead of the URL in the `file` parameter: +- `key`: a string with the file's identifier. Is required. +- `version`: a string with the file's version. +If this parameter is omitted, the current file version will be used. +- `includedNodes`: an array of strings with nodes identifiers, that should be used for code generation. +If this parameter is omitted, all file nodes will be used. +- `excludedNodes`: an array of strings with nodes identifiers that should be ignored when generating the code. +If this parameter is omitted, all file nodes specified in the `includedNodes` field will be used. + +The values for these fields must be manually extracted from the file URL, according to its format. +For example, for URL `https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo?version-id=194665614&node-id=0%3A1` +the configuration will look like: +```yaml +base: + file: + key: 61xw2FQn61Xr7VVFYwiHHy + version: 201889163 + includedNodes: + - 0%3A1 +... ``` -Once the file identifier is obtained then insert it to `fileKey` in the configuration file. +Such a representation may be useful for implementing more complex filtering of nodes. +For example, when it is necessary to exclude several elements specific to another platform from the code generation. + +### Generation configuration +Besides [base parameters](#base-parameters), +for each generation step the following configuration should be defined: +- `template`: a path to the Stencil-template file. +If omitted, the standard template will be used. +- `templateOptions`: a dictionary with additional options that will be used for generation in Stencil-template. +- `destination`: a path to save the generated file. +If omitted, the generated code will be displayed in the console. -### Generation settings -For all the commands one should set the following parameters: -- `destinationPath`: path to the resulting file. -- `templatePath`: path to the Stencil-template. -If missing then the default template will be used. -- `includingNodes`: array of node identifiers that should be used for code generation. -If missing then all the nodes will be used. -- `excludingNodes`: array of node identifiers that should be ignored. -If missing then all the nodes from `includingNodes` array will be used. +These generation steps also could have additional parameters. +The description of them and examples could be found in corresponding sections below. --- ## Color styles -Configuration example +To generate color styles [standard configuration set](#generation-configuration) with an additional parameter is used: +- `assets`: a path to the Xcode assets folder, in which all colors will be saved as a Color Set. +The parameter can be omitted if there is no need for assets. + +Sample configuration: ```yaml -base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc -colors: - destinationPath: Sources/Generated/Colors.swift - includingNodes: - - 7:24 +colorStyles: + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + assets: Sources/Assets.xcassets/Colors + destination: Sources/ColorStyle.swift + templateOptions: + publicAccess: true ``` -Usage example: +#### Xcode-assets +It's recommended to specify the path to a subfolder inside the Xcode-assets folder in the `assets` parameter, +so all colors are saved in this subfolder. For example `Sources/Assets.xcassets/Colors`. +The whole assets structure be created if it was missing. + +🚨 Folder specified in the `assets` parameter will be emptied before saving colors there. +You shouldn't use the same path for different generation steps, +but you can use different subfolders of the assets folder, +for example `Sources/Assets.xcassets/Colors` and `Sources/Assets.xcassets/Images`. + +#### Standard template +Examples of usage of the generated code: ```swift -view.backgroundColor = Colors.carolina +view.backgroundColor = ColorStyle.razzmatazz.color +// or +view.backgroundColor = UIColor(style: .razzmatazz) ``` +The template could be configured using different options that are specified in `templateOptions` parameter: + +Key | Type | Default value | Description +---- | ---- | ------------- | ----------- +`styleTypeName` | String | ColorStyle | Style type name. +`colorTypeName` | String | UIColor | Color type name. If the target platform is macOS, specify `NSColor`. +`publicAccess` | Boolean | false | Adds `public` access modifier to the declarations in the generated file. + ## Text styles -Configuration example: +To generate text styles [standard configuration set](#generation-configuration) is used. + +Sample configuration: ```yaml -base: - accessToken: 27482-71b3313c-0e88-481b-8c93-0e465ab8a868 - fileKey: ZvsRf99Ik11qS4PjS6MAFc textStyles: - destinationPath: Sources/Generated/TextStyle.swift - includingNodes: - - 3:19 + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + destination: Sources/TextStyle.swift + templateOptions: + publicAccess: true +``` + +#### Standard template +Sample usage of the generated code: +```swift +label.attributedText = TextStyle.title.attributetemplateOptionsdString("Hello world") +// or +label.attributedText = NSAttributedString(string: "Hello world", style: .title) +// or +label.attributedText = TextStyle + .title + .withColor(.white) + .withLineBreakMode(.byWordWrapping) + .attributedString("Hello world") +``` + +The template could be configured using additional options that specified in `templateOptions` parameter: + +Key | Type | Default value | Description +---- | ---- | ------------- | ----------- +`styleTypeName` | String | TextStyle | Style type name. +`colorTypeName` | String | UIColor | Color type name. If the target platform is macOS, specify `NSColor`. +`fontTypeName` | String | UIFont | Font type name. If the target platform is macOS, specify `NSFont`. +`publicAccess` | Boolean | false | Adds `public` access modifier to the declarations in the generated file. +`usingSystemFonts` | Boolean | false | If text style has system font (**SFProText** or **SFProDisplay**), then `font` property will be using `systemFont(ofSize:weight:)` method. + +## Shadow styles + +To generate shadow styles [standard configuration set](#generation-configuration) is used. + +Sample configuration: + +```yaml +shadowStyles: + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + destination: Sources/ShadowStyle.swift + templateOptions: + publicAccess: true +``` + +#### Standard template + +Sample usage of the generated code: + +```swift +// If style contains only one shadow, you can set it to any view or layer +label.shadow = .thinShadow + +// Styles with multiple shadows can only be set to an instance of ShadowStyleView +let cardView = ShadowStyleView() + +cardView.backgroundColor = .white +cardView.shadowStyle = .cardShadow ``` -Usage example: +The template could be configured using additional options that specified in `templateOptions` parameter: + +Key | Type | Default value | Description +---- | ---- | ------------- | ----------- +`styleTypeName` | String | ShadowStyle | Style type name. +`shadowTypeName` | String | Shadow | Shadow type name. +`shadowStyleLayerTypeName` | String | ShadowStyleLayer | Name of generated layer that provides rendering of shadow style. +`shadowStyleViewTypeName` | String | ShadowStyleView | Name of generated view to which the shadow style can be set. +`colorTypeName` | String | UIColor | Color type name. If the target platform is macOS, specify `NSColor`. +`viewTypeName` | String | UIView | View type name. If the target platform is macOS, specify `NSView`. +`bezierPathTypeName` | String | UIBezierPath | Bezier path type name. If the target platform is macOS, specify `NSBezierPath`. +`publicAccess` | Boolean | false | Adds `public` access modifier to the declarations in the generated file. + +## Images + +To load and generate code for images, the [standard configuration set](#generation-configuration) is used with additional parameters: +- `assets`: a path to Xcode-assets folder in which the images will be saved as Image Set. +The parameter could be omitted if there is no need for assets. +- `resources`: a path to the resources folder in which the image files will be saved. +The parameter can be skipped, if, for example, you only want to save assets. +- `format`: string with images format. `pdf`, `png`, `svg`, `jpg` are allowed. +The default format is `pdf`. +- `scales`: array with integer scaling factors from 1 to 3. +The default scaling factor is 1, so the image will have the original size. +- `onlyExportables`: renders only exportable components. +The default value is `false`. +- `useAbsoluteBounds`: uses full dimensions of the node. +The default value is `false`. +- `preserveVectorData`: sets `Preserve Vector Data` flag in Xcode assets. +The default value is `false`. + + +Sample configuration: +```yaml +images: + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + assets: Sources/Assets.xcassets/Images + destination: Sources/Images.swift + onlyExportables: true + useAbsoluteBounds: true + templateOptions: + publicAccess: true +``` + +#### Figma components +FigmaGen only uses nodes that are [components](https://help.figma.com/article/66-components) as images. +So, make sure that the chosen frame in the file URL (see [Figma file](#figma-file)) +allows to filter out the components that should not render images in Figma file. + +#### Only exportables +If you want to export only those components +that have [export settings](https://help.figma.com/hc/en-us/articles/360040028114-Getting-Started-with-Exports) in Figma, +set the `onlyExportables` flag to `true`. + +#### Use absolute bounds +By default FigmaGen exports the images using only space that is actually occupied by them, so if the node has extra space +around, it will be cropped. If you want to preserve this space set the `useAbsoluteBounds` to `true`. +See [Image Endpoint Description](https://www.figma.com/developers/api#get-images-endpoint) for details. + +#### Xcode-assets +It's recommended to specify the path to a subfolder inside the Xcode-assets folder in the `assets` parameter, +so all colors are saved in this subfolder. For example `Sources/Assets.xcassets/Images`. +The whole assets structure be created if it was missing. + +🚨 Folder specified in the `assets` parameter will be emptied before saving colors there. +You shouldn't use the same path for different generation steps, +but you can use different subfolders of the assets folder, +for example `Sources/Assets.xcassets/Colors` and `Sources/Assets.xcassets/Images`. + +#### Formats +For Xcode projects, it is recommended to use PDF format without additional scaling factors, +for that it is enough not to specify the `format` and `scales` parameter. + +There is no point to use SVG for Xcode projects, as it only could be used in other platforms (for example in Android). + +#### Scaling factors +An image of the corresponding size will be rendered for each scaling factor in the `scales` array, +regardless of the specified format in the `format` parameter. +However, it is not recommended to use additional scaling factors for vector PDF and SVG formats. + +When saved in Xcode assets, files of each scaled image will be kept in the same Image Set with Individual Scales option. +If the `scales` parameter is omitted in the configuration, the Image Set will have the Single Scale scaling type. + +#### Default template +Sample usage of the generated code: ```swift -label.attributedText = "Hello World".styled(.title1, textColor: Colors.black) +imageView.image = Images.menuIcon +``` + +The template could be configured using additional options specified in `templateOptions` parameter: + +Key | Type | Default value | Description +---- | ---- | ------------- | ----------- +`imagesEnumName` | String | Images | Name of a generated enum with static image fields +`imageTypeName` | String | UIImage | Image type name. If the target platform is macOS, specify `NSImage`. +`publicAccess` | Boolean | false | Adds `public` access modifier to the declarations in the generated file. + +## Tokens + +Configuration sections for generationg Tokens: +- `accessToken`: an access token string that is needed to execute Figma API requests (see [Figma access token](#figma-access-token)). +- `file`: URL of a Figma file, which data will be used to generate code (see [Figma file](#figma-file)). +- `remoteRepoConfig`: parameters that will be used to generate tokens from remote file (see [Remote repository parameters](#remote-repository-parameters)) +- `templates`: parameters of styles for generation (see [Parameters of styles](#parameters-of-styles-for-generation)) + +### Token parameters +Sample configuration: +```yaml +tokens: + accessToken: 25961-4ac9fbc9-3bd8-4c43-bbe2-95e477f8a067 + file: https://www.figma.com/file/61xw2FQn61Xr7VVFYwiHHy/FigmaGen-Demo + remoteRepoConfig: { [remote repository parameters](#remote-repository-parameters) } + templates: + colors: + { standard configuration set} + baseColors: + { standard configuration set } + fontFamilies: + { standard configuration set } + typographies: + { standard configuration set } + boxShadows: + { standard configuration set } + spacing: + { standard configuration set } + theme: + { standard configuration set } +``` + +### Parameters of styles for generation + +Sample configuration for color styles: +```yaml + colors: + destination: ./Generated/ColorTokens.swift + templateOptions: + publicAccess: true +``` + +### Remote repository parameters + +Remote repository section: +- `owner`: owner of the remote repository +- `repo`: name of the remote repository +- `branch`: name of the branch in remote repository +- `filePath`: file name or path to the file in remote repository +- `accessToken`: an access token string or object that is needed to execute GitHub API requests + +Remote repository parameters structure: +```yaml + remoteRepoConfig: + owner: { your repository owner } + repo: { your repository name } + branch: { your repository branch } + filePath: { your repository file path } + accessToken: { your GitHub access token } + # OR + accessToken: + env: { your repository environment } + keychain: + service: { service name } + key: { key name } +``` + +Sample configuration for remote repository parameters with string `accessToken`: +```yaml + remoteRepoConfig: + owner: hhru + repo: FigmaGen + branch: master + filePath: tokens.json + accessToken: ghp_259614ac9fbc93bd84c43bbe2bt53m0TaM4A ``` +Sample configuration for remote repository parameters with object `accessToken`: +```yaml + remoteRepoConfig: + owner: hhru + repo: FigmaGen + branch: master + filePath: tokens.json + accessToken: + env: GITHUB_API + keychain: + service: GitHub Token + key: hh +``` + +## Working on FigmaGen + +To work on FigmaGen you need to open the `Package.swift` file, select the `figmagen` scheme and edit the scheme + +#### +![](Docs/Choose_and_Edit_Scheme.png) + +#### Arguments for generating tokens with FigmaGen from file configurations +![](Docs/Generate_from_config.png) + +#### Arguments for generating tokens with FigmaGen using a Figma file +![](Docs/Generate_tokens_from_Figma.png) + +#### Arguments for generating tokens with FigmaGen using a Github file +![](Docs/Generate_tokens_from_GitHub_file.png) + +--- + +## Communication +- If you need help, open an issue. +- If you found a bug, open an issue. +- If you have a feature request, open an issue. +- If you want to contribute, submit a pull request. + ## License -FigmaGen is released under the MIT License. (see [LICENSE](LICENSE)). +FigmaGen is available under the MIT license. See the [LICENSE](LICENSE) file for more info. diff --git a/Scripts/Bootstrap/bundler.sh b/Scripts/Bootstrap/bundler.sh new file mode 100755 index 0000000..901ba37 --- /dev/null +++ b/Scripts/Bootstrap/bundler.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +readonly arguments=$@ +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +echo "Checking ${bundler_style}Bundler${default_style} installation:" + +if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +eval "$(rbenv init -)" + +if rbenv which bundler &> /dev/null; then + if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then + echo " Bundler already installed. Updating..." + assert_failure 'gem update bundler' + else + echo " Bundler already installed." + fi +else + echo " Bundler not found. Installing..." + assert_failure 'gem install bundler' +fi + +echo "" diff --git a/Scripts/Bootstrap/common.sh b/Scripts/Bootstrap/common.sh new file mode 100755 index 0000000..e463e9e --- /dev/null +++ b/Scripts/Bootstrap/common.sh @@ -0,0 +1,91 @@ +#!/bin/sh + +if [ -z "${script_path}" ]; then + readonly script_path="$( cd "$( dirname "$0" )" && pwd )" +fi + +readonly helpers_path="${script_path}/../Helpers" + +source "${helpers_path}/script-paths.sh" + +readonly default_style='\033[0m' +readonly warning_style='\033[33m' +readonly error_style='\033[31m' + +readonly macos_style='\033[38;5;99m' +readonly xcode_style='\033[38;5;75m' +readonly homebrew_style='\033[38;5;208m' +readonly rbenv_style='\033[38;5;43m' +readonly ruby_style='\033[38;5;89m' +readonly bundler_style='\033[38;5;45m' +readonly swiftenv_style='\033[38;5;226m' +readonly swift_style='\033[38;5;208m' +readonly spm_style='\033[38;5;202m' +readonly mint_style='\033[0;38;5;77m' +readonly congratulations_style='\033[38;5;48m' + +readonly update_flag='--update' +readonly verify_flag='--verify' + +failure() { + echo "${error_style}Fatal error:${default_style} '$1' failed with exit code $2" + exit 1 +} + +warning() { + echo "${warning_style}Warning:${default_style} '$1' failed with exit code $2" +} + +assert_failure() { + eval $1 2>&1 | sed -e "s/^/ /" + + local exit_code=${PIPESTATUS[0]} + + if [ $exit_code -ne 0 ]; then + failure "$1" $exit_code + fi +} + +assert_warning() { + eval $1 2>&1 | sed -e "s/^/ /" + + local exit_code=${PIPESTATUS[0]} + + if [ $exit_code -ne 0 ]; then + warning "$1" $exit_code + fi +} + +brew_install_if_needed() { + local options=$@ + local formulae=$1 + + if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + fi + + if brew ls --versions "${formulae}" &> /dev/null; then + if [[ " ${options[*]} " == *" ${update_flag} "* ]]; then + echo " ${formulae} already installed. Updating..." + + brew_outdated=$(brew outdated 2> /dev/null) + brew_outdated_exit_code=$? + + if [ $brew_outdated_exit_code -ne 0 ]; then + echo " Failed to find outdated formulae." + warning 'brew outdated' $brew_outdated_exit_code + else + if [[ $brew_outdated == *"${formulae}"* ]]; then + assert_failure 'brew upgrade ${formulae}' + else + echo " Already up-to-date." + fi + fi + else + echo " ${formulae} already installed." + fi + else + echo " ${formulae} not found. Installing..." + assert_failure 'brew install "${formulae}"' + fi +} diff --git a/Scripts/Bootstrap/congratulations.sh b/Scripts/Bootstrap/congratulations.sh new file mode 100755 index 0000000..9566b4f --- /dev/null +++ b/Scripts/Bootstrap/congratulations.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +echo "" +echo "${congratulations_style}Congratulations!${default_style} Setting up the development environment successfully completed 🥳" +echo "" diff --git a/Scripts/Bootstrap/gemfile.sh b/Scripts/Bootstrap/gemfile.sh new file mode 100755 index 0000000..c3ac301 --- /dev/null +++ b/Scripts/Bootstrap/gemfile.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +readonly arguments=$@ +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +eval "$(rbenv init -)" + +if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then + echo "Updating ${ruby_style}Ruby gems${default_style} specified in Gemfile..." + assert_failure '(cd "${root_path}" && bundle update)' +else + echo "Installing ${ruby_style}Ruby gems${default_style} specified in Gemfile..." + assert_failure '(cd "${root_path}" && bundle install)' +fi + +echo "" diff --git a/Scripts/Bootstrap/homebrew.sh b/Scripts/Bootstrap/homebrew.sh new file mode 100755 index 0000000..0f26f6c --- /dev/null +++ b/Scripts/Bootstrap/homebrew.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +readonly arguments=$@ +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +readonly shell_init_line="eval \"\$(/opt/homebrew/bin/brew shellenv)\"" + +setup_shell() { + local shell_profile_path=$1 + + if [[ ! -f "${shell_profile_path}" ]]; then + > "${shell_profile_path}" + fi + + if [[ $(grep -L "${shell_init_line}" "${shell_profile_path}") ]]; then + echo "${shell_init_line}" >> "${shell_profile_path}" + fi +} + +echo "Checking ${homebrew_style}Homebrew${default_style} installation:" + +if which -s brew; then + if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then + echo " Homebrew already installed. Updating..." + assert_failure 'brew update' + else + echo " Homebrew already installed." + fi +else + echo " Homebrew not found. Installing..." + assert_failure 'bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"' +fi + +if [[ "$(uname -m)" == "arm64" ]]; then + setup_shell "${HOME}/.zshrc" + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +if [[ " ${arguments[*]} " == *" ${verify_flag} "* ]]; then + echo "" + echo " Verifying that Homebrew is properly set up..." + + assert_warning 'brew doctor' +fi + +echo "" diff --git a/Scripts/Bootstrap/macos.sh b/Scripts/Bootstrap/macos.sh new file mode 100755 index 0000000..8b2d85d --- /dev/null +++ b/Scripts/Bootstrap/macos.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +plain_version() { + echo "$@" | awk -F. '{ printf("%d%03d%03d%03d", $1,$2,$3,$4); }' +} + +echo "Checking ${macos_style}macOS${default_style} version:" + +readonly macos_required_version='11.3.0' +readonly macos_version=$(/usr/bin/sw_vers -productVersion 2>&1) + +if [ "$(plain_version ${macos_version})" -lt "$(plain_version ${macos_required_version})" ]; then + echo " ${error_style}Your macOS version (${macos_version}) is older then required version (${macos_required_version}). Exiting...${default_style}" + exit 1 +else + echo " Your macOS version: ${macos_version}" +fi + +echo "" diff --git a/Scripts/Bootstrap/mint.sh b/Scripts/Bootstrap/mint.sh new file mode 100755 index 0000000..5340701 --- /dev/null +++ b/Scripts/Bootstrap/mint.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +readonly arguments=$@ +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +readonly shell_mint_path_line="export MINT_PATH=\"\$HOME/.mint\"" +readonly shell_mint_link_path_line="export MINT_LINK_PATH=\"\$MINT_PATH/bin\"" + +setup_shell() { + local shell_profile_path=$1 + + if [[ ! -f "${shell_profile_path}" ]]; then + > "${shell_profile_path}" + fi + + if [[ $(grep -L "${shell_mint_path_line}" "${shell_profile_path}") ]]; then + echo "${shell_mint_path_line}" >> "${shell_profile_path}" + fi + + if [[ $(grep -L "${shell_mint_link_path_line}" "${shell_profile_path}") ]]; then + echo "${shell_mint_link_path_line}" >> "${shell_profile_path}" + fi +} + +echo "Checking ${mint_style}Mint${default_style} installation:" + +brew_install_if_needed mint "$arguments" +setup_shell "${HOME}/.zshrc" + +if [[ -f "${HOME}/.bash_profile" ]]; then + setup_shell "${HOME}/.bash_profile" +fi + +echo "" diff --git a/Scripts/Bootstrap/mintfile.sh b/Scripts/Bootstrap/mintfile.sh new file mode 100755 index 0000000..b7ad940 --- /dev/null +++ b/Scripts/Bootstrap/mintfile.sh @@ -0,0 +1,16 @@ +#!/bin/sh + +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" +source "${helpers_path}/script-run.sh" + +echo "Installing ${swift_style}Swift tools${default_style} specified in Mintfile..." + +if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +assert_failure '(cd "${root_path}" && mint bootstrap)' + +echo "" diff --git a/Scripts/Bootstrap/rbenv.sh b/Scripts/Bootstrap/rbenv.sh new file mode 100755 index 0000000..587f85e --- /dev/null +++ b/Scripts/Bootstrap/rbenv.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +readonly arguments=$@ +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +readonly shell_init_line="if which rbenv > /dev/null; then eval \"\$(rbenv init -)\"; fi" +readonly doctor_url='https://github.com/rbenv/rbenv-installer/raw/main/bin/rbenv-doctor' +readonly doctor_temp_path="${script_path}/rbenv_doctor" + +setup_shell() { + local shell_profile_path=$1 + + if [[ ! -f "${shell_profile_path}" ]]; then + > "${shell_profile_path}" + fi + + if [[ $(grep -L "${shell_init_line}" "${shell_profile_path}") ]]; then + echo "${shell_init_line}" >> "${shell_profile_path}" + fi +} + +cleanup() { + rm -rf $doctor_temp_path; +} + +trap cleanup EXIT + +echo "Checking ${rbenv_style}rbenv${default_style} installation:" + +brew_install_if_needed rbenv +setup_shell "${HOME}/.zshrc" + +if [[ -f "${HOME}/.bash_profile" ]]; then + setup_shell "${HOME}/.bash_profile" +fi + +eval "$(rbenv init -)" + +if [[ " ${arguments[*]} " == *" ${verify_flag} "* ]]; then + echo "" + echo " Verifying that rbenv is properly set up..." + curl -fsSL "${doctor_url}" > "${doctor_temp_path}" 2> /dev/null + rbenv_doctor_exit_code=$? + + if [ "${rbenv_doctor_exit_code}" -ne 0 ]; then + echo " Failed to load rbenv-doctor script." + warning 'curl -fsSL "${doctor_url}"' "${rbenv_doctor_exit_code}" + else + chmod a+x "${doctor_temp_path}" + assert_warning 'bash "${doctor_temp_path}"' + fi +fi + +echo "" diff --git a/Scripts/Bootstrap/ruby.sh b/Scripts/Bootstrap/ruby.sh new file mode 100755 index 0000000..49707a4 --- /dev/null +++ b/Scripts/Bootstrap/ruby.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +echo "Checking ${ruby_style}Ruby${default_style} version:" + +if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +eval "$(rbenv init -)" + +readonly ruby_required_version=$(cat "${root_path}"/.ruby-version) +readonly ruby_versions=($(rbenv versions 2>&1)) +readonly ruby_install_flags="-Wno-error=implicit-function-declaration" + +if [[ " ${ruby_versions[@]} " =~ " ${ruby_required_version} " ]]; then + echo " Required Ruby version ($ruby_required_version) already installed." +else + echo " Required Ruby version ($ruby_required_version) not found. Installing..." + assert_failure '(cd "${root_path}" && RUBY_CFLAGS="${ruby_install_flags}" rbenv install $ruby_required_version)' + assert_warning '(cd "${root_path}" && rbenv rehash)' +fi + +echo "" diff --git a/Scripts/Bootstrap/spm.sh b/Scripts/Bootstrap/spm.sh new file mode 100755 index 0000000..3bbfb69 --- /dev/null +++ b/Scripts/Bootstrap/spm.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +readonly arguments=$@ +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +if [[ " ${arguments[*]} " == *" ${update_flag} "* ]]; then + echo "Updating ${spm_style}Swift packages${default_style} specified in Package.swift..." + assert_failure '(cd "${root_path}" && swift package update)' +else + echo "Resolving ${spm_style}Swift packages${default_style} specified in Package.swift..." + assert_failure '(cd "${root_path}" && swift package resolve)' +fi + +echo "" diff --git a/Scripts/Bootstrap/welcome.sh b/Scripts/Bootstrap/welcome.sh new file mode 100755 index 0000000..155fb36 --- /dev/null +++ b/Scripts/Bootstrap/welcome.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +echo "${default_style}" +echo "This script will set up your development environment." +echo "This might take a few minutes. Please don't interrupt the script." +echo "" \ No newline at end of file diff --git a/Scripts/Bootstrap/xcode.sh b/Scripts/Bootstrap/xcode.sh new file mode 100755 index 0000000..c851f64 --- /dev/null +++ b/Scripts/Bootstrap/xcode.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +set -e + +while [[ "$#" -gt 0 ]]; do + case $1 in + --sudo-password) sudo_password="${2}"; shift ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +if [ -n "${sudo_password}" ]; then + echo "${sudo_password}" | sudo -S -E "$0" "$@" + exit $? +fi + +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" + +source "${script_path}/common.sh" + +echo "Checking ${xcode_style}Xcode${default_style} installation:" + +if [[ "$(uname -m)" == "arm64" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +eval "$(rbenv init -)" + +readonly xcode_required_version=$(cat "${root_path}"/.xcode-version) +readonly xcode_version=($(bundle exec xcversion selected 2> /dev/null | sed -n 's/Xcode \(.*\)/\1/p')) + +if [[ "$xcode_version" == "$xcode_required_version" ]]; then + echo " Required Xcode version ($xcode_required_version) already installed." +else + echo " Required Xcode version ($xcode_required_version) not found. Installing..." + + bundle exec xcversion update + bundle exec xcversion install ${xcode_required_version} + + echo " Selecting Xcode version..." + + bundle exec xcversion select "${xcode_required_version}" + sudo xcodebuild -license accept +fi + +echo "" diff --git a/Scripts/Helpers/script-paths.sh b/Scripts/Helpers/script-paths.sh new file mode 100755 index 0000000..353b3ca --- /dev/null +++ b/Scripts/Helpers/script-paths.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +if [ -z "${helpers_path}" ]; then + readonly helpers_path="$( cd "$( dirname "$0" )" && pwd )" +fi + +readonly tools_path="$( cd "${helpers_path}/../" && pwd )" +readonly root_path="$( cd "${tools_path}/../" && pwd )" diff --git a/Scripts/Helpers/script-run.sh b/Scripts/Helpers/script-run.sh new file mode 100755 index 0000000..bf1aa4b --- /dev/null +++ b/Scripts/Helpers/script-run.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +export MINT_PATH="$HOME/.mint" +export MINT_LINK_PATH="$MINT_PATH/bin" + +if [[ -f "/opt/homebrew/bin/brew" ]]; then + eval "$(/opt/homebrew/bin/brew shellenv)" +fi + +run() { + if [ -z "${root_path}" ]; then + if [ -z "${helpers_path}" ]; then + readonly helpers_path="$( cd "$( dirname "$0" )" && pwd )" + fi + + source "${helpers_path}/script-paths.sh" + fi + + if which mint >/dev/null; then + (cd "${root_path}" && mint run "$@") + else + echo "warning: Mint does not exist" + fi +} diff --git a/Scripts/bootstrap.sh b/Scripts/bootstrap.sh index 649c685..0ed560a 100755 --- a/Scripts/bootstrap.sh +++ b/Scripts/bootstrap.sh @@ -1,272 +1,25 @@ #!/bin/sh -readonly arguments=$@ +set -e -readonly update_gems_flag='--update-gems' -readonly update_pods_flag='--update-pods' -readonly update_packages_flag='--update-packages' +readonly script_path="$( cd "$( dirname "$0" )" && pwd )" +readonly bootstrap_path="${script_path}/Bootstrap" -readonly project_path="$( pwd )" +"${bootstrap_path}/welcome.sh" +"${bootstrap_path}/macos.sh" -readonly required_macos_version='10.14.0' -readonly required_ruby_version=$(cat .ruby-version) +"${bootstrap_path}/homebrew.sh" --update --verify +"${bootstrap_path}/rbenv.sh" --update --verify +"${bootstrap_path}/ruby.sh" -readonly homebrew_url='https://raw.githubusercontent.com/Homebrew/install/master/install' -readonly rbenv_doctor_url='https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor' -readonly rbenv_doctor_temp_path="${project_path}/rbenv_doctor" -readonly rbenv_shell_init_line='eval "$(rbenv init -)"' +"${bootstrap_path}/bundler.sh" --update +"${bootstrap_path}/gemfile.sh" -readonly default_style='\033[0m' -readonly warning_style='\033[33m' -readonly error_style='\033[31m' +"${bootstrap_path}/xcode.sh" -readonly project_style='\033[38;5;196m' -readonly macos_style='\033[38;5;99m' -readonly xcode_style='\033[38;5;75m' -readonly homebrew_style='\033[38;5;208m' -readonly rbenv_style='\033[38;5;43m' -readonly ruby_style='\033[38;5;89m' -readonly bundler_style='\033[38;5;45m' -readonly cocoapods_style='\033[38;5;161m' -readonly spm_style='\033[38;5;202m' -readonly congratulations_style='\033[38;5;48m' +"${bootstrap_path}/spm.sh" -cleanup() { - rm -rf $rbenv_doctor_temp_path; -} +"${bootstrap_path}/mint.sh" --update +"${bootstrap_path}/mintfile.sh" -failure() { - echo "${error_style}Fatal error:${default_style} '$1' failed with exit code $2" - exit 1 -} - -warning() { - echo "${warning_style}Warning:${default_style} '$1' failed with exit code $2" -} - -assert_failure() { - eval $1 2>&1 | sed -e "s/^/ /" - - local exit_code=${PIPESTATUS[0]} - - if [ $exit_code -ne 0 ]; then - failure "$1" $exit_code - fi -} - -assert_warning() { - eval $1 2>&1 | sed -e "s/^/ /" - - local exit_code=${PIPESTATUS[0]} - - if [ $exit_code -ne 0 ]; then - warning "$1" $exit_code - fi -} - -plain_version() { - echo "$@" | awk -F. '{ printf("%d%03d%03d%03d", $1,$2,$3,$4); }' -} - -######################################################################################## - -welcome_message_step() { - echo "${default_style}" - echo "------------------------------------------------------------------" - echo "--- ---" - echo "--- Welcome to ${project_style}HeadHunter${default_style}"'!'" ---" - echo "--- ---" - echo "------------------------------------------------------------------" - echo "" - echo "This script will set up your development environment." - echo "This might take a few minutes. Please don't interrupt the script." -} - -macos_version_step() { - echo "" - echo "Checking ${macos_style}macOS${default_style} version:" - - macos_version=$(/usr/bin/sw_vers -productVersion 2>&1) - - if [ "$(plain_version $macos_version)" -lt "$(plain_version $required_macos_version)" ]; then - echo " ${error_style}Your macOS version ($macos_version) is older then required version ($required_macos_version). Exiting...${default_style}" - exit 1 - else - echo " Your macOS version: $macos_version" - fi -} - -xcode_command_line_tools_step() { - echo "" - echo "Checking ${xcode_style}Xcode Command Line Tools${default_style} installation:" - - if xcode-select --version &> /dev/null; then - echo " Xcode Command Line Tools already installed." - else - echo " Xcode Command Line Tools not found. Installing..." - assert_failure 'xcode-select --install' - fi -} - -homebrew_step() { - echo "" - echo "Checking ${homebrew_style}Homebrew${default_style} installation:" - - if which -s brew; then - echo " Homebrew already installed. Updating..." - assert_failure 'brew update' - else - echo " Homebrew not found. Installing..." - assert_failure 'ruby -e "$(curl -fsSL $homebrew_url)" < /dev/null' - fi - - echo "" - echo " Verifying that Homebrew is properly set up..." - assert_warning 'brew doctor' -} - -rbenv_shell_step() { - shell_profile_path=$1 - - if [[ -f $shell_profile_path ]]; then - shell_profile_content=$(grep rbenv $shell_profile_path 2> /dev/null) - - if [[ $shell_profile_content != *"$rbenv_shell_init_line"* ]]; then - echo $rbenv_shell_init_line >> $shell_profile_path - fi - fi -} - -rbenv_step() { - echo "" - echo "Checking ${rbenv_style}rbenv${default_style} installation:" - - if brew ls --versions rbenv &> /dev/null; then - echo " rbenv already installed. Updating..." - brew_outdated=$(brew outdated 2> /dev/null) - brew_outdated_exit_code=$? - - if [ $brew_outdated_exit_code -ne 0 ]; then - echo " Failed to find outdated formulae." - warning 'brew outdated' $brew_outdated_exit_code - else - if [[ $brew_outdated == *"rbenv"* ]]; then - assert_failure 'brew upgrade rbenv' - else - echo " Already up-to-date." - fi - fi - else - echo " rbenv not found. Installing..." - assert_failure 'brew install rbenv' - fi - - assert_warning 'rbenv rehash' - - rbenv_shell_step "${HOME}/.bash_profile" - rbenv_shell_step "${HOME}/.zshrc" - - eval "$(rbenv init -)" - - echo "" - echo " Verifying that rbenv is properly set up..." - curl -fsSL $rbenv_doctor_url > $rbenv_doctor_temp_path 2> /dev/null - rbenv_doctor_exit_code=$? - - if [ $rbenv_doctor_exit_code -ne 0 ]; then - echo " Failed to load rbenv-doctor script." - warning 'curl -fsSL $rbenv_doctor_url' $rbenv_doctor_exit_code - else - chmod a+x $rbenv_doctor_temp_path - assert_warning 'bash $rbenv_doctor_temp_path' - fi -} - -ruby_step() { - echo "" - echo "Checking ${ruby_style}Ruby${default_style} version:" - - ruby_versions=($(rbenv versions 2>&1)) - - if [[ " ${ruby_versions[@]} " =~ " ${required_ruby_version} " ]]; then - echo " Required Ruby version ($required_ruby_version) already installed." - else - echo " Required Ruby version ($required_ruby_version) not found. Installing..." - assert_failure 'rbenv install $required_ruby_version' - assert_warning 'rbenv rehash' - fi -} - -bundler_step() { - echo "" - echo "Checking ${bundler_style}Bundler${default_style} installation:" - - if rbenv which bundler &> /dev/null; then - echo " Bundler already installed. Updating..." - assert_failure 'gem update bundler' - else - echo " Bundler not found. Installing..." - assert_failure 'gem install bundler' - fi - - assert_warning 'rbenv rehash' -} - -gemfile_step() { - echo "" - - if [[ " ${arguments[*]} " == *" $update_gems_flag "* ]]; then - echo "Updating ${ruby_style}Ruby gems${default_style} specified in Gemfile..." - assert_failure 'bundle update' - else - echo "Installing ${ruby_style}Ruby gems${default_style} specified in Gemfile..." - assert_failure 'bundle install' - fi -} - -podfile_step() { - echo "" - - if [[ " ${arguments[*]} " == *" $update_pods_flag "* ]]; then - echo "Updating ${cocoapods_style}Cocoapods${default_style} specified in Podfile..." - assert_failure 'bundle exec pod update' - else - echo "Installing ${cocoapods_style}Cocoapods${default_style} specified in Podfile..." - assert_failure 'bundle exec pod install' - fi -} - -spm_step() { - echo "" - - if [[ " ${arguments[*]} " == *" $update_packages_flag "* ]]; then - echo "Updating ${spm_style}Swift packages${default_style} specified in Package.swift..." - assert_failure 'swift package update' - else - echo "Resolving ${spm_style}Swift packages${default_style} specified in Package.swift..." - assert_failure 'swift package resolve' - fi -} - -congratulations_step() { - echo "" - echo "" - echo "${congratulations_style}Congratulations!${default_style} Setting up the development environment successfully completed 🥳" - echo "" -} - -######################################################################################## - -trap cleanup EXIT - -welcome_message_step -macos_version_step -xcode_command_line_tools_step -homebrew_step -rbenv_step -ruby_step -bundler_step -gemfile_step -podfile_step -spm_step -congratulations_step +"${bootstrap_path}/congratulations.sh" diff --git a/Scripts/swiftlint.sh b/Scripts/swiftlint.sh new file mode 100755 index 0000000..57a6c56 --- /dev/null +++ b/Scripts/swiftlint.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +if [[ "${SKIP_SWIFTLINT}" == "YES" ]]; then + exit 0 +fi + +readonly helpers_path="$( cd "$( dirname "$0" )" && pwd )/Helpers" + +source "${helpers_path}/script-run.sh" +run swiftlint --quiet || true diff --git a/Sources/Commands/Colors/ColorsCommand.swift b/Sources/Commands/Colors/ColorsCommand.swift deleted file mode 100644 index 1e8253f..0000000 --- a/Sources/Commands/Colors/ColorsCommand.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import SwiftCLI -import PromiseKit - -final class ColorsCommand: Command { - - // MARK: - Instance Properties - - let name = "colors" - let shortDescription = "Generates code for colors from a Figma file." - - let fileKey = Key( - "--fileKey", - description: """ - Figma file key to generate colors from. - """ - ) - - let accessToken = Key( - "--accessToken", - description: """ - A personal access token to make requests to the Figma API. - Get more info: https://www.figma.com/developers/api#access-tokens - """ - ) - - let includingNodeIDs = Key( - "--including", - description: """ - Comma separated list of nodes whose styles will be extracted. - If omitted or empty, all nodes will be included. - """ - ) - - let excludingNodeIDs = Key( - "--excluding", - description: """ - Comma separated list of nodes whose styles will be ignored. - """ - ) - - let templatePath = Key( - "--templatePath", - "-t", - description: """ - Path to the template file. - If no template is passed a default template will be used. - """ - ) - - let destinationPath = Key( - "--destinationPath", - "-d", - description: """ - The path to the file to generate. - Defaults to '\(ColorsGenerator.defaultDestinationPath)'. - """ - ) - - private let services: ColorsServices - - // MARK: - Initializers - - init(services: ColorsServices) { - self.services = services - } - - // MARK: - Instance Methods - - func execute() throws { - let includingNodeIDs = self.includingNodeIDs.value?.components(separatedBy: ",") ?? [] - let excludingNodeIDs = self.excludingNodeIDs.value?.components(separatedBy: ",") ?? [] - - let configuration = StepConfiguration( - fileKey: fileKey.value, - accessToken: accessToken.value, - includingNodes: includingNodeIDs, - excludingNodes: excludingNodeIDs, - templatePath: templatePath.value, - destinationPath: destinationPath.value - ) - - let generator = ColorsGenerator(services: services) - - firstly { - generator.generateColors(configuration: configuration) - }.done { - self.success(message: "Color generation completed successfully!") - }.catch { error in - self.fail(error: error) - } - - RunLoop.main.run() - } -} diff --git a/Sources/Commands/Colors/ColorsGenerator.swift b/Sources/Commands/Colors/ColorsGenerator.swift deleted file mode 100644 index 6a86eeb..0000000 --- a/Sources/Commands/Colors/ColorsGenerator.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -final class ColorsGenerator { - - // MARK: - Type Properties - - static let defaultDestinationPath = "Generated/Colors.swift" - static let defaultTemplateName = "Colors.stencil" - - // MARK: - Instance Properties - - private let services: ColorsServices - - // MARK: - Initializers - - init(services: ColorsServices) { - self.services = services - } - - // MARK: - Instance Methods - - func generateColors(configuration: StepConfiguration) -> Promise { - guard let fileKey = configuration.fileKey, !fileKey.isEmpty else { - return Promise(error: ConfigurationError.invalidFileKey) - } - - guard let accessToken = configuration.accessToken, !accessToken.isEmpty else { - return Promise(error: ConfigurationError.invalidAccessToken) - } - - let templateType = resolveTemplateType(configuration: configuration) - let destinationPath = resolveDestinationPath(configuration: configuration) - - let colorsProvider = services.makeColorsProvider(accessToken: accessToken) - let colorsRenderer = services.makeColorsRenderer() - - return firstly { - colorsProvider.fetchColors( - fileKey: fileKey, - includingNodes: configuration.includingNodes, - excludingNodes: configuration.excludingNodes - ) - }.map { colors in - try colorsRenderer.renderTemplate( - templateType, - to: destinationPath, - colors: colors - ) - } - } - - private func resolveTemplateType(configuration: StepConfiguration) -> TemplateType { - if let templatePath = configuration.templatePath { - return .custom(path: templatePath) - } else { - return .native(name: Self.defaultTemplateName) - } - } - - private func resolveDestinationPath(configuration: StepConfiguration) -> String { - return configuration.destinationPath ?? Self.defaultDestinationPath - } -} diff --git a/Sources/Commands/Colors/ColorsServices.swift b/Sources/Commands/Colors/ColorsServices.swift deleted file mode 100644 index e4c6e7b..0000000 --- a/Sources/Commands/Colors/ColorsServices.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -protocol ColorsServices { - - // MARK: - Instance Methods - - func makeColorsProvider(accessToken: String) -> ColorsProvider - func makeColorsRenderer() -> ColorsRenderer -} diff --git a/Sources/Commands/Generate/GenerateCommand.swift b/Sources/Commands/Generate/GenerateCommand.swift deleted file mode 100644 index 9efbdbd..0000000 --- a/Sources/Commands/Generate/GenerateCommand.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import SwiftCLI -import PathKit -import Yams -import PromiseKit - -final class GenerateCommand: Command { - - // MARK: - Nested Types - - private enum Constants { - static let defaultConfigurationPath = ".figmagen.yml" - } - - // MARK: - Instance Properties - - let name = "generate" - let shortDescription = "Generates code from Figma files using a configuration file." - - let configurationPath = Key( - "--config", - description: """ - Path to the configuration file. - Defaults to '\(Constants.defaultConfigurationPath)'. - """ - ) - - private let services: GenerateServices - - // MARK: - Initializers - - init(services: GenerateServices) { - self.services = services - } - - // MARK: - Instance Methods - - func execute() throws { - let configurationPath = Path(self.configurationPath.value ?? Constants.defaultConfigurationPath) - let configuration = try YAMLDecoder().decode(Configuration.self, from: configurationPath.read()) - - let promises = [ - generateColorsIfNeeded(configuration: configuration), - generateTextStylesIfNeeded(configuration: configuration), - generateSpacingsIfNeeded(configuration: configuration) - ] - - firstly { - when(fulfilled: promises) - }.done { - self.success(message: "Generation completed successfully!") - }.catch { error in - self.fail(error: error) - } - - RunLoop.main.run() - } - - private func generateColorsIfNeeded(configuration: Configuration) -> Promise { - guard let colorsConfiguration = configuration.resolveColorsConfiguration() else { - return .value(Void()) - } - - return ColorsGenerator(services: services).generateColors(configuration: colorsConfiguration) - } - - private func generateTextStylesIfNeeded(configuration: Configuration) -> Promise { - guard let textStylesConfiguration = configuration.resolveTextStylesConfiguration() else { - return .value(Void()) - } - - return TextStylesGenerator(services: services).generateTextStyles(configuration: textStylesConfiguration) - } - - private func generateSpacingsIfNeeded(configuration: Configuration) -> Promise { - guard let spacingsConfiguration = configuration.resolveSpacingsConfiguration() else { - return .value(Void()) - } - - return SpacingsGenerator(services: services).generateSpacings(configuration: spacingsConfiguration) - } -} diff --git a/Sources/Commands/Generate/GenerateServices.swift b/Sources/Commands/Generate/GenerateServices.swift deleted file mode 100644 index d9efdac..0000000 --- a/Sources/Commands/Generate/GenerateServices.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -typealias GenerateServices = ColorsServices & TextStylesServices & SpacingsServices diff --git a/Sources/Commands/Spacings/SpacingsCommand.swift b/Sources/Commands/Spacings/SpacingsCommand.swift deleted file mode 100644 index f50a7a2..0000000 --- a/Sources/Commands/Spacings/SpacingsCommand.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation -import SwiftCLI -import PromiseKit - -final class SpacingsCommand: Command { - - // MARK: - Instance Properties - - let name = "spacings" - let shortDescription = "Generates code for spacings from a Figma file." - - let fileKey = Key( - "--fileKey", - description: """ - Figma file key to generate spacings from. - """ - ) - - let accessToken = Key( - "--accessToken", - description: """ - A personal access token to make requests to the Figma API. - Get more info: https://www.figma.com/developers/api#access-tokens - """ - ) - - let includingNodeIDs = Key( - "--including", - description: """ - Comma separated list of nodes whose styles will be extracted. - If omitted or empty, all nodes will be included. - """ - ) - - let excludingNodeIDs = Key( - "--excluding", - description: """ - Comma separated list of nodes whose styles will be ignored. - """ - ) - - let templatePath = Key( - "--templatePath", - "-t", - description: """ - Path to the template file. - If no template is passed a default template will be used. - """ - ) - - let destinationPath = Key( - "--destinationPath", - "-d", - description: """ - The path to the file to generate. - Defaults to '\(SpacingsGenerator.defaultDestinationPath)'. - """ - ) - - private let services: SpacingsServices - - // MARK: - Initializers - - init(services: SpacingsServices) { - self.services = services - } - - // MARK: - Instance Methods - - func execute() throws { - let includingNodeIDs = self.includingNodeIDs.value?.components(separatedBy: ",") ?? [] - let excludingNodeIDs = self.excludingNodeIDs.value?.components(separatedBy: ",") ?? [] - - let configuration = StepConfiguration( - fileKey: fileKey.value, - accessToken: accessToken.value, - includingNodes: includingNodeIDs, - excludingNodes: excludingNodeIDs, - templatePath: templatePath.value, - destinationPath: destinationPath.value - ) - - let generator = SpacingsGenerator(services: services) - - firstly { - generator.generateSpacings(configuration: configuration) - }.done { - self.success(message: "Spacings generation completed successfully!") - }.catch { error in - self.fail(error: error) - } - - RunLoop.main.run() - } -} diff --git a/Sources/Commands/Spacings/SpacingsGenerator.swift b/Sources/Commands/Spacings/SpacingsGenerator.swift deleted file mode 100644 index 3e9361d..0000000 --- a/Sources/Commands/Spacings/SpacingsGenerator.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -final class SpacingsGenerator { - - static let defaultDestinationPath = "Generated/Spacings.swift" - static let defaultTemplateName = "Spacings.stencil" - - // MARK: - Instance Properties - - private let services: SpacingsServices - - // MARK: - Initializers - - init(services: SpacingsServices) { - self.services = services - } - - // MARK: - Instance Methods - - func generateSpacings(configuration: StepConfiguration) -> Promise { - guard let fileKey = configuration.fileKey, !fileKey.isEmpty else { - return Promise(error: ConfigurationError.invalidFileKey) - } - - guard let accessToken = configuration.accessToken, !accessToken.isEmpty else { - return Promise(error: ConfigurationError.invalidAccessToken) - } - - let templateType = resolveTemplateType(configuration: configuration) - let destinationPath = resolveDestinationPath(configuration: configuration) - - let spacingsProvider = services.makeSpacingsProvider(accessToken: accessToken) - let spacingsRenderer = services.makeSpacingsRenderer() - - return firstly { - spacingsProvider.fetchSpacings( - fileKey: fileKey, - includingNodes: configuration.includingNodes, - excludingNodes: configuration.excludingNodes - ) - }.map { spacings in - try spacingsRenderer.renderTemplate( - templateType, - to: destinationPath, - spacings: spacings - ) - } - } - - private func resolveTemplateType(configuration: StepConfiguration) -> TemplateType { - if let templatePath = configuration.templatePath { - return .custom(path: templatePath) - } else { - return .native(name: Self.defaultTemplateName) - } - } - - private func resolveDestinationPath(configuration: StepConfiguration) -> String { - return configuration.destinationPath ?? Self.defaultDestinationPath - } -} diff --git a/Sources/Commands/Spacings/SpacingsServices.swift b/Sources/Commands/Spacings/SpacingsServices.swift deleted file mode 100644 index 6b7d29c..0000000 --- a/Sources/Commands/Spacings/SpacingsServices.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation - -protocol SpacingsServices { - - // MARK: - Instance Methods - - func makeSpacingsProvider(accessToken: String) -> SpacingsProvider - func makeSpacingsRenderer() -> SpacingsRenderer -} diff --git a/Sources/Commands/TextStyles/TextStylesCommand.swift b/Sources/Commands/TextStyles/TextStylesCommand.swift deleted file mode 100644 index 268f07f..0000000 --- a/Sources/Commands/TextStyles/TextStylesCommand.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import SwiftCLI -import PromiseKit - -final class TextStylesCommand: Command { - - // MARK: - Instance Properties - - let name = "textStyles" - let shortDescription = "Generates code for text styles from a Figma file." - - let fileKey = Key( - "--fileKey", - description: """ - Figma file key to generate text styles from. - """ - ) - - let accessToken = Key( - "--accessToken", - description: """ - A personal access token to make requests to the Figma API. - Get more info: https://www.figma.com/developers/api#access-tokens - """ - ) - - let includingNodeIDs = Key( - "--including", - description: """ - Comma separated list of nodes whose styles will be extracted. - If omitted or empty, all nodes will be included. - """ - ) - - let excludingNodeIDs = Key( - "--excluding", - description: """ - Comma separated list of nodes whose styles will be ignored. - """ - ) - - let templatePath = Key( - "--templatePath", - "-t", - description: """ - Path to the template file. - If no template is passed a default template will be used. - """ - ) - - let destinationPath = Key( - "--destinationPath", - "-d", - description: """ - The path to the file to generate. - Defaults to '\(TextStylesGenerator.defaultDestinationPath)'. - """ - ) - - private let services: TextStylesServices - - // MARK: - Initializers - - init(services: TextStylesServices) { - self.services = services - } - - // MARK: - Instance Methods - - func execute() throws { - let includingNodeIDs = self.includingNodeIDs.value?.components(separatedBy: ",") ?? [] - let excludingNodeIDs = self.excludingNodeIDs.value?.components(separatedBy: ",") ?? [] - - let configuration = StepConfiguration( - fileKey: fileKey.value, - accessToken: accessToken.value, - includingNodes: includingNodeIDs, - excludingNodes: excludingNodeIDs, - templatePath: self.templatePath.value, - destinationPath: self.destinationPath.value - ) - - let generator = TextStylesGenerator(services: services) - - firstly { - generator.generateTextStyles(configuration: configuration) - }.done { - self.success(message: "Text styles generation completed successfully!") - }.catch { error in - self.fail(error: error) - } - - RunLoop.main.run() - } -} diff --git a/Sources/Commands/TextStyles/TextStylesGenerator.swift b/Sources/Commands/TextStyles/TextStylesGenerator.swift deleted file mode 100644 index d8c16fa..0000000 --- a/Sources/Commands/TextStyles/TextStylesGenerator.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -final class TextStylesGenerator { - - // MARK: - Type Properties - - static let defaultTemplateName = "TextStyles.stencil" - static let defaultDestinationPath = "Generated/TextStyles.swift" - - // MARK: - Instance Properties - - private let services: TextStylesServices - - // MARK: - Initializers - - init(services: TextStylesServices) { - self.services = services - } - - // MARK: - Instance Methods - - func generateTextStyles(configuration: StepConfiguration) -> Promise { - guard let fileKey = configuration.fileKey, !fileKey.isEmpty else { - return Promise(error: ConfigurationError.invalidFileKey) - } - - guard let accessToken = configuration.accessToken, !accessToken.isEmpty else { - return Promise(error: ConfigurationError.invalidAccessToken) - } - - let templateType = resolveTemplateType(configuration: configuration) - let destinationPath = resolveDestinationPath(configuration: configuration) - - let textStylesProvider = services.makeTextStylesProvider(accessToken: accessToken) - let textStylesRenderer = services.makeTextStylesRenderer() - - return firstly { - textStylesProvider.fetchTextStyles( - fileKey: fileKey, - includingNodes: configuration.includingNodes, - excludingNodes: configuration.excludingNodes - ) - }.map { textStyles in - try textStylesRenderer.renderTemplate( - templateType, - to: destinationPath, - textStyles: textStyles - ) - } - } - - private func resolveTemplateType(configuration: StepConfiguration) -> TemplateType { - if let templatePath = configuration.templatePath { - return .custom(path: templatePath) - } else { - return .native(name: Self.defaultTemplateName) - } - } - - private func resolveDestinationPath(configuration: StepConfiguration) -> String { - return configuration.destinationPath ?? Self.defaultDestinationPath - } -} diff --git a/Sources/Commands/TextStyles/TextStylesServices.swift b/Sources/Commands/TextStyles/TextStylesServices.swift deleted file mode 100644 index ca8098a..0000000 --- a/Sources/Commands/TextStyles/TextStylesServices.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -protocol TextStylesServices { - - // MARK: - Instance Methods - - func makeTextStylesProvider(accessToken: String) -> TextStylesProvider - func makeTextStylesRenderer() -> TextStylesRenderer -} diff --git a/Sources/FigmaGen/Commands/AsyncExecutableCommand.swift b/Sources/FigmaGen/Commands/AsyncExecutableCommand.swift new file mode 100644 index 0000000..d2b183f --- /dev/null +++ b/Sources/FigmaGen/Commands/AsyncExecutableCommand.swift @@ -0,0 +1,38 @@ +import Foundation +import SwiftCLI +import Rainbow + +protocol AsyncExecutableCommand: Command { + + // MARK: - Instance Methods + + func executeAsyncAndExit() async throws + + func fail(message: String) -> Never + func succeed(message: String) -> Never +} + +extension AsyncExecutableCommand { + + // MARK: - Instance Methods + + func execute() throws { + _Concurrency.Task { + try await executeAsyncAndExit() + } + + RunLoop.main.run() + } + + func fail(message: String) -> Never { + stderr <<< message.red + + exit(EXIT_FAILURE) + } + + func succeed(message: String) -> Never { + stdout <<< message.green + + exit(EXIT_SUCCESS) + } +} diff --git a/Sources/FigmaGen/Commands/ColorStylesCommand.swift b/Sources/FigmaGen/Commands/ColorStylesCommand.swift new file mode 100644 index 0000000..73093e8 --- /dev/null +++ b/Sources/FigmaGen/Commands/ColorStylesCommand.swift @@ -0,0 +1,118 @@ +import Foundation +import SwiftCLI +import PromiseKit + +final class ColorStylesCommand: AsyncExecutableCommand, GenerationConfigurableCommand { + + // MARK: - Instance Properties + + let generator: ColorStylesGenerator + + // MARK: - + + let name = "colorStyles" + let shortDescription = "Generates code for color style from a Figma file." + + let fileKey = Key( + "--fileKey", + description: """ + Figma file key to generate color styles from. + """ + ) + + let fileVersion = Key( + "--fileVersion", + description: """ + Figma file version ID to generate color styles from. + """ + ) + + let includedNodes = VariadicKey( + "--includingNodes", + "-i", + description: #""" + A list of Figma nodes whose styles will be extracted. + Can be repeated multiple times and must be in the format: -i "1:23". + If omitted, all nodes will be included. + """# + ) + + let excludedNodes = VariadicKey( + "--excludingNodes", + "-e", + description: #""" + A list of Figma nodes whose styles will be ignored. + Can be repeated multiple times and must be in the format: -e "1:23". + """# + ) + + let accessToken = Key( + "--accessToken", + description: """ + A personal access token to make requests to the Figma API. + Get more info: https://www.figma.com/developers/api#access-tokens + """ + ) + + let assets = Key( + "--assets", + "-a", + description: """ + Optional path to Xcode-assets folder to store colors. + """ + ) + + let template = Key( + "--template", + "-t", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + let templateOptions = VariadicKey( + "--options", + "-o", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + let destination = Key( + "--destination", + "-d", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + + // MARK: - Initializers + + init(generator: ColorStylesGenerator) { + self.generator = generator + } + + // MARK: - Instance Methods + + private func resolveColorStylesConfiguration() -> ColorStylesConfiguration { + ColorStylesConfiguration( + generation: generationConfiguration, + assets: assets.value + ) + } + + // MARK: - + + func executeAsyncAndExit() throws { + firstly { + self.generator.generate(configuration: self.resolveColorStylesConfiguration()) + }.done { + self.succeed(message: "Color styles generated successfully!") + }.catch { error in + self.fail(message: "Failed to generate color styles: \(error)") + } + } +} diff --git a/Sources/FigmaGen/Commands/GenerateCommand.swift b/Sources/FigmaGen/Commands/GenerateCommand.swift new file mode 100644 index 0000000..f2aa69b --- /dev/null +++ b/Sources/FigmaGen/Commands/GenerateCommand.swift @@ -0,0 +1,61 @@ +import Foundation +import SwiftCLI +import PromiseKit + +final class GenerateCommand: AsyncExecutableCommand { + + // MARK: - Instance Properties + + let name = "generate" + let shortDescription = "Generates code from Figma files using a configuration file." + + let configurationPath = Key( + "--config", + "-c", + description: """ + Path to the configuration file. + Defaults to '\(String.defaultConfigurationPath)'. + """ + ) + + let verbose = Flag( + "--verbose", + description: """ + Enable verbose logging for debugging. + By default is disabled. + """ + ) + + let generator: LibraryGenerator + + // MARK: - Initializers + + init(generator: LibraryGenerator) { + self.generator = generator + } + + // MARK: - Instance Methods + + func executeAsyncAndExit() throws { + if verbose.value { + setenv(Logger.verboseLogKey, "YES", 1) + } + + let configurationPath = self.configurationPath.value ?? .defaultConfigurationPath + + firstly { + self.generator.generate(configurationPath: configurationPath) + }.done { + self.succeed(message: "Generation completed successfully!") + }.catch { error in + self.fail(message: "Failed to generate with error: \(error)") + } + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let defaultConfigurationPath = ".figmagen.yml" +} diff --git a/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift b/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift new file mode 100644 index 0000000..e4f3930 --- /dev/null +++ b/Sources/FigmaGen/Commands/GenerationConfigurableCommand.swift @@ -0,0 +1,85 @@ +import Foundation +import SwiftCLI + +protocol GenerationConfigurableCommand: Command { + + // MARK: - Instance Properties + + var fileKey: Key { get } + var fileVersion: Key { get } + var includedNodes: VariadicKey { get } + var excludedNodes: VariadicKey { get } + + var accessToken: Key { get } + + var template: Key { get } + var templateOptions: VariadicKey { get } + var destination: Key { get } + + var generationConfiguration: GenerationConfiguration { get } +} + +extension GenerationConfigurableCommand { + + // MARK: - Instance Properties + + // !!! Important note !!! + // For CLI usage of FigmaGen we don't support multiple templates for any token type. + var generationConfiguration: GenerationConfiguration { + GenerationConfiguration( + file: resolveFileConfiguration(), + accessToken: resolveAccessTokenConfiguration(), + templates: [ + TemplateConfiguration( + template: template.value, + templateOptions: resolveTemplateOptions(), + destination: destination.value + ) + ] + ) + } + + // MARK: - Instance Methods + + private func resolveFileConfiguration() -> FileConfiguration? { + guard let fileKey = fileKey.value else { + return nil + } + + return FileConfiguration( + key: fileKey, + version: fileVersion.value, + includedNodes: includedNodes.value, + excludedNodes: excludedNodes.value + ) + } + + private func resolveAccessTokenConfiguration() -> AccessTokenConfiguration? { + guard let accessToken = accessToken.value else { + return nil + } + + return AccessTokenConfiguration(value: accessToken) + } + + private func resolveTemplateOptions() -> [String: Any] { + var templateOptions: [String: String] = [:] + + for templateOption in self.templateOptions.value { + var optionComponents = templateOption.components(separatedBy: String.templateOptionSeparator) + let optionKey = optionComponents.removeFirst().trimmingCharacters(in: .whitespaces) + let optionValue = optionComponents.joined(separator: .templateOptionSeparator) + + templateOptions[optionKey] = optionValue + } + + return templateOptions + } +} + +extension String { + + // MARK: - Type Properties + + static let templateOptionSeparator = ":" +} diff --git a/Sources/FigmaGen/Commands/ImagesCommand.swift b/Sources/FigmaGen/Commands/ImagesCommand.swift new file mode 100644 index 0000000..8c28d00 --- /dev/null +++ b/Sources/FigmaGen/Commands/ImagesCommand.swift @@ -0,0 +1,285 @@ +import Foundation +import SwiftCLI +import PromiseKit + +final class ImagesCommand: AsyncExecutableCommand, GenerationConfigurableCommand { + + // MARK: - Instance Properties + + let generator: ImagesGenerator + + // MARK: - + + let name = "images" + let shortDescription = "Generates code for images from a Figma file." + + let fileKey = Key( + "--fileKey", + description: """ + Figma file key to generate images from. + """ + ) + + let fileVersion = Key( + "--fileVersion", + description: """ + Figma file version ID to generate images from. + """ + ) + + let includedNodes = VariadicKey( + "--includingNodes", + "-i", + description: #""" + A list of Figma nodes whose components will be rendered. + Can be repeated multiple times and must be in the format: -i "1:23". + If omitted, all nodes will be included. + """# + ) + + let excludedNodes = VariadicKey( + "--excludingNodes", + "-e", + description: #""" + A list of Figma nodes whose components will be ignored. + Can be repeated multiple times and must be in the format: -e "1:23". + """# + ) + + let accessToken = Key( + "--accessToken", + description: """ + A personal access token to make requests to the Figma API. + Get more info: https://www.figma.com/developers/api#access-tokens + """ + ) + + let assets = Key( + "--assets", + "-a", + description: """ + Optional path to Xcode-assets folder to store images. + """ + ) + + let resources = Key( + "--resources", + "-r", + description: """ + Optional path to folder to store images. + """ + ) + + let postProcessor = Key( + "--postProcessor", + "-p", + description: """ + The path to the bash script to make operations with generated images. + Only executes for generated images from --resources folder. + """ + ) + + let format = Key( + "--format", + "-f", + description: """ + Optional image output format, can be 'pdf', 'png', 'jpg' or 'svg'. + Defaults to 'pdf'. + """ + ) + + let scales = Key( + "-scales", + "-s", + description: #""" + A comma separated list of integer image scaling factors. + Each scaling factor should be between 1 and 3: -s "1,2,3". + If omitted, images will be rendered with the original sizes. + """# + ) + + let template = Key( + "--template", + "-t", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + let templateOptions = VariadicKey( + "--options", + "-o", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + let destination = Key( + "--destination", + "-d", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + + let onlyExportables = Flag( + "--onlyExportables", + description: """ + Render only exportable components. + By default, all components will be rendered. + """ + ) + + let useAbsoluteBounds = Flag( + "--useAbsoluteBounds", + description: """ + Use full dimensions of the node. + By default, images will omit empty space or crop. + """ + ) + + let preserveVectorData = Flag( + "--preserveVectorData", + description: """ + Set preserve vector data flag in Xcode assets. + By default, Xcode assets will be generated without vector data preserving. + """ + ) + + let renderAs = Key( + "--renderAs", + description: """ + Set rendering mode in Xcode assets, can be 'original' or 'template'. + By default, Xcode assets will be generated with default rendering mode. + """ + ) + + let groupByFrame = Flag( + "--groupByFrame", + description: """ + Group generated assets and resources into folders with name of parent frame. + By default without grouping. + """ + ) + + let groupByComponentSet = Flag( + "--groupByComponentSet", + description: """ + Group generated assets and resources into folders with name of component set. + Only for components with variants. + By default without grouping. + """ + ) + + 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) { + self.generator = generator + } + + // MARK: - Instance Methods + + private func resolveImageFormat() -> ImageFormat { + switch format.value { + case nil: + return .pdf + + case let rawFormat?: + guard let format = ImageFormat(rawValue: rawFormat) else { + fail(message: "Failed to generate images: Invalid format (\(rawFormat))") + } + + return format + } + } + + private func resolveImageScales() -> [ImageScale] { + scales + .value? + .components(separatedBy: String.scaleSeparator) + .map { rawScale in + guard let scale = ImageScale(rawValue: rawScale) else { + fail(message: "Failed to generate images: Invalid scaling factor (\(rawScale))") + } + + return scale + } ?? [.none] + } + + private func resolveRenderingMode() -> ImageRenderingMode? { + switch renderAs.value { + case nil: + return nil + + case let rawRenderingMode?: + guard let mode = ImageRenderingMode(rawValue: rawRenderingMode) else { + fail(message: "Failed to generated images: Invalid rendering mode (\(rawRenderingMode)") + } + + return mode + } + } + + 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, + assets: assets.value, + resources: resources.value, + postProcessor: postProcessor.value, + format: resolveImageFormat(), + scales: resolveImageScales(), + onlyExportables: onlyExportables.value, + useAbsoluteBounds: useAbsoluteBounds.value, + preserveVectorData: preserveVectorData.value, + renderAs: resolveRenderingMode(), + groupByFrame: groupByFrame.value, + groupByComponentSet: groupByComponentSet.value, + namingStyle: resolveNamingStyle() + ) + } + + // MARK: - + + func executeAsyncAndExit() throws { + firstly { + self.generator.generate(configuration: self.resolveImagesConfiguration()) + }.done { + self.succeed(message: "Images generated successfully!") + }.catch { error in + self.fail(message: "Failed to generate images: \(error)") + } + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let scaleSeparator = "," +} diff --git a/Sources/FigmaGen/Commands/ShadowStylesCommand.swift b/Sources/FigmaGen/Commands/ShadowStylesCommand.swift new file mode 100644 index 0000000..e066693 --- /dev/null +++ b/Sources/FigmaGen/Commands/ShadowStylesCommand.swift @@ -0,0 +1,101 @@ +import Foundation +import SwiftCLI +import PromiseKit + +final class ShadowStylesCommand: AsyncExecutableCommand, GenerationConfigurableCommand { + + // MARK: - Instance Properties + + let generator: ShadowStylesGenerator + + // MARK: - + + let name = "shadowStyles" + let shortDescription = "Generates code for shadow styles from a Figma file." + + let fileKey = Key( + "--fileKey", + description: """ + Figma file key to generate shadow styles from. + """ + ) + + let fileVersion = Key( + "--fileVersion", + description: """ + Figma file version ID to generate shadow styles from. + """ + ) + + let includedNodes = VariadicKey( + "--includingNodes", + "-i", + description: #""" + A list of Figma nodes whose styles will be extracted. + Can be repeated multiple times and must be in the format: -i "1:23". + If omitted, all nodes will be included. + """# + ) + + let excludedNodes = VariadicKey( + "--excludingNodes", + "-e", + description: #""" + A list of Figma nodes whose styles will be ignored. + Can be repeated multiple times and must be in the format: -e "1:23". + """# + ) + + let accessToken = Key( + "--accessToken", + description: """ + A personal access token to make requests to the Figma API. + Get more info: https://www.figma.com/developers/api#access-tokens + """ + ) + + let template = Key( + "--template", + "-t", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + let templateOptions = VariadicKey( + "--options", + "-o", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + let destination = Key( + "--destination", + "-d", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + + // MARK: - Initializers + + init(generator: ShadowStylesGenerator) { + self.generator = generator + } + + // MARK: - Instance Methods + + func executeAsyncAndExit() throws { + firstly { + self.generator.generate(configuration: self.generationConfiguration) + }.done { + self.succeed(message: "Shadow styles generated successfully!") + }.catch { error in + self.fail(message: "Failed to generate shadow styles: \(error)") + } + } +} diff --git a/Sources/FigmaGen/Commands/TextStylesCommand.swift b/Sources/FigmaGen/Commands/TextStylesCommand.swift new file mode 100644 index 0000000..8470f7f --- /dev/null +++ b/Sources/FigmaGen/Commands/TextStylesCommand.swift @@ -0,0 +1,101 @@ +import Foundation +import SwiftCLI +import PromiseKit + +final class TextStylesCommand: AsyncExecutableCommand, GenerationConfigurableCommand { + + // MARK: - Instance Properties + + let generator: TextStylesGenerator + + // MARK: - + + let name = "textStyles" + let shortDescription = "Generates code for text styles from a Figma file." + + let fileKey = Key( + "--fileKey", + description: """ + Figma file key to generate text styles from. + """ + ) + + let fileVersion = Key( + "--fileVersion", + description: """ + Figma file version ID to generate color styles from. + """ + ) + + let includedNodes = VariadicKey( + "--includingNodes", + "-i", + description: #""" + A list of Figma nodes whose styles will be extracted. + Can be repeated multiple times and must be in the format: -i "1:23". + If omitted, all nodes will be included. + """# + ) + + let excludedNodes = VariadicKey( + "--excludingNodes", + "-e", + description: #""" + A list of Figma nodes whose styles will be ignored. + Can be repeated multiple times and must be in the format: -e "1:23". + """# + ) + + let accessToken = Key( + "--accessToken", + description: """ + A personal access token to make requests to the Figma API. + Get more info: https://www.figma.com/developers/api#access-tokens + """ + ) + + let template = Key( + "--template", + "-t", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + let templateOptions = VariadicKey( + "--options", + "-o", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + let destination = Key( + "--destination", + "-d", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + + // MARK: - Initializers + + init(generator: TextStylesGenerator) { + self.generator = generator + } + + // MARK: - Instance Methods + + func executeAsyncAndExit() throws { + firstly { + self.generator.generate(configuration: self.generationConfiguration) + }.done { + self.succeed(message: "Text styles generated successfully!") + }.catch { error in + self.fail(message: "Failed to generate text styles: \(error)") + } + } +} diff --git a/Sources/FigmaGen/Commands/TokensCommand.swift b/Sources/FigmaGen/Commands/TokensCommand.swift new file mode 100644 index 0000000..0050b56 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommand.swift @@ -0,0 +1,263 @@ +import SwiftCLI + +final class TokensCommand: AsyncExecutableCommand { + + // MARK: - Instance Properties + + let generator: TokensGenerator + + // MARK: - + + let name = "tokens" + let shortDescription = "Generates code for tokens from a Figma file." + + let fileKey = FigmaCommandKeys.fileKey + let fileVersion = FigmaCommandKeys.fileVersion + let accessToken = FigmaCommandKeys.accessToken + + let remoteFileOwnerKey = GitHubRemoteFileCommandKeys.ownerKey + let remoteFileRepoKey = GitHubRemoteFileCommandKeys.repoKey + let remoteFileBranchKey = GitHubRemoteFileCommandKeys.branchKey + let remoteFilePathKey = GitHubRemoteFileCommandKeys.pathKey + let remoteRepoAccessTokenKey = GitHubRemoteFileCommandKeys.repoAccessTokenKey + + let themes = ThemeCommandKeys.themes + let fallbackTheme = ThemeCommandKeys.fallbackTheme + let themeTemplate = ThemeCommandKeys.themeTemplate + let themeTemplateOptions = ThemeCommandKeys.themeTemplateOptions + let themeDestination = ThemeCommandKeys.themeDestination + + let colorsTemplate = ColorsCommandKeys.template + let colorsTemplateOptions = ColorsCommandKeys.templateOptions + let colorsDestination = ColorsCommandKeys.destination + + let baseColorsTemplate = ColorsCommandKeys.baseColorsTemplate + let baseColorsTemplateOptions = ColorsCommandKeys.baseColorsTemplateOptions + let baseColorsDestination = ColorsCommandKeys.baseColorsDestination + + let fontFamiliesTemplate = FontFamiliesCommandKeys.template + let fontFamiliesTemplateOptions = FontFamiliesCommandKeys.templateOptions + let fontFamiliesDestination = FontFamiliesCommandKeys.destination + + let typographiesTemplate = TypographiesCommandKeys.template + let typographiesTemplateOptions = TypographiesCommandKeys.templateOptions + let typographiesDestination = TypographiesCommandKeys.destination + + let boxShadowsTemplate = BoxShadowsCommandKeys.template + let boxShadowsTemplateOptions = BoxShadowsCommandKeys.templateOptions + let boxShadowsDestination = BoxShadowsCommandKeys.destination + + let spacingTemplate = SpacingCommandKeys.template + let spacingTemplateOptions = SpacingCommandKeys.templateOptions + let spacingDestination = SpacingCommandKeys.destination + + let bordersTemplate = BordersCommandKeys.template + let bordersTemplateOptions = BordersCommandKeys.templateOptions + let bordersDestination = BordersCommandKeys.destination + + let borderRadiusesTemplate = BordersRadiusCommandKeys.template + let borderRadiusesTemplateOptions = BordersRadiusCommandKeys.templateOptions + let borderRadiusesDestination = BordersRadiusCommandKeys.destination + + let gradientTemplate = GradientCommandKeys.template + let gradientTemplateOptions = GradientCommandKeys.templateOptions + let gradientDestination = GradientCommandKeys.destination + + let animationTimeTemplate = AnimationTimeCommandKeys.template + let animationTimeTemplateOptions = AnimationTimeCommandKeys.templateOptions + let animationTimeDestination = AnimationTimeCommandKeys.destination + + let animationEaseTemplate = AnimationEaseCommandKeys.template + let animationEaseTemplateOptions = AnimationEaseCommandKeys.templateOptions + let animationEaseDestination = AnimationEaseCommandKeys.destination + + // MARK: - Initializers + + init(generator: TokensGenerator) { + self.generator = generator + } + + // MARK: - Instance Methods + + func executeAsyncAndExit() async throws { + do { + try await generator.generate(configuration: configuration) + succeed(message: "Tokens generated successfully!") + } catch { + fail(message: "Failed to generate tokens: \(error)") + } + } +} + +extension TokensCommand { + + // MARK: - Instance Properties + + // !!! Important note !!! + // For CLI usage of FigmaGen we don't support multiple templates for any token type. + var configuration: TokensConfiguration { + TokensConfiguration( + file: resolveFileConfiguration(), + remoteRepoConfig: resolveRemoteRepoConfiguration(), + accessToken: resolveAccessTokenConfiguration(), + themesConfiguration: resolveTokenThemesConfiguration(), + templates: TokensTemplateConfiguration( + colors: [ + TemplateConfiguration( + template: colorsTemplate.value, + templateOptions: resolveTemplateOptions(colorsTemplateOptions.value), + destination: colorsDestination.value + ) + ], + baseColors: [ + TemplateConfiguration( + template: baseColorsTemplate.value, + templateOptions: resolveTemplateOptions(baseColorsTemplateOptions.value), + destination: baseColorsDestination.value + ) + ], + fontFamilies: [ + TemplateConfiguration( + template: fontFamiliesTemplate.value, + templateOptions: resolveTemplateOptions(fontFamiliesTemplateOptions.value), + destination: fontFamiliesDestination.value + ) + ], + typographies: [ + TemplateConfiguration( + template: typographiesTemplate.value, + templateOptions: resolveTemplateOptions(typographiesTemplateOptions.value), + destination: typographiesDestination.value + ) + ], + boxShadows: [ + TemplateConfiguration( + template: boxShadowsTemplate.value, + templateOptions: resolveTemplateOptions(boxShadowsTemplateOptions.value), + destination: boxShadowsDestination.value + ) + ], + theme: [ + TemplateConfiguration( + template: themeTemplate.value, + templateOptions: resolveTemplateOptions(themeTemplateOptions.value), + destination: themeDestination.value + ) + ], + spacing: [ + TemplateConfiguration( + template: spacingTemplate.value, + templateOptions: resolveTemplateOptions(spacingTemplateOptions.value), + destination: spacingDestination.value + ) + ], + borders: [ + TemplateConfiguration( + template: bordersTemplate.value, + templateOptions: resolveTemplateOptions(bordersTemplateOptions.value), + destination: bordersDestination.value + ) + ], + borderRadiuses: [ + TemplateConfiguration( + template: borderRadiusesTemplate.value, + templateOptions: resolveTemplateOptions(borderRadiusesTemplateOptions.value), + destination: borderRadiusesDestination.value + ) + ], + gradients: [ + TemplateConfiguration( + template: gradientTemplate.value, + templateOptions: resolveTemplateOptions(gradientTemplateOptions.value), + destination: gradientDestination.value + ) + ], + animationTimes: [ + TemplateConfiguration( + template: animationTimeTemplate.value, + templateOptions: resolveTemplateOptions(animationTimeTemplateOptions.value), + destination: animationTimeDestination.value + ) + ], + animationEases: [ + TemplateConfiguration( + template: animationEaseTemplate.value, + templateOptions: resolveTemplateOptions(animationEaseTemplateOptions.value), + destination: animationEaseDestination.value + ) + ] + ) + ) + } + + // MARK: - Instance Methods + + private func resolveFileConfiguration() -> FileConfiguration? { + guard let fileKey = fileKey.value else { + return nil + } + + return FileConfiguration( + key: fileKey, + version: fileVersion.value, + includedNodes: nil, + excludedNodes: nil + ) + } + + private func resolveRemoteRepoConfiguration() -> RemoteRepoConfiguration? { + guard + let fileOwner = remoteFileOwnerKey.value, + let fileRepo = remoteFileRepoKey.value, + let fileBranch = remoteFileBranchKey.value, + let filePath = remoteFilePathKey.value, + let remoteRepoAccessToken = remoteRepoAccessTokenKey.value + else { + return nil + } + + return RemoteRepoConfiguration( + owner: fileOwner, + repo: fileRepo, + branch: fileBranch, + filePath: filePath, + accessToken: AccessTokenConfiguration(value: remoteRepoAccessToken) + ) + } + + private func resolveAccessTokenConfiguration() -> AccessTokenConfiguration? { + guard let accessToken = accessToken.value else { + return nil + } + + return AccessTokenConfiguration(value: accessToken) + } + + private func resolveTemplateOptions(_ templateOptionsValues: [String]) -> [String: Any] { + var templateOptions: [String: String] = [:] + + for templateOption in templateOptionsValues { + var optionComponents = templateOption.components(separatedBy: String.templateOptionSeparator) + let optionKey = optionComponents.removeFirst().trimmingCharacters(in: .whitespaces) + let optionValue = optionComponents.joined(separator: .templateOptionSeparator) + + templateOptions[optionKey] = optionValue + } + + return templateOptions + } + + private func resolveTokenThemesConfiguration() -> TokenThemesConfiguration? { + let themes = themes.value + let fallbackTheme = fallbackTheme.value + + guard let fallbackTheme, !themes.isEmpty else { + return nil + } + + return TokenThemesConfiguration( + themes: themes.map { Theme($0) }, + fallbackTheme: Theme(fallbackTheme) + ) + } +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/AnimationEaseCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/AnimationEaseCommandKeys.swift new file mode 100644 index 0000000..2391c74 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/AnimationEaseCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum AnimationEaseCommandKeys { + + static let template = Key( + "--animationEase-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--animationEase-options", + description: """ + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """ + ) + + static let destination = Key( + "--animationEase-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/AnimationTimeCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/AnimationTimeCommandKeys.swift new file mode 100644 index 0000000..09f2212 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/AnimationTimeCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum AnimationTimeCommandKeys { + + static let template = Key( + "--animationTime-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--animationTime-options", + description: """ + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """ + ) + + static let destination = Key( + "--animationTime-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/BordersCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/BordersCommandKeys.swift new file mode 100644 index 0000000..53a960c --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/BordersCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum BordersCommandKeys { + + static let template = Key( + "--borders-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--borders-options", + description: """ + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """ + ) + + static let destination = Key( + "--borders-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/BordersRadiusCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/BordersRadiusCommandKeys.swift new file mode 100644 index 0000000..fa9d0d5 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/BordersRadiusCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum BordersRadiusCommandKeys { + + static let template = Key( + "--borderRadius-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--borderRadius-options", + description: """ + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """ + ) + + static let destination = Key( + "--borderRadius-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/BoxShadowsCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/BoxShadowsCommandKeys.swift new file mode 100644 index 0000000..8abe68e --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/BoxShadowsCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum BoxShadowsCommandKeys { + + static let template = Key( + "--box-shadows-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--box-shadows-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let destination = Key( + "--box-shadows-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/ColorsCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/ColorsCommandKeys.swift new file mode 100644 index 0000000..ba5e593 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/ColorsCommandKeys.swift @@ -0,0 +1,52 @@ +import SwiftCLI + +enum ColorsCommandKeys { + + static let template = Key( + "--colors-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--colors-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let destination = Key( + "--colors-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) + + static let baseColorsTemplate = Key( + "--base-colors-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let baseColorsTemplateOptions = VariadicKey( + "--base-colors-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let baseColorsDestination = Key( + "--base-colors-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/FigmaCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/FigmaCommandKeys.swift new file mode 100644 index 0000000..49a9640 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/FigmaCommandKeys.swift @@ -0,0 +1,26 @@ +import SwiftCLI + +enum FigmaCommandKeys { + + static let fileKey = Key( + "--fileKey", + description: """ + Figma file key to generate text styles from. + """ + ) + + static let fileVersion = Key( + "--fileVersion", + description: """ + Figma file version ID to generate color styles from. + """ + ) + + static let accessToken = Key( + "--accessToken", + description: """ + A personal access token to make requests to the Figma API. + Get more info: https://www.figma.com/developers/api#access-tokens + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/FontCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/FontCommandKeys.swift new file mode 100644 index 0000000..1685c86 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/FontCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum FontFamiliesCommandKeys { + + static let template = Key( + "--font-families-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--font-families-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let destination = Key( + "--font-families-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/GitHubRemoteFileCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/GitHubRemoteFileCommandKeys.swift new file mode 100644 index 0000000..b1146d1 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/GitHubRemoteFileCommandKeys.swift @@ -0,0 +1,39 @@ +import SwiftCLI + +enum GitHubRemoteFileCommandKeys { + + static let ownerKey = Key( + "--remoteFileOwner", + description: """ + Remote Repo owner key to generate text styles from. + """ + ) + + static let repoKey = Key( + "--remoteFileRepo", + description: """ + Remote Repo key to generate text styles from. + """ + ) + + static let branchKey = Key( + "--remoteFileBranch", + description: """ + Remote Repo branch to generate text styles from. + """ + ) + + static let pathKey = Key( + "--remoteFilePath", + description: """ + Remote Repo file key to generate text styles from. + """ + ) + + static let repoAccessTokenKey = Key( + "--remoteRepoAccessToken", + description: """ + Remote Repo personal access token to make requests to the GitHub. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/GradientCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/GradientCommandKeys.swift new file mode 100644 index 0000000..e8d8bf8 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/GradientCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum GradientCommandKeys { + + static let template = Key( + "--gradient-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--gradient-options", + description: """ + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """ + ) + + static let destination = Key( + "--gradient-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/SpacingCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/SpacingCommandKeys.swift new file mode 100644 index 0000000..0631f15 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/SpacingCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum SpacingCommandKeys { + + static let template = Key( + "--spacing-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--spacing-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let destination = Key( + "--spacing-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/ThemeCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/ThemeCommandKeys.swift new file mode 100644 index 0000000..372c520 --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/ThemeCommandKeys.swift @@ -0,0 +1,42 @@ +import SwiftCLI + +enum ThemeCommandKeys { + + static let themes = VariadicKey( + "--themes", + description: """ + An option that will be merged with template context. + """ + ) + + static let fallbackTheme = Key( + "--fallbackTheme", + description: """ + the default theme used when the requested theme is unavailable. Must be one of the values listed in themes. + """ + ) + + static let themeTemplate = Key( + "--theme-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let themeTemplateOptions = VariadicKey( + "--theme-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let themeDestination = Key( + "--theme-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Commands/TokensCommandKeys/TypographiesCommandKeys.swift b/Sources/FigmaGen/Commands/TokensCommandKeys/TypographiesCommandKeys.swift new file mode 100644 index 0000000..8ed8c0f --- /dev/null +++ b/Sources/FigmaGen/Commands/TokensCommandKeys/TypographiesCommandKeys.swift @@ -0,0 +1,28 @@ +import SwiftCLI + +enum TypographiesCommandKeys { + + static let template = Key( + "--typographies-template", + description: """ + Path to the template file. + If no template is passed a default template will be used. + """ + ) + + static let templateOptions = VariadicKey( + "--typographies-options", + description: #""" + An option that will be merged with template context, and overwrite any values of the same name. + Can be repeated multiple times and must be in the format: -o "name:value". + """# + ) + + static let destination = Key( + "--typographies-destination", + description: """ + The path to the file to generate. + By default, generated code will be printed on stdout. + """ + ) +} diff --git a/Sources/FigmaGen/Dependencies.swift b/Sources/FigmaGen/Dependencies.swift new file mode 100644 index 0000000..64ac0fe --- /dev/null +++ b/Sources/FigmaGen/Dependencies.swift @@ -0,0 +1,238 @@ +import Foundation +import FigmaGenTools + +enum Dependencies { + + // MARK: - Type Properties + + static let dataProvider: DataProvider = DefaultDataProvider() + + static let gitHubHTTPService: GitHubHTTPService = HTTPService() + static let gitHubAPIProvider: RemoteRepoProvider = GitHubAPIProvider(httpService: gitHubHTTPService) + + static let figmaHTTPService: FigmaHTTPService = HTTPService() + static let figmaAPIProvider: FigmaAPIProvider = DefaultFigmaAPIProvider(httpService: figmaHTTPService) + + static let figmaFilesProvider: FigmaFilesProvider = DefaultFigmaFilesProvider(apiProvider: figmaAPIProvider) + static let figmaNodesProvider: FigmaNodesProvider = DefaultFigmaNodesProvider() + + static let assetsProvider: AssetsProvider = DefaultAssetsProvider() + + static let colorStyleAssetsProvider: ColorStyleAssetsProvider = DefaultColorStyleAssetsProvider( + assetsProvider: assetsProvider + ) + + static let colorStylesProvider: ColorStylesProvider = DefaultColorStylesProvider( + filesProvider: figmaFilesProvider, + nodesProvider: figmaNodesProvider, + colorStyleAssetsProvider: colorStyleAssetsProvider + ) + + static let textStylesProvider: TextStylesProvider = DefaultTextStylesProvider( + filesProvider: figmaFilesProvider, + nodesProvider: figmaNodesProvider + ) + + static let imageRenderProvider: ImageRenderProvider = DefaultImageRenderProvider(apiProvider: figmaAPIProvider) + + static let imageAssetsProvider: ImageAssetsProvider = DefaultImageAssetsProvider( + assetsProvider: assetsProvider, + dataProvider: dataProvider + ) + + static let imageResourcesProvider: ImageResourcesProvider = DefaultImageResourcesProvider( + dataProvider: dataProvider, + postProcessor: ImageResourcesPostProcessor() + ) + + static let imagesProvider: ImagesProvider = DefaultImagesProvider( + filesProvider: figmaFilesProvider, + nodesProvider: figmaNodesProvider, + imageRenderProvider: imageRenderProvider, + imageAssetsProvider: imageAssetsProvider, + imageResourcesProvider: imageResourcesProvider + ) + + static let configurationProvider: ConfigurationProvider = DefaultConfigurationProvider() + + static let shadowStylesProvider: ShadowStylesProvider = DefaultShadowStylesProvider( + filesProvider: figmaFilesProvider, + nodesProvider: figmaNodesProvider + ) + + static let tokensProvider: TokensProvider = DefaultTokensProvider( + figmaApiProvider: figmaAPIProvider, + gitHubApiProvider: gitHubAPIProvider + ) + + // MARK: - + + static let tokensResolver: TokensResolver = DefaultTokensResolver() + static let renderParametersResolver: RenderParametersResolver = DefaultRenderParametersResolver() + static let accessTokenResolver: AccessTokenResolver = DefaultAccessTokenResolver() + + static let tokensGenerationParametersResolver: TokensGenerationParametersResolver = + DefaultTokensGenerationParametersResolver( + renderParametersResolver: renderParametersResolver, + accessTokenResolver: accessTokenResolver + ) + + static let colorTokensContextProvider: ColorTokensContextProvider = DefaultColorTokensContextProvider( + tokensResolver: tokensResolver + ) + + static let boxShadowTokensContextProvider: BoxShadowTokensContextProvider = DefaultBoxShadowTokensContextProvider() + + static let gradientTokensContextProvider: ColorTokensContextProvider = DefaultGradientTokensContextProvider( + tokensResolver: tokensResolver + ) + + // MARK: - + + static let templateContextCoder: TemplateContextCoder = DefaultTemplateContextCoder() + + static let stencilExtensions: [StencilExtension] = [ + StencilByteToHexFilter(), + StencilHexToByteFilter(), + StencilByteToFloatFilter(), + StencilFloatToByteFilter(), + StencilVectorInfoFilter(contextCoder: templateContextCoder), + StencilColorRGBHexInfoFilter(contextCoder: templateContextCoder), + StencilColorRGBAHexInfoFilter(contextCoder: templateContextCoder), + StencilColorRGBInfoFilter(contextCoder: templateContextCoder), + StencilColorRGBAInfoFilter(contextCoder: templateContextCoder), + StencilColorInfoFilter(contextCoder: templateContextCoder), + StencilFontInfoFilter(contextCoder: templateContextCoder), + StencilFontInitializerModificator(contextCoder: templateContextCoder), + StencilFontSystemFilter(contextCoder: templateContextCoder), + StencilCollectionDropFirstModificator(), + StencilCollectionDropLastModificator(), + StencilCollectionRemovingFirstModificator(), + StencilHexToAlphaFilter(), + StencilFullHexModificator(), + StencilRecursiveTokenFindModicator() + ] + + static let templateRenderer: TemplateRenderer = DefaultTemplateRenderer( + contextCoder: templateContextCoder, + stencilExtensions: stencilExtensions + ) + + // MARK: - + + static let colorStylesGenerator: ColorStylesGenerator = DefaultColorStylesGenerator( + colorStylesProvider: colorStylesProvider, + templateRenderer: templateRenderer, + accessTokenResolver: accessTokenResolver, + renderParametersResolver: renderParametersResolver + ) + + static let textStylesGenerator: TextStylesGenerator = DefaultTextStylesGenerator( + textStylesProvider: textStylesProvider, + templateRenderer: templateRenderer, + accessTokenResolver: accessTokenResolver, + renderParametersResolver: renderParametersResolver + ) + + static let imagesGenerator: ImagesGenerator = DefaultImagesGenerator( + imagesProvider: imagesProvider, + templateRenderer: templateRenderer, + accessTokenResolver: accessTokenResolver, + renderParametersResolver: renderParametersResolver + ) + + static let shadowStylesGenerator: ShadowStylesGenerator = DefaultShadowStylesGenerator( + shadowStylesProvider: shadowStylesProvider, + templateRenderer: templateRenderer, + accessTokenResolver: accessTokenResolver, + renderParametersResolver: renderParametersResolver + ) + + static let colorTokensGenerator: ColorTokensGenerator = DefaultColorTokensGenerator( + templateRenderer: templateRenderer, + colorTokensContextProvider: colorTokensContextProvider + ) + + static let baseColorTokensGenerator: BaseColorTokensGenerator = DefaultBaseColorTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let fontFamilyTokensGenerator: FontFamilyTokensGenerator = DefaultFontFamilyTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let typographyTokensGenerator: TypographyTokensGenerator = DefaultTypographyTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let boxShadowTokensGenerator: BoxShadowTokensGenerator = DefaultBoxShadowTokensGenerator( + boxShadowTokensContextProvider: boxShadowTokensContextProvider, + templateRenderer: templateRenderer + ) + + static let themeTokensGenerator: ThemeTokensGenerator = DefaultThemeTokensGenerator( + colorTokensContextProvider: colorTokensContextProvider, + gradientTokensContextProvider: gradientTokensContextProvider, + boxShadowsContextProvider: boxShadowTokensContextProvider, + templateRenderer: templateRenderer + ) + + static let spacingTokensGenerator: SpacingTokensGenerator = DefaultSpacingTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let borderTokensGenerator: BorderTokensGenerator = DefaultBorderTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let borderRadiusTokensGenerator: BorderRadiusTokensGenerator = DefaultBorderRadiusTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let gradientTokensGenerator: GradientTokensGenerator = DefaultGradientTokensGenerator( + templateRenderer: templateRenderer, + gradientProvider: gradientTokensContextProvider + ) + + static let animationTimeTokensGenerator: AnimationTimeTokensGenerator = DefaultAnimationTimeTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let animationEaseTokensGenerator: AnimationEaseTokensGenerator = DefaultAnimationEaseTokensGenerator( + tokensResolver: tokensResolver, + templateRenderer: templateRenderer + ) + + static let tokensGenerator: TokensGenerator = DefaultTokensGenerator( + tokensProvider: tokensProvider, + tokensGenerationParametersResolver: tokensGenerationParametersResolver, + colorTokensGenerator: colorTokensGenerator, + baseColorTokensGenerator: baseColorTokensGenerator, + fontFamilyTokensGenerator: fontFamilyTokensGenerator, + typographyTokensGenerator: typographyTokensGenerator, + boxShadowTokensGenerator: boxShadowTokensGenerator, + themeTokensGenerator: themeTokensGenerator, + spacingTokensGenerator: spacingTokensGenerator, + bordersTokensGenerator: borderTokensGenerator, + borderRadiusesTokensGenerator: borderRadiusTokensGenerator, + gradientTokensGenerator: gradientTokensGenerator, + animationTimeTokensGenerator: animationTimeTokensGenerator, + animationEaseTokensGenerator: animationEaseTokensGenerator + ) + + static let libraryGenerator: LibraryGenerator = DefaultLibraryGenerator( + configurationProvider: configurationProvider, + colorStylesGenerator: colorStylesGenerator, + textStylesGenerator: textStylesGenerator, + imagesGenerator: imagesGenerator, + shadowStylesGenerator: shadowStylesGenerator, + tokensGenerator: tokensGenerator + ) +} diff --git a/Sources/FigmaGen/Generators/ColorStyles/ColorStylesContext.swift b/Sources/FigmaGen/Generators/ColorStyles/ColorStylesContext.swift new file mode 100644 index 0000000..19605ee --- /dev/null +++ b/Sources/FigmaGen/Generators/ColorStyles/ColorStylesContext.swift @@ -0,0 +1,8 @@ +import Foundation + +struct ColorStylesContext: Encodable { + + // MARK: - Instance Properties + + let colorStyles: [ColorStyle] +} diff --git a/Sources/FigmaGen/Generators/ColorStyles/ColorStylesGenerator.swift b/Sources/FigmaGen/Generators/ColorStyles/ColorStylesGenerator.swift new file mode 100644 index 0000000..fa805f4 --- /dev/null +++ b/Sources/FigmaGen/Generators/ColorStyles/ColorStylesGenerator.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ColorStylesGenerator { + + // MARK: - Instance Methods + + func generate(configuration: ColorStylesConfiguration) -> Promise +} diff --git a/Sources/FigmaGen/Generators/ColorStyles/DefaultColorStylesGenerator.swift b/Sources/FigmaGen/Generators/ColorStyles/DefaultColorStylesGenerator.swift new file mode 100644 index 0000000..032cfb0 --- /dev/null +++ b/Sources/FigmaGen/Generators/ColorStyles/DefaultColorStylesGenerator.swift @@ -0,0 +1,62 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +final class DefaultColorStylesGenerator: ColorStylesGenerator, GenerationParametersResolving { + + // MARK: - Instance Properties + + let colorStylesProvider: ColorStylesProvider + let templateRenderer: TemplateRenderer + let accessTokenResolver: AccessTokenResolver + let renderParametersResolver: RenderParametersResolver + + let defaultTemplateType = RenderTemplateType.native(name: "ColorStyles") + let defaultDestination = RenderDestination.console + + // MARK: - Initializers + + init( + colorStylesProvider: ColorStylesProvider, + templateRenderer: TemplateRenderer, + accessTokenResolver: AccessTokenResolver, + renderParametersResolver: RenderParametersResolver + ) { + self.colorStylesProvider = colorStylesProvider + self.templateRenderer = templateRenderer + self.accessTokenResolver = accessTokenResolver + self.renderParametersResolver = renderParametersResolver + } + + // MARK: - Instance Methods + + private func generate(parameters: GenerationParameters, assets: String?) -> Promise { + firstly { + colorStylesProvider.fetchColorStyles( + from: parameters.file, + nodes: parameters.nodes, + assets: assets + ) + }.map { colorStyles in + ColorStylesContext(colorStyles: colorStyles) + }.done { context in + try parameters.renderParameters.forEach { params in + try self.templateRenderer.renderTemplate( + params.template, + to: params.destination, + context: context + ) + } + } + } + + // MARK: - + + func generate(configuration: ColorStylesConfiguration) -> Promise { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + try self.resolveGenerationParameters(from: configuration.generation) + }.then { parameters in + self.generate(parameters: parameters, assets: configuration.assets) + } + } +} diff --git a/Sources/FigmaGen/Generators/GenerationParametersError.swift b/Sources/FigmaGen/Generators/GenerationParametersError.swift new file mode 100644 index 0000000..5c4eb44 --- /dev/null +++ b/Sources/FigmaGen/Generators/GenerationParametersError.swift @@ -0,0 +1,25 @@ +import Foundation + +enum GenerationParametersError: Error, CustomStringConvertible { + + // MARK: - Enumeration Cases + + case invalidFileConfiguration + case invalidAccessToken + case invalidGitHubAccessToken + + // MARK: - Instance Properties + + var description: String { + switch self { + case .invalidFileConfiguration: + return "Figma file configuration cannot be nil" + + case .invalidAccessToken: + return "Figma access token cannot be empty or nil" + + case .invalidGitHubAccessToken: + return "GitHiub access token cannot be empty or nil" + } + } +} diff --git a/Sources/FigmaGen/Generators/GenerationParametersResolving.swift b/Sources/FigmaGen/Generators/GenerationParametersResolving.swift new file mode 100644 index 0000000..2f0b10e --- /dev/null +++ b/Sources/FigmaGen/Generators/GenerationParametersResolving.swift @@ -0,0 +1,58 @@ +import Foundation + +protocol GenerationParametersResolving { + + // MARK: - Instance Properties + + var accessTokenResolver: AccessTokenResolver { get } + var renderParametersResolver: RenderParametersResolver { get } + + // MARK: - Instance Properties + + var defaultTemplateType: RenderTemplateType { get } + var defaultDestination: RenderDestination { get } + + // MARK: - Instance Methods + + func resolveGenerationParameters(from configuration: GenerationConfiguration) throws -> GenerationParameters +} + +extension GenerationParametersResolving { + + // MARK: - + + func resolveGenerationParameters(from configuration: GenerationConfiguration) throws -> GenerationParameters { + guard let fileConfiguration = configuration.file else { + throw GenerationParametersError.invalidFileConfiguration + } + + let accessToken = accessTokenResolver.resolveAccessToken(from: configuration.accessToken) + + guard let accessToken, !accessToken.isEmpty else { + throw GenerationParametersError.invalidAccessToken + } + + let file = FileParameters( + key: fileConfiguration.key, + version: fileConfiguration.version, + accessToken: accessToken + ) + + let nodes = NodesParameters( + includedIDs: fileConfiguration.includedNodes, + excludedIDs: fileConfiguration.excludedNodes + ) + + let renderParametersList = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates, + defaultTemplateType: defaultTemplateType, + defaultDestination: defaultDestination + ) + + return GenerationParameters( + file: file, + nodes: nodes, + renderParameters: renderParametersList + ) + } +} diff --git a/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift b/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift new file mode 100644 index 0000000..7a6673d --- /dev/null +++ b/Sources/FigmaGen/Generators/Images/DefaultImagesGenerator.swift @@ -0,0 +1,86 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +final class DefaultImagesGenerator: ImagesGenerator, GenerationParametersResolving { + + // MARK: - Instance Properties + + let imagesProvider: ImagesProvider + let templateRenderer: TemplateRenderer + let accessTokenResolver: AccessTokenResolver + let renderParametersResolver: RenderParametersResolver + + let defaultTemplateType = RenderTemplateType.native(name: "Images") + let defaultDestination = RenderDestination.console + + // MARK: - Initializers + + init( + imagesProvider: ImagesProvider, + templateRenderer: TemplateRenderer, + accessTokenResolver: AccessTokenResolver, + renderParametersResolver: RenderParametersResolver + ) { + self.imagesProvider = imagesProvider + self.templateRenderer = templateRenderer + self.accessTokenResolver = accessTokenResolver + self.renderParametersResolver = renderParametersResolver + } + + // MARK: - Instance Methods + + private func generate(parameters: GenerationParameters, imagesParameters: ImagesParameters) -> Promise { + firstly { + self.imagesProvider.fetchImages( + from: parameters.file, + nodes: parameters.nodes, + parameters: imagesParameters + ) + }.map { imageSets in + ImagesContext( + imageSets: imageSets.sorted { $0.name.lowercased() < $1.name.lowercased() } + ) + }.done { context in + try parameters.renderParameters.forEach { params in + try self.templateRenderer.renderTemplate( + params.template, + to: params.destination, + context: context + ) + } + } + } + + // MARK: - + + func generate(configuration: ImagesConfiguration) -> Promise { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + try self.resolveGenerationParameters(from: configuration.generatation) + }.then { parameters in + self.generate(parameters: parameters, imagesParameters: configuration.imagesParameters) + } + } +} + +extension ImagesConfiguration { + + // MARK: - Instance Properties + + fileprivate var imagesParameters: ImagesParameters { + ImagesParameters( + format: format, + scales: scales, + assets: assets, + resources: resources, + postProcessor: postProcessor, + onlyExportables: onlyExportables, + useAbsoluteBounds: useAbsoluteBounds, + preserveVectorData: preserveVectorData, + renderAs: renderAs, + groupByFrame: groupByFrame, + groupByComponentSet: groupByComponentSet, + namingStyle: namingStyle + ) + } +} diff --git a/Sources/FigmaGen/Generators/Images/ImagesContext.swift b/Sources/FigmaGen/Generators/Images/ImagesContext.swift new file mode 100644 index 0000000..19dea07 --- /dev/null +++ b/Sources/FigmaGen/Generators/Images/ImagesContext.swift @@ -0,0 +1,8 @@ +import Foundation + +struct ImagesContext: Encodable { + + // MARK: - Instance Properties + + let imageSets: [ImageSet] +} diff --git a/Sources/FigmaGen/Generators/Images/ImagesGenerator.swift b/Sources/FigmaGen/Generators/Images/ImagesGenerator.swift new file mode 100644 index 0000000..d882af0 --- /dev/null +++ b/Sources/FigmaGen/Generators/Images/ImagesGenerator.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ImagesGenerator { + + // MARK: - Instance Methods + + func generate(configuration: ImagesConfiguration) -> Promise +} diff --git a/Sources/FigmaGen/Generators/Library/DefaultLibraryGenerator.swift b/Sources/FigmaGen/Generators/Library/DefaultLibraryGenerator.swift new file mode 100644 index 0000000..c8a1727 --- /dev/null +++ b/Sources/FigmaGen/Generators/Library/DefaultLibraryGenerator.swift @@ -0,0 +1,70 @@ +import Foundation +import PromiseKit + +final class DefaultLibraryGenerator: LibraryGenerator { + + // MARK: - Instance Properties + + let configurationProvider: ConfigurationProvider + let colorStylesGenerator: ColorStylesGenerator + let textStylesGenerator: TextStylesGenerator + let imagesGenerator: ImagesGenerator + let shadowStylesGenerator: ShadowStylesGenerator + let tokensGenerator: TokensGenerator + + // MARK: - Initializers + + init( + configurationProvider: ConfigurationProvider, + colorStylesGenerator: ColorStylesGenerator, + textStylesGenerator: TextStylesGenerator, + imagesGenerator: ImagesGenerator, + shadowStylesGenerator: ShadowStylesGenerator, + tokensGenerator: TokensGenerator + ) { + self.configurationProvider = configurationProvider + self.colorStylesGenerator = colorStylesGenerator + self.textStylesGenerator = textStylesGenerator + self.imagesGenerator = imagesGenerator + self.shadowStylesGenerator = shadowStylesGenerator + self.tokensGenerator = tokensGenerator + } + + // MARK: - Instance Methods + + private func generate(configuration: Configuration) -> Promise { + var promises: [Promise] = [] + + if let colorStylesConfiguration = configuration.resolveColorStyles() { + promises.append(colorStylesGenerator.generate(configuration: colorStylesConfiguration)) + } + + if let textStylesConfiguration = configuration.resolveTextStyles() { + promises.append(textStylesGenerator.generate(configuration: textStylesConfiguration)) + } + + if let imagesConfiguration = configuration.resolveImages() { + promises.append(imagesGenerator.generate(configuration: imagesConfiguration)) + } + + if let shadowStylesConfiguration = configuration.resolveShadowStyles() { + promises.append(shadowStylesGenerator.generate(configuration: shadowStylesConfiguration)) + } + + if let tokensConfiguration = configuration.resolveTokens() { + promises.append(Promise { try await self.tokensGenerator.generate(configuration: tokensConfiguration) }) + } + + return when(fulfilled: promises) + } + + // MARK: - + + func generate(configurationPath: String) -> Promise { + firstly { + self.configurationProvider.fetchConfiguration(from: configurationPath) + }.then { configuration in + self.generate(configuration: configuration) + } + } +} diff --git a/Sources/FigmaGen/Generators/Library/LibraryGenerator.swift b/Sources/FigmaGen/Generators/Library/LibraryGenerator.swift new file mode 100644 index 0000000..53e9f29 --- /dev/null +++ b/Sources/FigmaGen/Generators/Library/LibraryGenerator.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol LibraryGenerator { + + // MARK: - Instance Methods + + func generate(configurationPath: String) -> Promise +} diff --git a/Sources/FigmaGen/Generators/ShadowStyles/DefaultShadowStylesGenerator.swift b/Sources/FigmaGen/Generators/ShadowStyles/DefaultShadowStylesGenerator.swift new file mode 100644 index 0000000..0d67c66 --- /dev/null +++ b/Sources/FigmaGen/Generators/ShadowStyles/DefaultShadowStylesGenerator.swift @@ -0,0 +1,58 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +final class DefaultShadowStylesGenerator: ShadowStylesGenerator, GenerationParametersResolving { + + // MARK: - Instance Properties + + let shadowStylesProvider: ShadowStylesProvider + let templateRenderer: TemplateRenderer + let accessTokenResolver: AccessTokenResolver + let renderParametersResolver: RenderParametersResolver + + let defaultTemplateType: RenderTemplateType = .native(name: "ShadowStyles") + let defaultDestination: RenderDestination = .console + + // MARK: - Initializers + + init( + shadowStylesProvider: ShadowStylesProvider, + templateRenderer: TemplateRenderer, + accessTokenResolver: AccessTokenResolver, + renderParametersResolver: RenderParametersResolver + ) { + self.shadowStylesProvider = shadowStylesProvider + self.templateRenderer = templateRenderer + self.accessTokenResolver = accessTokenResolver + self.renderParametersResolver = renderParametersResolver + } + + // MARK: - Instance Methods + + private func generate(parameters: GenerationParameters) -> Promise { + firstly { + self.shadowStylesProvider.fetchShadowStyles(from: parameters.file, nodes: parameters.nodes) + }.map { shadowStyles in + ShadowStylesContext(shadowStyles: shadowStyles) + }.done { context in + try parameters.renderParameters.forEach { params in + try self.templateRenderer.renderTemplate( + params.template, + to: params.destination, + context: context + ) + } + } + } + + // MARK: - ShadowStylesGenerator + + func generate(configuration: ShadowStylesConfiguration) -> Promise { + perform(on: .global(qos: .userInitiated)) { + try self.resolveGenerationParameters(from: configuration) + }.then { parameters in + self.generate(parameters: parameters) + } + } +} diff --git a/Sources/FigmaGen/Generators/ShadowStyles/ShadowStylesContext.swift b/Sources/FigmaGen/Generators/ShadowStyles/ShadowStylesContext.swift new file mode 100644 index 0000000..ae21146 --- /dev/null +++ b/Sources/FigmaGen/Generators/ShadowStyles/ShadowStylesContext.swift @@ -0,0 +1,8 @@ +import Foundation + +struct ShadowStylesContext: Encodable { + + // MARK: - Instance Properties + + let shadowStyles: [ShadowStyle] +} diff --git a/Sources/FigmaGen/Generators/ShadowStyles/ShadowStylesGenerator.swift b/Sources/FigmaGen/Generators/ShadowStyles/ShadowStylesGenerator.swift new file mode 100644 index 0000000..80a1600 --- /dev/null +++ b/Sources/FigmaGen/Generators/ShadowStyles/ShadowStylesGenerator.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ShadowStylesGenerator { + + // MARK: - Instance Methods + + func generate(configuration: ShadowStylesConfiguration) -> Promise +} diff --git a/Sources/FigmaGen/Generators/TextStyles/DefaultTextStylesGenerator.swift b/Sources/FigmaGen/Generators/TextStyles/DefaultTextStylesGenerator.swift new file mode 100644 index 0000000..86430aa --- /dev/null +++ b/Sources/FigmaGen/Generators/TextStyles/DefaultTextStylesGenerator.swift @@ -0,0 +1,58 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +final class DefaultTextStylesGenerator: TextStylesGenerator, GenerationParametersResolving { + + // MARK: - Instance Properties + + let textStylesProvider: TextStylesProvider + let templateRenderer: TemplateRenderer + let accessTokenResolver: AccessTokenResolver + let renderParametersResolver: RenderParametersResolver + + let defaultTemplateType = RenderTemplateType.native(name: "TextStyles") + let defaultDestination = RenderDestination.console + + // MARK: - Initializers + + init( + textStylesProvider: TextStylesProvider, + templateRenderer: TemplateRenderer, + accessTokenResolver: AccessTokenResolver, + renderParametersResolver: RenderParametersResolver + ) { + self.textStylesProvider = textStylesProvider + self.templateRenderer = templateRenderer + self.accessTokenResolver = accessTokenResolver + self.renderParametersResolver = renderParametersResolver + } + + // MARK: - Instance Methods + + private func generate(parameters: GenerationParameters) -> Promise { + firstly { + self.textStylesProvider.fetchTextStyles(from: parameters.file, nodes: parameters.nodes) + }.map { textStyles in + TextStylesContext(textStyles: textStyles) + }.done { context in + try parameters.renderParameters.forEach { params in + try self.templateRenderer.renderTemplate( + params.template, + to: params.destination, + context: context + ) + } + } + } + + // MARK: - + + func generate(configuration: TextStylesConfiguration) -> Promise { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + try self.resolveGenerationParameters(from: configuration) + }.then { parameters in + self.generate(parameters: parameters) + } + } +} diff --git a/Sources/FigmaGen/Generators/TextStyles/TextStylesContext.swift b/Sources/FigmaGen/Generators/TextStyles/TextStylesContext.swift new file mode 100644 index 0000000..53f9df6 --- /dev/null +++ b/Sources/FigmaGen/Generators/TextStyles/TextStylesContext.swift @@ -0,0 +1,8 @@ +import Foundation + +struct TextStylesContext: Encodable { + + // MARK: - Instance Properties + + let textStyles: [TextStyle] +} diff --git a/Sources/FigmaGen/Generators/TextStyles/TextStylesGenerator.swift b/Sources/FigmaGen/Generators/TextStyles/TextStylesGenerator.swift new file mode 100644 index 0000000..eaffd03 --- /dev/null +++ b/Sources/FigmaGen/Generators/TextStyles/TextStylesGenerator.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol TextStylesGenerator { + + // MARK: - Instance Methods + + func generate(configuration: TextStylesConfiguration) -> Promise +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/AnimationEase.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/AnimationEase.swift new file mode 100644 index 0000000..a884ba8 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/AnimationEase.swift @@ -0,0 +1,22 @@ +import Foundation + +struct AnimationEase { + + struct Base { + let path: [String] + let x1: String + let y1: String + let x2: String + let y2: String + } + + struct Spring { + let path: [String] + let stiffness: String + let damping: String + let mass: String + } + + let base: Base? + let spring: Spring? +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/AnimationTime.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/AnimationTime.swift new file mode 100644 index 0000000..05d930a --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/AnimationTime.swift @@ -0,0 +1,8 @@ +import Foundation + +struct AnimationTime { + + let path: [String] + let duration: Double + let durationMs: String +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/BaseColorToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/BaseColorToken.swift new file mode 100644 index 0000000..1badfd4 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/BaseColorToken.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias BaseColorToken = ContextToken diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/BorderRadiusToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/BorderRadiusToken.swift new file mode 100644 index 0000000..9469585 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/BorderRadiusToken.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias BorderRadiusToken = ContextToken diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/BorderToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/BorderToken.swift new file mode 100644 index 0000000..bf16663 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/BorderToken.swift @@ -0,0 +1,8 @@ +import Foundation + +struct BorderToken { + + let path: [String] + let width: String + let style: String +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/BorderWidthToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/BorderWidthToken.swift new file mode 100644 index 0000000..a0df264 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/BorderWidthToken.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias BorderWidthToken = ContextToken diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/BoxShadowToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/BoxShadowToken.swift new file mode 100644 index 0000000..cc2e156 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/BoxShadowToken.swift @@ -0,0 +1,15 @@ +import Foundation + +struct BoxShadowToken { + + // MARK: - Instance Properties + + let path: [String] + + let color: String + let type: String + let x: String + let y: String + let blur: String + let spread: String +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift new file mode 100644 index 0000000..6538217 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/ColorToken.swift @@ -0,0 +1,11 @@ +import Foundation + +struct ColorToken: TokenProtocol, Encodable { + + // MARK: - Instance Properties + + let name: String + let path: [String] + let value: String + let reference: String +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/ContextToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/ContextToken.swift new file mode 100644 index 0000000..f27cd50 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/ContextToken.swift @@ -0,0 +1,9 @@ +import Foundation + +struct ContextToken { + + // MARK: - Instance Properties + + let path: [String] + let value: String +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/FontFamilyToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/FontFamilyToken.swift new file mode 100644 index 0000000..5b94c51 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/FontFamilyToken.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias FontFamilyToken = ContextToken diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/FontWeightToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/FontWeightToken.swift new file mode 100644 index 0000000..b4b4433 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/FontWeightToken.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias FontWeightToken = ContextToken diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/LinearGradientToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/LinearGradientToken.swift new file mode 100644 index 0000000..f517621 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/LinearGradientToken.swift @@ -0,0 +1,21 @@ +import Foundation + +struct LinearGradientToken: TokenProtocol, Encodable { + + struct ColorStop: Encodable { + let color: String + let percentage: CGFloat + } + + // Нужен, т.к CGPoint нельзя использовать корректно в stencil шаблоне + struct Point: Encodable { + let x: CGFloat + let y: CGFloat + } + + let path: [String] + let name: String + let stops: [ColorStop] + let startPoint: Point + let endPoint: Point +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/SpacingToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/SpacingToken.swift new file mode 100644 index 0000000..76f915e --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/SpacingToken.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias SpacingToken = ContextToken diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/TokenProtocol.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/TokenProtocol.swift new file mode 100644 index 0000000..d9198f3 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/TokenProtocol.swift @@ -0,0 +1,12 @@ +import Foundation + +protocol TokenProtocol { + var name: String { get } + var path: [String] { get } +} + +extension TokenProtocol { + var name: String { + path.joined(separator: ".") + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/TokenThemeValue.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/TokenThemeValue.swift new file mode 100644 index 0000000..45d6127 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/TokenThemeValue.swift @@ -0,0 +1,12 @@ +import Foundation + +struct TokenThemeValue { + + let themeName: String + let value: T + + init(theme: String, value: T) { + self.themeName = theme + self.value = value + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Contexts/TypographyToken.swift b/Sources/FigmaGen/Generators/Tokens/Contexts/TypographyToken.swift new file mode 100644 index 0000000..28d06cf --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Contexts/TypographyToken.swift @@ -0,0 +1,28 @@ +import Foundation + +struct TypographyToken { + + // MARK: - Nested Types + + typealias FontSizeToken = ContextToken + typealias FontScaleToken = ContextToken + typealias LetterSpacingToken = ContextToken + typealias LineHeightToken = ContextToken + typealias TextDecorationToken = ContextToken + typealias ParagraphSpacingToken = ContextToken + typealias ParagraphIndentToken = ContextToken + + // MARK: - Instance Properties + + let path: [String] + let name: String + let fontFamily: FontFamilyToken + let fontWeight: FontWeightToken + let lineHeight: LineHeightToken + let fontSize: FontSizeToken + let letterSpacing: LetterSpacingToken? + let paragraphSpacing: ParagraphSpacingToken + let paragraphIndent: ParagraphIndentToken? + let textDecoration: TextDecorationToken? + let fontScale: FontScaleToken? +} diff --git a/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift new file mode 100644 index 0000000..f42ca65 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/DefaultTokensGenerator.swift @@ -0,0 +1,249 @@ +import Foundation +import FigmaGenTools +import Expression + +final class DefaultTokensGenerator: TokensGenerator { + + // MARK: - Instance Properties + + let tokensProvider: TokensProvider + let tokensGenerationParametersResolver: TokensGenerationParametersResolver + let colorTokensGenerator: ColorTokensGenerator + let baseColorTokensGenerator: BaseColorTokensGenerator + let fontFamilyTokensGenerator: FontFamilyTokensGenerator + let typographyTokensGenerator: TypographyTokensGenerator + let boxShadowTokensGenerator: BoxShadowTokensGenerator + let themeTokensGenerator: ThemeTokensGenerator + let spacingTokensGenerator: SpacingTokensGenerator + let bordersTokensGenerator: BorderTokensGenerator + let borderRadiusesTokensGenerator: BorderRadiusTokensGenerator + let gradientTokensGenerator: GradientTokensGenerator + let animationTimeTokensGenerator: AnimationTimeTokensGenerator + let animationEaseTokensGenerator: AnimationEaseTokensGenerator + + // MARK: - Initializers + + init( + tokensProvider: TokensProvider, + tokensGenerationParametersResolver: TokensGenerationParametersResolver, + colorTokensGenerator: ColorTokensGenerator, + baseColorTokensGenerator: BaseColorTokensGenerator, + fontFamilyTokensGenerator: FontFamilyTokensGenerator, + typographyTokensGenerator: TypographyTokensGenerator, + boxShadowTokensGenerator: BoxShadowTokensGenerator, + themeTokensGenerator: ThemeTokensGenerator, + spacingTokensGenerator: SpacingTokensGenerator, + bordersTokensGenerator: BorderTokensGenerator, + borderRadiusesTokensGenerator: BorderRadiusTokensGenerator, + gradientTokensGenerator: GradientTokensGenerator, + animationTimeTokensGenerator: AnimationTimeTokensGenerator, + animationEaseTokensGenerator: AnimationEaseTokensGenerator + ) { + self.tokensProvider = tokensProvider + self.tokensGenerationParametersResolver = tokensGenerationParametersResolver + self.colorTokensGenerator = colorTokensGenerator + self.baseColorTokensGenerator = baseColorTokensGenerator + self.fontFamilyTokensGenerator = fontFamilyTokensGenerator + self.typographyTokensGenerator = typographyTokensGenerator + self.boxShadowTokensGenerator = boxShadowTokensGenerator + self.themeTokensGenerator = themeTokensGenerator + self.spacingTokensGenerator = spacingTokensGenerator + self.bordersTokensGenerator = bordersTokensGenerator + self.borderRadiusesTokensGenerator = borderRadiusesTokensGenerator + self.gradientTokensGenerator = gradientTokensGenerator + self.animationTimeTokensGenerator = animationTimeTokensGenerator + self.animationEaseTokensGenerator = animationEaseTokensGenerator + } + + // MARK: - Instance Methods + + private func generate(parameters: TokensGenerationParameters) async throws { + let tokenValues = try await fetchTokens(from: parameters) + + try generateColorsTokens(parameters: parameters, tokenValues: tokenValues) + try generateBaseColorsTokens(parameters: parameters, tokenValues: tokenValues) + try generateFontFamilyTokens(parameters: parameters, tokenValues: tokenValues) + try generateTypographyTokens(parameters: parameters, tokenValues: tokenValues) + try generateBoxShadowTokens(parameters: parameters, tokenValues: tokenValues) + try generateThemeTokens(parameters: parameters, tokenValues: tokenValues) + try generateSpacingTokens(parameters: parameters, tokenValues: tokenValues) + try generateBorderTokens(parameters: parameters, tokenValues: tokenValues) + try generateBorderRadiusTokens(parameters: parameters, tokenValues: tokenValues) + try generateGradientTokens(parameters: parameters, tokenValues: tokenValues) + try generateAnimationTimeTokens(parameters: parameters, tokenValues: tokenValues) + try generateAnimationEaseTokens(parameters: parameters, tokenValues: tokenValues) + } + + private func fetchTokens(from parameters: TokensGenerationParameters) async throws -> TokenValues { + if let file = parameters.file { + return try await tokensProvider.fetchTokens(from: file) + } else if let remoteFile = parameters.remoteFile { + return try await tokensProvider.fetchTokens(from: remoteFile) + } else { + throw GenerationParametersError.invalidFileConfiguration + } + } + + private func generateSpacingTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + spacingTokensGenerator, + tokensName: "spacings", + renderParameters: parameters.tokens.spacingRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateThemeTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + themeTokensGenerator, + tokensName: "theme", + renderParameters: parameters.tokens.themeRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateBoxShadowTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + boxShadowTokensGenerator, + tokensName: "box shadow", + renderParameters: parameters.tokens.boxShadowRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateTypographyTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + typographyTokensGenerator, + tokensName: "typogrphy", + renderParameters: parameters.tokens.typographyRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateFontFamilyTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + fontFamilyTokensGenerator, + tokensName: "font family", + renderParameters: parameters.tokens.fontFamilyRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateBaseColorsTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + baseColorTokensGenerator, + tokensName: "base colors", + renderParameters: parameters.tokens.baseColorRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateColorsTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + colorTokensGenerator, + tokensName: "colors", + renderParameters: parameters.tokens.colorRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateBorderTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + bordersTokensGenerator, + tokensName: "border", + renderParameters: parameters.tokens.bordersRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateBorderRadiusTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + borderRadiusesTokensGenerator, + tokensName: "borderRadius", + renderParameters: parameters.tokens.borderRadiusesRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateGradientTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + gradientTokensGenerator, + tokensName: "gradients", + renderParameters: parameters.tokens.gradientsRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateAnimationTimeTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + animationTimeTokensGenerator, + tokensName: "animationTime", + renderParameters: parameters.tokens.animationTimesRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateAnimationEaseTokens(parameters: TokensGenerationParameters, tokenValues: TokenValues) throws { + try generateTokens( + animationEaseTokensGenerator, + tokensName: "animationEase", + renderParameters: parameters.tokens.animationEasesRenderParameters, + tokenValues: tokenValues, + themes: parameters.themes, + fallbackTheme: parameters.fallbackTheme + ) + } + + private func generateTokens( + _ generator: BaseTokenGenerator, + tokensName: String, + renderParameters: [RenderParameters]?, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + if let renderParametersList = renderParameters { + for params in renderParametersList { + logger.info("📦 Generating \(tokensName) tokens...", highlighted: true) + try generator.generate( + renderParameters: params, + tokenValues: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + } + } + } + + // MARK: - + + func generate(configuration: TokensConfiguration) async throws { + let parameters = try await Task.detached(priority: .userInitiated) { + try self.tokensGenerationParametersResolver.resolveGenerationParameters(from: configuration) + }.value + + try await generate(parameters: parameters) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift new file mode 100644 index 0000000..a565dc2 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/DefaultTokensGenerationParametersResolver.swift @@ -0,0 +1,141 @@ +import Foundation + +final class DefaultTokensGenerationParametersResolver: TokensGenerationParametersResolver { + + // MARK: - Instance Properties + + let renderParametersResolver: RenderParametersResolver + let accessTokenResolver: AccessTokenResolver + + // MARK: - Initializers + + init( + renderParametersResolver: RenderParametersResolver, + accessTokenResolver: AccessTokenResolver + ) { + self.renderParametersResolver = renderParametersResolver + self.accessTokenResolver = accessTokenResolver + } + + // MARK: - + + // swiftlint:disable:next function_body_length + func resolveGenerationParameters(from configuration: TokensConfiguration) throws -> TokensGenerationParameters { + + let file = try configuration.file.map { fileConfiguration in + guard let accessToken = accessTokenResolver.resolveAccessToken(from: configuration.accessToken) else { + throw GenerationParametersError.invalidAccessToken + } + + return FileParameters( + key: fileConfiguration.key, + version: fileConfiguration.version, + accessToken: accessToken + ) + } + + let remoteFile = try configuration.remoteRepoConfig.map { remoteFileConfiguration in + guard + let accessToken = accessTokenResolver.resolveAccessToken(from: remoteFileConfiguration.accessToken) + else { + throw GenerationParametersError.invalidGitHubAccessToken + } + + return RemoteFileParameters( + owner: remoteFileConfiguration.owner, + repo: remoteFileConfiguration.repo, + branch: remoteFileConfiguration.branch, + filePath: remoteFileConfiguration.filePath, + accessToken: accessToken + ) + } + + let themes = configuration.themesConfiguration?.themes ?? [.light, .dark] + let fallbackTheme = configuration.themesConfiguration?.fallbackTheme ?? .light + + if file.isNil && remoteFile.isNil { + throw GenerationParametersError.invalidFileConfiguration + } + + let colorRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.colors, + defaultTemplateType: .native(name: "ColorTokens") + ) + + let baseColorRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.baseColors, + defaultTemplateType: .native(name: "BaseColorTokens") + ) + + let fontFamilyRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.fontFamilies, + defaultTemplateType: .native(name: "FontFamilyTokens") + ) + + let typographyRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.typographies, + defaultTemplateType: .native(name: "TypographyTokens") + ) + + let boxShadowRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.boxShadows, + defaultTemplateType: .native(name: "BoxShadowTokens") + ) + + let themeRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.theme, + defaultTemplateType: .native(name: "Theme") + ) + + let spacingRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.spacing, + defaultTemplateType: .native(name: "SpacingTokens") + ) + + let borderRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.borders, + defaultTemplateType: .native(name: "BorderTokens") + ) + + let borderRadiusParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.borderRadiuses, + defaultTemplateType: .native(name: "BorderRadiusTokens") + ) + + let gradientRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.gradients, + defaultTemplateType: .native(name: "GradientTokens") + ) + + let animationTimesRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.animationTimes, + defaultTemplateType: .native(name: "AnimationTimeTokens") + ) + + let animationEasesRenderParameters = renderParametersResolver.resolveRenderParameters( + templates: configuration.templates?.animationEases, + defaultTemplateType: .native(name: "AnimationEaseTokens") + ) + + return TokensGenerationParameters( + file: file, + remoteFile: remoteFile, + themes: themes, + fallbackTheme: fallbackTheme, + tokens: TokensGenerationParameters.TokensParameters( + colorRenderParameters: colorRenderParameters, + baseColorRenderParameters: baseColorRenderParameters, + fontFamilyRenderParameters: fontFamilyRenderParameters, + typographyRenderParameters: typographyRenderParameters, + boxShadowRenderParameters: boxShadowRenderParameters, + themeRenderParameters: themeRenderParameters, + spacingRenderParameters: spacingRenderParameters, + bordersRenderParameters: borderRenderParameters, + borderRadiusesRenderParameters: borderRadiusParameters, + gradientsRenderParameters: gradientRenderParameters, + animationTimesRenderParameters: animationTimesRenderParameters, + animationEasesRenderParameters: animationEasesRenderParameters + ) + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/TokensGenerationParametersResolver.swift b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/TokensGenerationParametersResolver.swift new file mode 100644 index 0000000..f3d73e5 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/GenerationParametersResolver/TokensGenerationParametersResolver.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol TokensGenerationParametersResolver { + + // MARK: - Instance Methods + + func resolveGenerationParameters(from configuration: TokensConfiguration) throws -> TokensGenerationParameters +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/AnimationEase/AnimationEaseTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationEase/AnimationEaseTokensGenerator.swift new file mode 100644 index 0000000..49ef062 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationEase/AnimationEaseTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol AnimationEaseTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/AnimationEase/DefaultAnimationEaseTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationEase/DefaultAnimationEaseTokensGenerator.swift new file mode 100644 index 0000000..9bb0795 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationEase/DefaultAnimationEaseTokensGenerator.swift @@ -0,0 +1,108 @@ +import Foundation + +struct DefaultAnimationEaseTokensGenerator: AnimationEaseTokensGenerator { + + // MARK: - Instance properties + + private let tokensResolver: TokensResolver + private let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Private instance methods + + private func makeAnimationEaseToken( + from token: TokenValue, + tokenValues: TokenValues + ) throws -> AnimationEase? { + if case .animationEaseBase(let value) = token.type { + return AnimationEase( + base: AnimationEase.Base( + path: token.name.components(separatedBy: "."), + x1: value.x1, + y1: value.y1, + x2: value.x2, + y2: value.y2 + ), + spring: nil + ) + } else if case .animationEaseSpring(let value) = token.type { + return AnimationEase( + base: nil, + spring: AnimationEase.Spring( + path: token.name.components(separatedBy: "."), + stiffness: value.stiffness, + damping: value.damping, + mass: value.mass + ) + ) + } else { + return nil + } + } + + private func makeAnimationTimeToken( + from token: TokenValue, + tokenValues: TokenValues + ) throws -> AnimationTime? { + guard case .animationTime(let value) = token.type else { + return nil + } + + return AnimationTime( + path: token.name.components(separatedBy: "."), + duration: try tokensResolver.resolveAnimationDurationValue( + value.duration, + tokenValues: tokenValues, + theme: nil + ), + durationMs: value.duration + ) + } + + // MARK: - AnimationTokensGenerator + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let timesCore = try tokenValues.core.compactMap { value in + try makeAnimationTimeToken(from: value, tokenValues: tokenValues) + } + let timesSemantic = try tokenValues.semantic.compactMap { value in + try makeAnimationTimeToken(from: value, tokenValues: tokenValues) + } + + let coreAnimationEase = try tokenValues.core.compactMap { value in + try makeAnimationEaseToken(from: value, tokenValues: tokenValues) + } + let coreAnimationEaseBase = coreAnimationEase.compactMap { $0.base } + let coreAnimationEaseSpring = coreAnimationEase.compactMap { $0.spring } + + let semanticAnimationEase = try tokenValues.semantic.compactMap { value in + try makeAnimationEaseToken(from: value, tokenValues: tokenValues) + } + let semanticAnimationEaseBase = semanticAnimationEase.compactMap { $0.base } + let semanticAnimationEaseSpring = semanticAnimationEase.compactMap { $0.spring } + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "animationTimesCore": timesCore, + "animationTimesSemantic": timesSemantic, + "coreAnimationEaseBase": coreAnimationEaseBase, + "coreAnimationEaseSpring": coreAnimationEaseSpring, + "semanticAnimationEaseBase": semanticAnimationEaseBase, + "semanticAnimationEaseSpring": semanticAnimationEaseSpring + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/AnimationTime/AnimationTimeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationTime/AnimationTimeTokensGenerator.swift new file mode 100644 index 0000000..755cf1f --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationTime/AnimationTimeTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol AnimationTimeTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/AnimationTime/DefaultAnimationTimeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationTime/DefaultAnimationTimeTokensGenerator.swift new file mode 100644 index 0000000..040885e --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/AnimationTime/DefaultAnimationTimeTokensGenerator.swift @@ -0,0 +1,63 @@ +import Foundation + +struct DefaultAnimationTimeTokensGenerator: AnimationTimeTokensGenerator { + + // MARK: - Instance properties + + private let tokensResolver: TokensResolver + private let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Private instance methods + + private func makeAnimationTimeToken( + from token: TokenValue, + tokenValues: TokenValues + ) throws -> AnimationTime? { + guard case .animationTime(let value) = token.type else { + return nil + } + + return AnimationTime( + path: token.name.components(separatedBy: "."), + duration: try tokensResolver.resolveAnimationDurationValue( + value.duration, + tokenValues: tokenValues, + theme: nil + ), + durationMs: value.duration + ) + } + + // MARK: - AnimationTokensGenerator + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let coreAnimationTimes = try tokenValues.core.compactMap { value in + try makeAnimationTimeToken(from: value, tokenValues: tokenValues) + } + + let semanticAnimationTimes = try tokenValues.semantic.compactMap { value in + try makeAnimationTimeToken(from: value, tokenValues: tokenValues) + } + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "coreAnimationTimes": coreAnimationTimes, + "semanticAnimationTimes": semanticAnimationTimes + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/BaseColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/BaseColorTokensGenerator.swift new file mode 100644 index 0000000..a72b9a7 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/BaseColorTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol BaseColorTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift new file mode 100644 index 0000000..2084589 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BaseColor/DefaultBaseColorTokensGenerator.swift @@ -0,0 +1,53 @@ +import Foundation + +final class DefaultBaseColorTokensGenerator: BaseColorTokensGenerator { + + // MARK: - Instance Properties + + let tokensResolver: TokensResolver + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + private func makeBaseColorToken(from token: TokenValue, tokenValues: TokenValues) throws -> BaseColorToken? { + guard case .color(let value) = token.type else { + return nil + } + + return BaseColorToken( + path: token.name.components(separatedBy: "."), + value: try tokensResolver.resolveHexColorValue( + value, + tokenValues: tokenValues, + theme: nil + ) + ) + } + + // MARK: - + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let colors = try tokenValues.colors.compactMap { try makeBaseColorToken(from: $0, tokenValues: tokenValues) } + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "colors": colors, + "themes": themes + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BaseTokenGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BaseTokenGenerator.swift new file mode 100644 index 0000000..01fbe5e --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BaseTokenGenerator.swift @@ -0,0 +1,13 @@ +import Foundation + +protocol BaseTokenGenerator { + + // MARK: - Instance Methods + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BorderRadius/BorderRadiusTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BorderRadius/BorderRadiusTokensGenerator.swift new file mode 100644 index 0000000..3a9b62b --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BorderRadius/BorderRadiusTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol BorderRadiusTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BorderRadius/DefaultBorderRadiusTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BorderRadius/DefaultBorderRadiusTokensGenerator.swift new file mode 100644 index 0000000..ab8164c --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BorderRadius/DefaultBorderRadiusTokensGenerator.swift @@ -0,0 +1,65 @@ +import Foundation + +struct DefaultBorderRadiusTokensGenerator: BorderRadiusTokensGenerator { + + // MARK: - Instance properties + + private let tokensResolver: TokensResolver + private let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Private instance methods + + private func makeBorderRadiusToken( + from token: TokenValue, + tokenValues: TokenValues + ) throws -> BorderRadiusToken? { + guard case .borderRadius(let value) = token.type else { + return nil + } + + return BorderRadiusToken( + path: token.name.components(separatedBy: "."), + value: try tokensResolver.resolveValue( + value, + tokenValues: tokenValues, + theme: nil + ) + ) + } + + // MARK: - BorderRadiusTokensGenerator + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let coreBorderRadius = try tokenValues.core.compactMap { value in + try makeBorderRadiusToken(from: value, tokenValues: tokenValues) + } + let semanticBorderRadiuses = try tokenValues.semantic.compactMap { value in + try makeBorderRadiusToken(from: value, tokenValues: tokenValues) + } + + let semanticBorderRadius = semanticBorderRadiuses.filter { !$0.path.contains("static") } + let staticBorderRadius = semanticBorderRadiuses.filter { $0.path.contains("static") } + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "coreBorderRadius": coreBorderRadius, + "semanticBorderRadius": semanticBorderRadius, + "staticBorderRadius": staticBorderRadius + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Borders/BorderTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Borders/BorderTokensGenerator.swift new file mode 100644 index 0000000..f142449 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Borders/BorderTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol BorderTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Borders/DefaultBorderTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Borders/DefaultBorderTokensGenerator.swift new file mode 100644 index 0000000..34b9a45 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Borders/DefaultBorderTokensGenerator.swift @@ -0,0 +1,97 @@ +import Foundation + +struct DefaultBorderTokensGenerator: BorderTokensGenerator { + + // MARK: - Instance properties + + private let tokensResolver: TokensResolver + private let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Private instance methods + + private func getBorderToken( + from token: TokenValue, + tokens: TokenValues + ) throws -> BorderToken? { + guard case .border(let value) = token.type else { + return nil + } + + return BorderToken( + path: token.name.components(separatedBy: "."), + width: try tokensResolver.resolveValue( + value.width, + tokenValues: tokens, + theme: nil + ), + style: value.style + ) + } + + private func getBorderWidth( + from token: TokenValue, + tokens: TokenValues + ) throws -> BorderWidthToken? { + guard case .borderWidth(let value) = token.type else { + return nil + } + + return BorderWidthToken( + path: token.name.components(separatedBy: "."), + value: try tokensResolver.resolveValue( + value, + tokenValues: tokens, + theme: nil + ) + ) + } + + private func extractBorderData( + from tokens: [TokenValue], + tokenValues: TokenValues + ) throws -> (borders: [BorderToken], borderWidth: [BorderWidthToken]) { + try tokens.reduce((borders: [BorderToken](), borderWidth: [BorderWidthToken]())) { partialResult, value in + var result = partialResult + + if let borderToken = try getBorderToken(from: value, tokens: tokenValues) { + result.borders.append(borderToken) + } + + if let borderWidth = try getBorderWidth(from: value, tokens: tokenValues) { + result.borderWidth.append(borderWidth) + } + + return result + } + } + + // MARK: - BorderTokensGenerator + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let coreBorderData = try extractBorderData(from: tokenValues.core, tokenValues: tokenValues) + let semanticBorderData = try extractBorderData(from: tokenValues.semantic, tokenValues: tokenValues) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "coreBorders": coreBorderData.borders, + "semanticBorders": semanticBorderData.borders, + "coreBorderWidth": coreBorderData.borderWidth, + "semanticBorderWidth": semanticBorderData.borderWidth + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/BoxShadowTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/BoxShadowTokensGenerator.swift new file mode 100644 index 0000000..9448fd4 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/BoxShadowTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol BoxShadowTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift new file mode 100644 index 0000000..602cae0 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/BoxShadow/DefaultBoxShadowTokensGenerator.swift @@ -0,0 +1,44 @@ +import Foundation + +final class DefaultBoxShadowTokensGenerator: BoxShadowTokensGenerator { + + // MARK: - Instance Properties + + let boxShadowTokensContextProvider: BoxShadowTokensContextProvider + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(boxShadowTokensContextProvider: BoxShadowTokensContextProvider, templateRenderer: TemplateRenderer) { + self.boxShadowTokensContextProvider = boxShadowTokensContextProvider + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let boxShadows = try boxShadowTokensContextProvider.fetchBoxShadowTokensContext( + from: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + + let dictBoxShadows = Dictionary(uniqueKeysWithValues: boxShadows.map { ($0.themeName, $0.value) }) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "themedBoxShadows": boxShadows, + "dictThemedBoxShadows": dictBoxShadows, + "themes": themes, + "fallbackTheme": fallbackTheme.name + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Color/ColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Color/ColorTokensGenerator.swift new file mode 100644 index 0000000..cb1f939 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Color/ColorTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol ColorTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift new file mode 100644 index 0000000..f9dac3d --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Color/DefaultColorTokensGenerator.swift @@ -0,0 +1,52 @@ +import Foundation + +final class DefaultColorTokensGenerator: ColorTokensGenerator { + + // MARK: - Instance Properties + + let templateRenderer: TemplateRenderer + let colorTokensContextProvider: ColorTokensContextProvider + + // MARK: - Initializers + + init( + templateRenderer: TemplateRenderer, + colorTokensContextProvider: ColorTokensContextProvider + ) { + self.templateRenderer = templateRenderer + self.colorTokensContextProvider = colorTokensContextProvider + } + + // MARK: - Instance Methods + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let colorsContext = try colorTokensContextProvider.extractTokenContext( + from: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + + let dictColorsContext = Dictionary( + uniqueKeysWithValues: colorsContext + .map { + ($0.themeName, $0.value) + } + ) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "themedColors": colorsContext, + "dictThemedColors": dictColorsContext, + "themes": themes, + "fallbackTheme": fallbackTheme.name + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift new file mode 100644 index 0000000..a821550 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/DefaultFontFamilyTokensGenerator.swift @@ -0,0 +1,73 @@ +import Foundation + +final class DefaultFontFamilyTokensGenerator: FontFamilyTokensGenerator { + + // MARK: - Instance Properties + + let tokensResolver: TokensResolver + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + private func makeFontFamilyTokens(tokenValues: TokenValues) throws -> [FontFamilyToken] { + try tokenValues.typography.compactMap { tokenValue in + guard case let .fontFamilies(value) = tokenValue.type else { + return nil + } + + return FontFamilyToken( + path: tokenValue.name.components(separatedBy: "."), + value: try tokensResolver.resolveValue( + value, + tokenValues: tokenValues, + theme: nil + ) + ) + } + } + + private func makeFontWightTokens(tokenValues: TokenValues) throws -> [FontWeightToken] { + try tokenValues.typography.compactMap { tokenValue in + guard case let .fontWeights(value) = tokenValue.type else { + return nil + } + + return FontWeightToken( + path: tokenValue.name.components(separatedBy: "."), + value: try tokensResolver.resolveValue( + value, + tokenValues: tokenValues, + theme: nil + ) + ) + } + } + + // MARK: - + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let fontFamilyTokens = try makeFontFamilyTokens(tokenValues: tokenValues) + let fontWeightTokens = try makeFontWightTokens(tokenValues: tokenValues) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "fontFamilies": fontFamilyTokens, + "fontWeights": fontWeightTokens + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/FontFamilyTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/FontFamilyTokensGenerator.swift new file mode 100644 index 0000000..5ecec86 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/FontFamily/FontFamilyTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol FontFamilyTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/DefaultGradientTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/DefaultGradientTokensGenerator.swift new file mode 100644 index 0000000..0940cff --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/DefaultGradientTokensGenerator.swift @@ -0,0 +1,50 @@ +import Foundation + +struct DefaultGradientTokensGenerator: GradientTokensGenerator { + + // MARK: - Instance properties + + private let templateRenderer: TemplateRenderer + private let provider: ColorTokensContextProvider + + init( + templateRenderer: TemplateRenderer, + gradientProvider: ColorTokensContextProvider + ) { + self.templateRenderer = templateRenderer + self.provider = gradientProvider + } + + // MARK: - GradientTokensGenerator + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let gradientsContext = try provider.extractTokenContext( + from: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + + let dictGradientsContext = Dictionary( + uniqueKeysWithValues: gradientsContext + .map { + ($0.themeName, $0.value) + } + ) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "themedGradients": gradientsContext, + "dictThemedGradients": dictGradientsContext, + "themes": themes, + "fallbackTheme": fallbackTheme.name + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/GradientTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/GradientTokensGenerator.swift new file mode 100644 index 0000000..0cb5b79 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Gradient/GradientTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol GradientTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift new file mode 100644 index 0000000..8c978f1 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/DefaultSpacingTokensGenerator.swift @@ -0,0 +1,55 @@ +import Foundation + +final class DefaultSpacingTokensGenerator: SpacingTokensGenerator { + + // MARK: - Instance Properties + + let tokensResolver: TokensResolver + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + private func makeSpacingToken(from token: TokenValue, tokenValues: TokenValues) throws -> SpacingToken? { + guard case .spacing(let value) = token.type else { + return nil + } + + return SpacingToken( + path: token.name.components(separatedBy: "."), + value: try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: nil) + ) + } + + // MARK: - + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let coreSpacings = try tokenValues.core.compactMap { value in + try makeSpacingToken(from: value, tokenValues: tokenValues) + } + + let semanticSpacings = try tokenValues.semantic.compactMap { value in + try makeSpacingToken(from: value, tokenValues: tokenValues) + } + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "coreSpacings": coreSpacings, + "semanticSpacings": semanticSpacings + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/SpacingTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/SpacingTokensGenerator.swift new file mode 100644 index 0000000..8eb4ef9 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Spacing/SpacingTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol SpacingTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift new file mode 100644 index 0000000..6ab74f4 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/DefaultThemeTokensGenerator.swift @@ -0,0 +1,69 @@ +import Foundation + +final class DefaultThemeTokensGenerator: ThemeTokensGenerator { + + // MARK: - Instance Properties + + let colorTokensContextProvider: ColorTokensContextProvider + let gradientTokensContextProvider: ColorTokensContextProvider + let boxShadowsContextProvider: BoxShadowTokensContextProvider + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init( + colorTokensContextProvider: ColorTokensContextProvider, + gradientTokensContextProvider: ColorTokensContextProvider, + boxShadowsContextProvider: BoxShadowTokensContextProvider, + templateRenderer: TemplateRenderer + ) { + self.colorTokensContextProvider = colorTokensContextProvider + self.gradientTokensContextProvider = gradientTokensContextProvider + self.boxShadowsContextProvider = boxShadowsContextProvider + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + func dictionaryValue(from tokens: [TokenThemeValue]) -> [String: T] { + Dictionary(uniqueKeysWithValues: tokens.map { ($0.themeName, $0.value) }) + } + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let colorsContext = try colorTokensContextProvider.extractTokenContext( + from: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + let gradientsContext = try gradientTokensContextProvider.extractTokenContext( + from: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + let boxShadowsContext = try boxShadowsContextProvider.fetchBoxShadowTokensContext( + from: tokenValues, + themes: themes, + fallbackTheme: fallbackTheme + ) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "themedColors": colorsContext, + "themedBoxShadows": boxShadowsContext, + "themedGradients": gradientsContext, + "dictThemedColors": dictionaryValue(from: colorsContext), + "dictThemedGradients": dictionaryValue(from: gradientsContext), + "dictThemedBoxShadows": dictionaryValue(from: boxShadowsContext), + "themes": themes, + "fallbackTheme": fallbackTheme.name + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift new file mode 100644 index 0000000..8814aac --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Theme/ThemeTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol ThemeTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift new file mode 100644 index 0000000..ea3d501 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/DefaultTypographyTokensGenerator.swift @@ -0,0 +1,184 @@ +import Foundation + +final class DefaultTypographyTokensGenerator: TypographyTokensGenerator { + + // MARK: - Instance Properties + + let tokensResolver: TokensResolver + let templateRenderer: TemplateRenderer + + // MARK: - Initializers + + init(tokensResolver: TokensResolver, templateRenderer: TemplateRenderer) { + self.tokensResolver = tokensResolver + self.templateRenderer = templateRenderer + } + + // MARK: - Instance Methods + + private func makeLineHeightToken( + from value: TokenTypographyValue, + tokenValues: TokenValues + ) throws -> TypographyToken.LineHeightToken { + let lineHeightValue = value.lineHeight + let lineHeightResolvedValue = try tokensResolver.resolveValue( + lineHeightValue, + tokenValues: tokenValues, + theme: nil + ) + + guard lineHeightResolvedValue.hasSuffix("%") else { + return TypographyToken.LineHeightToken( + path: lineHeightValue.components(separatedBy: "."), + value: lineHeightResolvedValue + ) + } + + let fontSize = Double( + try tokensResolver.resolveValue(value.fontSize, tokenValues: tokenValues, theme: nil) + ) + let lineHeight = Double(lineHeightResolvedValue.dropLast()).map { $0 / 100.0 } + + guard let fontSize else { + throw TypographyTokensGeneratorError(code: .failedExtractFontSize(value: value.fontSize)) + } + + guard let lineHeight else { + throw TypographyTokensGeneratorError(code: .failedExtractLetterSpacing(value: lineHeightValue)) + } + + let fontLineHeight = fontSize * lineHeight + + return TypographyToken.LineHeightToken( + path: lineHeightValue.components(separatedBy: "."), + value: String(round(fontLineHeight * 100) / 100.0) + ) + } + + private func makeLetterSpacingToken( + from value: TokenTypographyValue, + tokenValues: TokenValues + ) throws -> TypographyToken.LetterSpacingToken? { + guard let letterSpacingValue = value.letterSpacing else { + return nil + } + + let letterSpacing = Double( + try tokensResolver + .resolveValue(letterSpacingValue, tokenValues: tokenValues, theme: nil) + .removingFirst("%") + ).map { $0 / 100.0 } + + let fontSize = Double( + try tokensResolver.resolveValue(value.fontSize, tokenValues: tokenValues, theme: nil) + ) + + guard let fontSize else { + throw TypographyTokensGeneratorError(code: .failedExtractFontSize(value: value.fontSize)) + } + + guard let letterSpacing else { + throw TypographyTokensGeneratorError(code: .failedExtractLetterSpacing(value: letterSpacingValue)) + } + + let fontLetterSpacing = fontSize * letterSpacing + + return TypographyToken.LetterSpacingToken( + path: letterSpacingValue.components(separatedBy: "."), + value: String(round(fontLetterSpacing * 100) / 100.0) + ) + } + + private func makeContextToken(value: String, tokenValues: TokenValues) throws -> ContextToken { + ContextToken( + path: value.components(separatedBy: "."), + value: try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: nil) + ) + } + + private func makeTypographyToken( + tokenValue: TokenValue, + value: TokenTypographyValue, + tokenValues: TokenValues + ) throws -> TypographyToken { + TypographyToken( + path: tokenValue.name.components(separatedBy: "."), + name: tokenValue.name, + fontFamily: try makeContextToken(value: value.fontFamily, tokenValues: tokenValues), + fontWeight: try makeContextToken(value: value.fontWeight, tokenValues: tokenValues), + lineHeight: try makeLineHeightToken(from: value, tokenValues: tokenValues), + fontSize: try makeContextToken(value: value.fontSize, tokenValues: tokenValues), + letterSpacing: try makeLetterSpacingToken(from: value, tokenValues: tokenValues), + paragraphSpacing: try makeContextToken(value: value.paragraphSpacing, tokenValues: tokenValues), + paragraphIndent: try value.paragraphIndent.map { paragraphIndent in + try makeContextToken(value: paragraphIndent, tokenValues: tokenValues) + }, + textDecoration: try value.textDecoration.map { textDecoration in + try makeContextToken(value: textDecoration, tokenValues: tokenValues) + }, + fontScale: try value.fontScale.map { fontScale in + try makeContextToken(value: fontScale, tokenValues: tokenValues) + } + ) + } + + private func makeTypographyTokens(tokenValues: TokenValues) throws -> [TypographyToken] { + try tokenValues.typography.compactMap { tokenValue in + guard case let .typography(value) = tokenValue.type else { + return nil + } + + return try makeTypographyToken(tokenValue: tokenValue, value: value, tokenValues: tokenValues) + } + } + + private func structure(tokens: [TypographyToken], atPath path: [String] = []) -> [String: Any] { + var structuredTypography: [String: Any] = [:] + + if let name = path.last { + structuredTypography["name"] = name + } + + let typographies = tokens + .filter { $0.path.removingFirst("typography").count == path.count + 1 } + .sorted { $0.name.lowercased() < $1.name.lowercased() } + + if !typographies.isEmpty { + structuredTypography["typographies"] = typographies + } + + let childTypographyTokens = tokens.filter { $0.path.removingFirst("typography").count > path.count + 1 } + + let children = Dictionary(grouping: childTypographyTokens) { $0.path.removingFirst("typography")[path.count] } + .sorted { $0.key < $1.key } + .map { name, tokens in + structure(tokens: tokens, atPath: path + [name]) + } + + if !children.isEmpty { + structuredTypography["children"] = children + } + + return structuredTypography + } + + // MARK: - + + func generate( + renderParameters: RenderParameters, + tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws { + let tokens = try makeTypographyTokens(tokenValues: tokenValues) + + try templateRenderer.renderTemplate( + renderParameters.template, + to: renderParameters.destination, + context: [ + "typographies": tokens, + "structuredTypography": structure(tokens: tokens) + ] + ) + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Typography/TypographyTokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/TypographyTokensGenerator.swift new file mode 100644 index 0000000..5dd6f20 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/TypographyTokensGenerator.swift @@ -0,0 +1,3 @@ +import Foundation + +protocol TypographyTokensGenerator: BaseTokenGenerator { } diff --git a/Sources/FigmaGen/Generators/Tokens/Generators/Typography/TypographyTokensGeneratorError.swift b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/TypographyTokensGeneratorError.swift new file mode 100644 index 0000000..6d93623 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Generators/Typography/TypographyTokensGeneratorError.swift @@ -0,0 +1,27 @@ +import Foundation + +struct TypographyTokensGeneratorError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case failedExtractFontSize(value: String) + case failedExtractLetterSpacing(value: String) + } + + // MARK: - Instance Properties + + let code: Code + + // MARK: - CustomStringConvertible + + var description: String { + switch code { + case .failedExtractFontSize(let value): + return "Failed to extract font size from value: \(value)" + + case .failedExtractLetterSpacing(let value): + return "Failed to extract letter spacing from value: \(value)" + } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift new file mode 100644 index 0000000..eb56669 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProvider.swift @@ -0,0 +1,12 @@ +import Foundation + +protocol BoxShadowTokensContextProvider { + + // MARK: - Instance Methods + + func fetchBoxShadowTokensContext( + from tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws -> [TokenThemeValue<[BoxShadowToken]>] +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift new file mode 100644 index 0000000..780c938 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/BoxShadowTokensContextProviderError.swift @@ -0,0 +1,26 @@ +import Foundation + +struct BoxShadowTokensContextProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + + // MARK: - Enumeration Cases + + case valueNotFound(tokenName: String, theme: String) + } + + // MARK: - Instance Properties + + let code: Code + + // MARK: - CustomStringConvertible + + var description: String { + switch code { + case let .valueNotFound(tokenName, theme): + return "\(theme) value for token '\(tokenName)' not found" + } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift new file mode 100644 index 0000000..6565122 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/BoxShadowTokensContext/DefaultBoxShadowTokensContextProvider.swift @@ -0,0 +1,78 @@ +import Foundation + +final class DefaultBoxShadowTokensContextProvider: BoxShadowTokensContextProvider { + + // MARK: - Instance Methods + + private func resolveBoxShadowToken( + token: TokenValue, + values: TokenValues, + theme: Theme + ) throws -> BoxShadowToken? { + let tokens = values.tokens(for: theme) + + guard let themedToken = tokens.first(where: { $0.name == token.name }) else { + return nil + } + + guard case let .boxShadow(boxShadowValue) = themedToken.type else { + return nil + } + + return BoxShadowToken( + path: token.name.components(separatedBy: "."), + color: boxShadowValue.color, + type: boxShadowValue.type, + x: boxShadowValue.x, + y: boxShadowValue.y, + blur: boxShadowValue.blur, + spread: boxShadowValue.spread + ) + } + + private func makeBoxShadowToken( + from token: TokenValue, + tokenValues: TokenValues, + theme: Theme, + fallbackTheme: Theme + ) throws -> BoxShadowToken? { + guard case .boxShadow(_) = token.type else { + return nil + } + + let fallbackToken = try resolveBoxShadowToken(token: token, values: tokenValues, theme: fallbackTheme) + let resolvedToken = try resolveBoxShadowToken(token: token, values: tokenValues, theme: theme) + + guard let fallbackToken else { + logger.warning("Fallback token not found for \(token.name)", isVerbose: true) + return nil + } + + if resolvedToken.isNil { + logger.warning( + "\(theme.name) value for token '\(token.name)' not found, using \(fallbackTheme.name)", + isVerbose: true + ) + } + + return resolvedToken ?? fallbackToken + } + + // MARK: - + + func fetchBoxShadowTokensContext( + from tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws -> [TokenThemeValue<[BoxShadowToken]>] { + return try themes.map { theme in + let shadows = try tokenValues.tokens(for: fallbackTheme) + .compactMap { + try makeBoxShadowToken(from: $0, tokenValues: tokenValues, theme: theme, fallbackTheme: fallbackTheme) + } + .sorted { $0.path.joined() < $1.path.joined() } + + return TokenThemeValue(theme: theme.name, value: shadows) + } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider+Extensions.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider+Extensions.swift new file mode 100644 index 0000000..55de38d --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider+Extensions.swift @@ -0,0 +1,42 @@ +import Foundation + +extension ColorTokensContextProvider { + + func structure( + tokens: [T], + atNamePath namePath: [String] = [], + contextName: String = "tokens" + ) -> [String: Any] { + var structuredTokens: [String: Any] = [:] + + if let name = namePath.last { + structuredTokens["name"] = name + } + + if !namePath.isEmpty { + structuredTokens["path"] = namePath + } + + let filteredTokens = tokens + .filter { $0.path.count == namePath.count + 1 } + .sorted { $0.name.lowercased() < $1.name.lowercased() } + + if !filteredTokens.isEmpty { + structuredTokens[contextName] = filteredTokens + } + + let childTokens = tokens.filter { $0.path.count > namePath.count + 1 } + + let children = Dictionary(grouping: childTokens) { $0.path[namePath.count] } + .sorted { $0.key < $1.key } + .map { name, tokens in + structure(tokens: tokens, atNamePath: namePath + [name], contextName: contextName) + } + + if !children.isEmpty { + structuredTokens["children"] = children + } + + return structuredTokens + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift new file mode 100644 index 0000000..8f49706 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/ColorTokensContextProvider.swift @@ -0,0 +1,12 @@ +import Foundation + +protocol ColorTokensContextProvider { + + // MARK: - Instance Methods + + func extractTokenContext( + from tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws -> [TokenThemeValue<[String: Any]>] +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Colors/DefaultColorTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Colors/DefaultColorTokensContextProvider.swift new file mode 100644 index 0000000..b8608b5 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Colors/DefaultColorTokensContextProvider.swift @@ -0,0 +1,112 @@ +import Foundation + +final class DefaultColorTokensContextProvider: ColorTokensContextProvider { + + // MARK: - Instance Properties + + let tokensResolver: TokensResolver + + // MARK: - Initializers + + init(tokensResolver: TokensResolver) { + self.tokensResolver = tokensResolver + } + + // MARK: - Instance Methods + + private func fallbackWarning(warningPrefix: String, tokenName: String, fallbackTheme: String) { + logger.warning("\(warningPrefix) value for token '\(tokenName)' not found, using \(fallbackTheme)", isVerbose: true) + } + + private func resolveColorToken( + token: TokenValue, + theme: Theme, + tokenValues: TokenValues + ) throws -> ColorToken? { + let tokens = tokenValues.tokens(for: theme) + + guard let themeToken = tokens.first(where: { $0.name == token.name }) else { + return nil + } + + guard case .color(let themeValue) = themeToken.type else { + return nil + } + + // Resolve hex color value + let themeHexColorValue = try tokensResolver.resolveHexColorValue( + themeValue, + tokenValues: tokenValues, + theme: theme + ) + + // Resolve reference + let themeReference = try tokensResolver.resolveBaseReference(themeValue, tokenValues: tokens) + + let path = token.name.components(separatedBy: ".") + + return ColorToken( + name: token.name, + path: path, + value: themeHexColorValue, + reference: themeReference + ) + } + + func makeColorToken( + using token: TokenValue, + values: TokenValues, + theme: Theme, + fallbackTheme: Theme + ) throws -> ColorToken? { + guard case .color(let dayValue) = token.type else { + return nil + } + + let path = token.name.components(separatedBy: ".") + + guard path.first != "gradient" && !dayValue.contains("gradient") else { + return nil + } + + let fallbackToken = try resolveColorToken(token: token, theme: fallbackTheme, tokenValues: values) + let resolvedToken = try resolveColorToken(token: token, theme: theme, tokenValues: values) + + guard let fallbackToken else { + logger.warning("Fallback token not found for \(token.name)", isVerbose: true) + return nil + } + + if resolvedToken.isNil { + fallbackWarning(warningPrefix: theme.name, tokenName: token.name, fallbackTheme: fallbackTheme.name) + } + + return resolvedToken ?? fallbackToken + } + + // MARK: - + + func extractTokenContext( + from tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws -> [TokenThemeValue<[String: Any]>] { + return try themes.map { theme in + let colors = try tokenValues + .tokens(for: fallbackTheme) + .compactMap { token in + try makeColorToken( + using: token, + values: tokenValues, + theme: theme, + fallbackTheme: fallbackTheme + ) + } + + return TokenThemeValue( + theme: theme.name, + value: structure(tokens: colors, contextName: "colors") + ) + } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Gradient/DefaultGradientTokensContextProvider.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Gradient/DefaultGradientTokensContextProvider.swift new file mode 100644 index 0000000..dedc523 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Gradient/DefaultGradientTokensContextProvider.swift @@ -0,0 +1,159 @@ +import Foundation + +struct DefaultGradientTokensContextProvider: ColorTokensContextProvider { + + let tokensResolver: TokensResolver + + init(tokensResolver: TokensResolver) { + self.tokensResolver = tokensResolver + } + + // MARK: - Private methods + + private func resolvePoints(angle: CGFloat) -> (start: CGPoint, end: CGPoint)? { + let start = 3.0 * .pi / 2 + let u = start + angle + + let ucos = cos(u) + let usin = sin(u) + + let xedge = ucos > 0 ? 1.0 : 0 + let yedge = usin > 0 ? 1.0 : 0 + + let tx = ucos == 0 ? nil : (xedge - 0.5) / ucos + let ty = usin == 0 ? nil : (yedge - 0.5) / usin + + let t = [tx, ty] + .compactMap { $0 } + .filter { $0 > 0 } + .min() + + guard let t else { + // Невозможно вычислить пересечение с границей + return nil + } + + let endPoint = CGPoint( + x: round((0.5 + t * ucos) * 1000) / 1000, + y: round((0.5 + t * usin) * 1000) / 1000 + ) + let startPoint = CGPoint( + x: round((0.5 - t * ucos) * 1000) / 1000, + y: round((0.5 - t * usin) * 1000) / 1000 + ) + + return (start: startPoint, end: endPoint) + } + + private func resolveGradientToken( + for token: TokenValue, + theme: Theme, + values: TokenValues + ) throws -> LinearGradientToken? { + let tokens = values.tokens(for: theme) + + guard let themeToken = tokens.first(where: { $0.name == token.name }) else { + return nil + } + + guard case .color(let themeValue) = themeToken.type else { + return nil + } + + let gradient = try tokensResolver.resolveLinearGradientValue( + themeValue, + tokenValues: values, + theme: theme + ) + + let path = token.name.components(separatedBy: ".") + + guard let points = resolvePoints(angle: gradient.radians) else { + throw DefaultGradientTokensContextProviderError.invalidAngle(token.name) + } + + return LinearGradientToken( + path: path, + name: token.name, + stops: gradient.colorStopList.compactMap { stop in + let percentage = stop.percentage.replacingOccurrences(of: "%", with: "") + guard let percentage = Double(percentage) else { + return nil + } + + return .init( + color: stop.color.hexString, + percentage: percentage / 100 + ) + }, + startPoint: LinearGradientToken.Point( + x: points.start.x, + y: points.start.y + ), + endPoint: LinearGradientToken.Point( + x: points.end.x, + y: points.end.y + ) + ) + } + + private func makeGradientToken( + from token: TokenValue, + tokenValues: TokenValues, + theme: Theme, + fallbackTheme: Theme + ) throws -> LinearGradientToken? { + let path = token.name.components(separatedBy: ".") + + guard + case .color(let fallbackTokenValue) = token.type, + fallbackTokenValue.contains("gradient"), + path.first != "color" + else { + return nil + } + + let fallbackToken = try resolveGradientToken(for: token, theme: fallbackTheme, values: tokenValues) + let resolvedToken = try resolveGradientToken(for: token, theme: theme, values: tokenValues) + + guard let fallbackToken else { + logger.warning("Fallback token not found for \(token.name)", isVerbose: true) + return nil + } + + if resolvedToken.isNil { + logger.warning( + "\(theme.name) value for token '\(token.name)' not found, using \(fallbackTheme.name)", + isVerbose: true + ) + } + + return resolvedToken ?? fallbackToken + } + + // MARK: - GradientTokensContextProvider + + func extractTokenContext( + from tokenValues: TokenValues, + themes: [Theme], + fallbackTheme: Theme + ) throws -> [TokenThemeValue<[String: Any]>] { + return try themes.map { theme in + let gradients = try tokenValues + .tokens(for: fallbackTheme) + .compactMap { token in + try makeGradientToken( + from: token, + tokenValues: tokenValues, + theme: theme, + fallbackTheme: fallbackTheme + ) + } + + return TokenThemeValue( + theme: theme.name, + value: structure(tokens: gradients, contextName: "gradients") + ) + } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Gradient/DefaultGradientTokensContextProviderError.swift b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Gradient/DefaultGradientTokensContextProviderError.swift new file mode 100644 index 0000000..1ae5169 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Providers/ColorTokensContext/Gradient/DefaultGradientTokensContextProviderError.swift @@ -0,0 +1,15 @@ +import Foundation + +enum DefaultGradientTokensContextProviderError: Error, CustomStringConvertible { + case invalidAngle(String) + case fallbackTokenNotFound(String) + + var description: String { + switch self { + case .invalidAngle(let tokenName): + "Invalid agnle in gradient token: \(tokenName)" + case .fallbackTokenNotFound(let tokenName): + "Fallback token not found for \(tokenName)" + } + } +} diff --git a/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift b/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift new file mode 100644 index 0000000..04dd9db --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Resolver/DefaultTokensResolver.swift @@ -0,0 +1,256 @@ +import Foundation +import Expression + +final class DefaultTokensResolver: TokensResolver { + + // MARK: - Instance Methods + + private func evaluteValue(_ value: String) -> String { + let expression = AnyExpression(value) + + do { + return try expression.evaluate() + } catch { + return value + } + } + + private func colorComponent(from string: String, start: Int, length: Int) -> Double { + let startIndex = string.index(string.startIndex, offsetBy: start) + let endIndex = string.index(startIndex, offsetBy: length) + let substring = string[startIndex.. Color { + let hex = hex + .replacingOccurrences(of: "#", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + .uppercased() + + switch hex.count { + case .rgb: + return Color( + red: colorComponent(from: hex, start: 0, length: 1), + green: colorComponent(from: hex, start: 1, length: 1), + blue: colorComponent(from: hex, start: 2, length: 1), + alpha: alpha + ) + + case .rrggbb: + return Color( + red: colorComponent(from: hex, start: 0, length: 2), + green: colorComponent(from: hex, start: 2, length: 2), + blue: colorComponent(from: hex, start: 4, length: 2), + alpha: alpha + ) + + default: + throw TokensGeneratorError(code: .invalidHEXComponent(hex: hex)) + } + } + + private func resolveColorValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> Color { + if value.hasPrefix("rgba") { + return try resolveRGBAColorValue(value, tokenValues: tokenValues, theme: theme) + } + + return try makeColor(hex: value, alpha: 1.0) + } + + private func resolveRGBAWithHex( + hex: String, + alphaPercent: String, + value: String + ) throws -> Color { + let alpha = alphaPercent.replacingOccurrences(of: "%", with: "") + guard let alpha = Double(alpha) else { + throw TokensGeneratorError(code: .invalidAlphaComponent(alpha: alphaPercent + value)) + } + + return try makeColor(hex: hex, alpha: alpha / 100.0) + } + + private func resolveRGBA( + red: String, + green: String, + blue: String, + alpha: String, + rgbaValue: String + ) throws -> Color { + guard + let red = Double(red), + let green = Double(green), + let blue = Double(blue), + let alpha = Double(alpha) + else { + throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: rgbaValue)) + } + + let normalizationFactor: CGFloat = 255.0 + + return Color( + red: CGFloat(red) / normalizationFactor, + green: CGFloat(green) / normalizationFactor, + blue: CGFloat(blue) / normalizationFactor, + alpha: alpha + ) + } + + // MARK: - TokensResolver + + func resolveValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> String { + let themeTokens = tokenValues.getThemeTokenValues(theme: theme) + + let resolvedValue = try value.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in + let referenceName = String( + referenceName + .dropFirst() + .dropLast() + ) + + guard let token = themeTokens.first(where: { $0.name == referenceName }) else { + throw TokensGeneratorError(code: .referenceNotFound(name: referenceName)) + } + + guard let value = token.type.stringValue else { + throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName)) + } + + return try resolveValue(value, tokenValues: tokenValues, theme: theme) + } + + return evaluteValue(resolvedValue) + } + + func resolveBaseReference(_ reference: String, tokenValues: [TokenValue]) throws -> String { + try reference.replacingOccurrences(matchingPattern: #"\{.*?\}"#) { referenceName in + if referenceName.contains(".base.") || referenceName.contains(".service.") { + return referenceName + } + + guard referenceName.contains("color.") else { + return referenceName + } + + let referenceName = String( + referenceName + .dropFirst() + .dropLast() + ) + + guard let token = tokenValues.first(where: { $0.name == referenceName }) else { + throw TokensGeneratorError(code: .referenceNotFound(name: referenceName)) + } + + guard let value = token.type.stringValue else { + throw TokensGeneratorError(code: .unexpectedTokenValueType(name: referenceName)) + } + + return try resolveBaseReference(value, tokenValues: tokenValues) + } + } + + func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> Color { + let components = try resolveValue(value, tokenValues: tokenValues, theme: theme) + .slice(from: "(", to: ")", includingBounds: false)? + .components(separatedBy: ", ") + + guard let components else { + throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value)) + } + + switch components.count { + case .rgbaWithHex: + return try resolveRGBAWithHex( + hex: components[0], + alphaPercent: components[1], + value: value + ) + case .rgba: + return try resolveRGBA( + red: components[0], + green: components[1], + blue: components[2], + alpha: components[3], + rgbaValue: value + ) + default: + throw TokensGeneratorError(code: .invalidRGBAColorValue(rgba: value)) + } + } + + func resolveHexColorValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> String { + let resolvedValue = try resolveValue(value, tokenValues: tokenValues, theme: theme) + + if resolvedValue.hasPrefix("#") { + return resolvedValue + } + return try resolveColorValue(resolvedValue, tokenValues: tokenValues, theme: theme).hexString + } + + func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> LinearGradient { + let value = try resolveValue(value, tokenValues: tokenValues, theme: theme) + + guard let startFunctionIndex = value.firstIndex(of: "("), let endFunctionIndex = value.lastIndex(of: ")") else { + throw TokensGeneratorError(code: .failedToExtractLinearGradientParams(linearGradient: value)) + } + + let rawParams = value[value.index(after: startFunctionIndex).. Double { + let resolvedValue = try resolveValue(value, tokenValues: tokenValues, theme: theme) + guard let value = Double(resolvedValue) else { + throw TokensGeneratorError(code: .unexpectedTokenValueType(name: "duration")) + } + + return value / 1000 + } +} + +extension Int { + + // MARK: - Type Properties + + fileprivate static let rgb = 3 + fileprivate static let rrggbb = 6 + fileprivate static let rgbaWithHex = 2 + fileprivate static let rgba = 4 +} diff --git a/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift b/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift new file mode 100644 index 0000000..916ffd2 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/Resolver/TokensResolver.swift @@ -0,0 +1,105 @@ +import Foundation + +protocol TokensResolver { + + // MARK: - Instance Methods + + /// Resolving references and mathematical expressions in `value` from `tokenValues`. + /// + /// Reference example: `{core.space.1-x} + {core.space.1-x} / 2` + /// where `core.space.1-x == 1` the resolved value would be `1 + 1 / 2` + /// and after evaluating the mathematical expression, the function will return `1.5` + /// + /// - Parameters: + /// - value: String value to resolve + /// - tokenValues: All token values + /// - theme: Theme + /// - Returns: Resolved value. + func resolveValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> String + + /// Resolving `reference` from `tokenValues`. + /// + /// Example: If reference `{color.background.primary}` and `tokenValues` has token with name + /// `color.background.primary` and reference `{color.base.black}`, + /// the function will return `{color.base.black}` else `{color.background.primary}`. + /// + /// - Parameters: + /// - reference: String reference to resolve + /// - tokenValues: Tokens to search reference. Use theme specific tokens. + /// - Returns: Resolved value. + func resolveBaseReference(_ reference: String, tokenValues: [TokenValue]) throws -> String + + /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` + /// and convert `rgba()` to ``Color`` object + /// + /// Supported formats: + /// - `rgba(hex_color, alpha-value-percentage)` + /// – `rgba(red, green, blue, alpha-value-percentage)` + /// - TO DO: Support more formats + /// + /// [Color tokens examples and should be supported later](https://docs.tokens.studio/available-tokens/color-tokens#solid-colors) + /// + /// - Parameters: + /// - value: Raw `rgba()` with references, e.g.: + /// ``` + /// rgba( + /// {color.base.white}, + /// {semantic.opacity.disabled} + /// ) + /// ``` + /// - tokenValues: All token values + /// - theme: Theme + /// - Returns: ``Color`` object with values resolved from `rgba()` + func resolveRGBAColorValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> Color + + /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` + /// and convert `rgba()` to hex value + /// + /// See ``resolveRGBAColorValue(_:tokenValues:)`` for supported formats for `rgba()` + /// + /// - Parameters: + /// - value: Raw `rgba()` with references, e.g.: + /// ``` + /// rgba( + /// {color.base.white}, + /// {semantic.opacity.disabled} + /// ) + /// ``` + /// Or simple reference to another color: `{color.base.white}` + /// - tokenValues: All token values + /// - theme: Theme + /// - Returns: Hex value of the color + func resolveHexColorValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> String + + /// Resolving references and mathematical expressions in `value` using ``resolveValue(_:tokenValues:)`` + /// and convert `linear-gradient()` to ``LinearGradient`` object + /// + /// Supported formats: + /// - `linear-gradient(angle, [hex_color||rgba() length-percentage])` + /// + /// [Gradients tokens examples](https://docs.tokens.studio/available-tokens/color-tokens#gradients) + /// + /// - Parameters: + /// - value: Raw `linear-gradient()` with references, e.g: + /// ``` + /// linear-gradient( + /// 0deg, + /// rgba({color.base.red.50}, {semantic.opacity.transparent}) 0%, + /// {color.base.red.50} {semantic.opacity.visible} + /// ) + /// ``` + /// - tokenValues: All token values + /// - theme: Theme + /// - Returns: ``LinearGradient`` object with values resolved from `linear-gradient()` + func resolveLinearGradientValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> LinearGradient + + /// Resolving animation duration in `value` using ``resolveValue(_:tokenValues:)``, + /// convert `String` to `Double` value and convert milliseconds to seconds + /// + /// - Parameters: + /// - value: String value to resolve + /// - tokenValues: All token values + /// - theme: Theme + /// - Returns: Resolved value. + func resolveAnimationDurationValue(_ value: String, tokenValues: TokenValues, theme: Theme?) throws -> Double +} diff --git a/Sources/FigmaGen/Generators/Tokens/TokensGenerator.swift b/Sources/FigmaGen/Generators/Tokens/TokensGenerator.swift new file mode 100644 index 0000000..c7fa0da --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/TokensGenerator.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol TokensGenerator { + + // MARK: - Instance Methods + + func generate(configuration: TokensConfiguration) async throws +} diff --git a/Sources/FigmaGen/Generators/Tokens/TokensGeneratorError.swift b/Sources/FigmaGen/Generators/Tokens/TokensGeneratorError.swift new file mode 100644 index 0000000..e7d4542 --- /dev/null +++ b/Sources/FigmaGen/Generators/Tokens/TokensGeneratorError.swift @@ -0,0 +1,52 @@ +import Foundation + +struct TokensGeneratorError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case referenceNotFound(name: String) + case unexpectedTokenValueType(name: String) + case expressionFailed(expression: String) + case invalidRGBAColorValue(rgba: String) + case invalidAlphaComponent(alpha: String) + case invalidHEXComponent(hex: String) + case failedToExtractLinearGradientParams(linearGradient: String) + + case nightColorNotFound(tokenName: String) + } + + // MARK: - Instance Properties + + let code: Code + + // MARK: - CustomStringConvertible + + var description: String { + switch code { + case .referenceNotFound(let name): + return "Reference for token with name '\(name)' not found" + + case .unexpectedTokenValueType(let name): + return "Unexpected token value type with name '\(name)'" + + case .expressionFailed(let expression): + return "Failed to evaluate value from expression: \(expression)" + + case .invalidRGBAColorValue(let rgba): + return "Invalid rgba() value: \(rgba)" + + case .invalidAlphaComponent(let alpha): + return "Failed to convert alpha to float: \(alpha)" + + case .invalidHEXComponent(let hex): + return "Invalid hex value: \(hex)" + + case .failedToExtractLinearGradientParams(let linearGradient): + return "Failed to extract linear gradient parameters: \(linearGradient)" + + case .nightColorNotFound(let tokenName): + return "Night color for token '\(tokenName)' not found" + } + } +} diff --git a/Sources/FigmaGen/Logger/DefaultLoggerPrinter.swift b/Sources/FigmaGen/Logger/DefaultLoggerPrinter.swift new file mode 100644 index 0000000..1c33ae1 --- /dev/null +++ b/Sources/FigmaGen/Logger/DefaultLoggerPrinter.swift @@ -0,0 +1,10 @@ +import Foundation + +struct DefaultLoggerPrinter: LoggerPrinting { + + // MARK: - LoggerPrinting + + func print(_ message: String, terminator: String) { + Swift.print(message, terminator: terminator) + } +} diff --git a/Sources/FigmaGen/Logger/Logger.swift b/Sources/FigmaGen/Logger/Logger.swift new file mode 100644 index 0000000..0f5a3b9 --- /dev/null +++ b/Sources/FigmaGen/Logger/Logger.swift @@ -0,0 +1,82 @@ +import Foundation + +struct Logger { + + // MARK: - Type Properties + + static let verboseLogKey = "VERBOSE_LOG" + + // MARK: - Instance Properties + + let isSilent: Bool + let printer: LoggerPrinting + + var isVerbose: Bool { + ProcessInfo.processInfo.environment[Self.verboseLogKey] != nil + } + + // MARK: - Initializers + + init(isSilent: Bool = false, printer: LoggerPrinting = DefaultLoggerPrinter()) { + self.isSilent = isSilent + self.printer = printer + } + + // MARK: - Instance Methods + + private func print(_ message: String, terminator: String = "\n", isVerbose: Bool) { + guard !isSilent else { + return + } + + guard !isVerbose || (isVerbose && self.isVerbose) else { + return + } + + printer.print(message, terminator: terminator) + } + + // MARK: - + + func debug(_ items: Any..., separator: String = " ", terminator: String = "\n", isVerbose: Bool = true) { + let message = items.joinedDescription(separator: separator) + print(message, terminator: terminator, isVerbose: isVerbose) + } + + func info( + _ items: Any..., + separator: String = " ", + terminator: String = "\n", + highlighted: Bool = false, + isVerbose: Bool = false + ) { + let message = items.joinedDescription(separator: separator) + print(highlighted ? message.blue : message, terminator: terminator, isVerbose: isVerbose) + } + + func warning(_ items: Any..., separator: String = " ", terminator: String = "\n", isVerbose: Bool = false) { + let message = "⚠️ WARNING: \(items.joinedDescription(separator: separator))".yellow + print(message, terminator: terminator, isVerbose: isVerbose) + } + + func success(_ items: Any..., separator: String = " ", terminator: String = "\n", isVerbose: Bool = false) { + let message = "✅ \(items.joinedDescription(separator: separator))".green + print(message, terminator: terminator, isVerbose: isVerbose) + } + + func error(_ items: Any..., separator: String = " ", terminator: String = "\n", isVerbose: Bool = false) { + let message = "🛑 ERROR: \(items.joinedDescription(separator: separator))".red + print(message, terminator: terminator, isVerbose: isVerbose) + } +} + +// MARK: - + +extension Sequence { + + // MARK: - Instance Methods + + fileprivate func joinedDescription(separator: String) -> String { + map { "\($0)" }.joined(separator: separator) + } +} diff --git a/Sources/FigmaGen/Logger/LoggerPrinting.swift b/Sources/FigmaGen/Logger/LoggerPrinting.swift new file mode 100644 index 0000000..29c5936 --- /dev/null +++ b/Sources/FigmaGen/Logger/LoggerPrinting.swift @@ -0,0 +1,8 @@ +import Foundation + +protocol LoggerPrinting { + + // MARK: - Instance Methods + + func print(_ message: String, terminator: String) +} diff --git a/Sources/FigmaGen/Models/Color.swift b/Sources/FigmaGen/Models/Color.swift new file mode 100644 index 0000000..4a113b9 --- /dev/null +++ b/Sources/FigmaGen/Models/Color.swift @@ -0,0 +1,49 @@ +import Foundation + +struct Color: Codable, Hashable { + + // MARK: - Instance Properties + + let red: Double + let green: Double + let blue: Double + let alpha: Double +} + +extension Color { + + // MARK: - Type Methods + + static func == (lhs: Color, rhs: Color) -> Bool { + lhs.hexString == rhs.hexString + } + + // MARK: - Instance Properties + + var hexString: String { + let multiplier = CGFloat(255.0) + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier) + ) + } + + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier), + Int(alpha * multiplier) + ) + } + + // MARK: - Instance Methods + + func hash(into hasher: inout Hasher) { + hasher.combine(hexString) + } +} diff --git a/Sources/FigmaGen/Models/ColorStyle/ColorStyle.swift b/Sources/FigmaGen/Models/ColorStyle/ColorStyle.swift new file mode 100644 index 0000000..9f2c171 --- /dev/null +++ b/Sources/FigmaGen/Models/ColorStyle/ColorStyle.swift @@ -0,0 +1,23 @@ +import Foundation +import FigmaGenTools + +struct ColorStyle: Encodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case asset + } + + // MARK: - Instance Properties + + let node: ColorStyleNode + let asset: ColorStyleAsset? + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + try node.encode(to: encoder) + try asset?.encode(to: encoder, forKey: CodingKeys.asset) + } +} diff --git a/Sources/FigmaGen/Models/ColorStyle/ColorStyleAsset.swift b/Sources/FigmaGen/Models/ColorStyle/ColorStyleAsset.swift new file mode 100644 index 0000000..aae75c8 --- /dev/null +++ b/Sources/FigmaGen/Models/ColorStyle/ColorStyleAsset.swift @@ -0,0 +1,8 @@ +import Foundation + +struct ColorStyleAsset: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String +} diff --git a/Sources/FigmaGen/Models/ColorStyle/ColorStyleNode.swift b/Sources/FigmaGen/Models/ColorStyle/ColorStyleNode.swift new file mode 100644 index 0000000..18415ad --- /dev/null +++ b/Sources/FigmaGen/Models/ColorStyle/ColorStyleNode.swift @@ -0,0 +1,10 @@ +import Foundation + +struct ColorStyleNode: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let description: String? + let color: Color +} diff --git a/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift b/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift new file mode 100644 index 0000000..ae1e8f8 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/AccessTokenConfiguration.swift @@ -0,0 +1,51 @@ +import Foundation + +struct AccessTokenConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case environmentVariable = "env" + case keychain + } + + // MARK: - + + struct KeychainParameters: Decodable, Equatable { + + // MARK: - Instance Properties + + let service: String + let key: String + } + + // MARK: - Enumeration Cases + + let value: String? + let environmentVariable: String? + let keychainParameters: KeychainParameters? + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + if let container = try? decoder.container(keyedBy: CodingKeys.self) { + self.value = nil + self.environmentVariable = try container.decodeIfPresent(forKey: .environmentVariable) + self.keychainParameters = try container.decodeIfPresent(forKey: .keychain) + } else { + self.value = try String(from: decoder) + self.environmentVariable = nil + self.keychainParameters = nil + } + } + + init( + value: String? = nil, + environmentVariable: String? = nil, + keychainParameters: KeychainParameters? = nil + ) { + self.value = value + self.environmentVariable = environmentVariable + self.keychainParameters = keychainParameters + } +} diff --git a/Sources/FigmaGen/Models/Configuration/BaseConfiguration.swift b/Sources/FigmaGen/Models/Configuration/BaseConfiguration.swift new file mode 100644 index 0000000..a4de9e2 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/BaseConfiguration.swift @@ -0,0 +1,10 @@ +import Foundation +import FigmaGenTools + +struct BaseConfiguration: Decodable { + + // MARK: - Instance Properties + + let file: FileConfiguration? + let accessToken: AccessTokenConfiguration? +} diff --git a/Sources/FigmaGen/Models/Configuration/ColorStylesConfiguration.swift b/Sources/FigmaGen/Models/Configuration/ColorStylesConfiguration.swift new file mode 100644 index 0000000..645c5ac --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/ColorStylesConfiguration.swift @@ -0,0 +1,42 @@ +import Foundation + +struct ColorStylesConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case assets + } + + // MARK: - Instance Properties + + let generation: GenerationConfiguration + let assets: String? + + // MARK: - Initializers + + init( + generation: GenerationConfiguration, + assets: String? + ) { + self.generation = generation + self.assets = assets + } + + init(from decoder: Decoder) throws { + assets = try decoder + .container(keyedBy: CodingKeys.self) + .decodeIfPresent(forKey: .assets) + + generation = try GenerationConfiguration(from: decoder) + } + + // MARK: - Instance Methods + + func resolve(base: BaseConfiguration?) -> Self { + Self( + generation: generation.resolve(base: base), + assets: assets + ) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/Configuration.swift b/Sources/FigmaGen/Models/Configuration/Configuration.swift new file mode 100644 index 0000000..959a9dc --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/Configuration.swift @@ -0,0 +1,36 @@ +import Foundation + +struct Configuration: Decodable { + + // MARK: - Instance Properties + + let base: BaseConfiguration? + + let colorStyles: ColorStylesConfiguration? + let textStyles: TextStylesConfiguration? + let images: ImagesConfiguration? + let shadowStyles: ShadowStylesConfiguration? + let tokens: TokensConfiguration? + + // MARK: - Instance Methods + + func resolveColorStyles() -> ColorStylesConfiguration? { + colorStyles?.resolve(base: base) + } + + func resolveTextStyles() -> TextStylesConfiguration? { + textStyles?.resolve(base: base) + } + + func resolveImages() -> ImagesConfiguration? { + images?.resolve(base: base) + } + + func resolveShadowStyles() -> ShadowStylesConfiguration? { + shadowStyles?.resolve(base: base) + } + + func resolveTokens() -> TokensConfiguration? { + tokens?.resolve(base: base) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/FileConfiguration.swift b/Sources/FigmaGen/Models/Configuration/FileConfiguration.swift new file mode 100644 index 0000000..f7f04d8 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/FileConfiguration.swift @@ -0,0 +1,106 @@ +import Foundation + +struct FileConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case key + case version + case includedNodes + case excludedNodes + } + + // MARK: - Instance Properties + + let key: String + let version: String? + let includedNodes: [String]? + let excludedNodes: [String]? + + // MARK: - Initializers + + init( + key: String, + version: String?, + includedNodes: [String]?, + excludedNodes: [String]? + ) { + self.key = key + self.version = version + self.includedNodes = includedNodes + self.excludedNodes = excludedNodes + } + + init?(url: URL) { + guard url.scheme == .fileURLScheme, url.host == .fileURLHost else { + return nil + } + + guard url.pathComponents.count == .filePathComponentCount else { + return nil + } + + key = url.pathComponents[.fileKeyPathComponentIndex] + + let urlComponents = URLComponents(string: url.absoluteString) + let urlQueryItems = urlComponents.flatMap { $0.queryItems } + + if let urlQueryItems { + version = urlQueryItems + .first { $0.name == .fileURLVersionParameterName }? + .value + + includedNodes = urlQueryItems + .first { $0.name == .fileURLNodeParameterName }? + .value + .map { [$0] } + } else { + version = nil + includedNodes = nil + } + + excludedNodes = nil + } + + init(from decoder: Decoder) throws { + if let container = try? decoder.container(keyedBy: CodingKeys.self) { + self.init( + key: try container.decode(forKey: .key), + version: try container.decodeIfPresent(forKey: .version), + includedNodes: try container.decodeIfPresent(forKey: .includedNodes), + excludedNodes: try container.decodeIfPresent(forKey: .excludedNodes) + ) + } else { + let urlContainer = try decoder.singleValueContainer() + let url = try urlContainer.decode(URL.self) + + guard let configuration = Self(url: url) else { + throw DecodingError.dataCorruptedError( + in: urlContainer, + debugDescription: "'\(url)' is not a valid Figma file URL" + ) + } + + self = configuration + } + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let fileURLScheme = "https" + fileprivate static let fileURLHost = "www.figma.com" + fileprivate static let fileURLVersionParameterName = "version-id" + fileprivate static let fileURLNodeParameterName = "node-id" +} + +extension Int { + + // MARK: - Type Properties + + fileprivate static let filePathComponentCount = 4 + fileprivate static let fileKeyPathComponentIndex = 2 +} diff --git a/Sources/FigmaGen/Models/Configuration/GenerationConfiguration.swift b/Sources/FigmaGen/Models/Configuration/GenerationConfiguration.swift new file mode 100644 index 0000000..6be0f9f --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/GenerationConfiguration.swift @@ -0,0 +1,56 @@ +import Foundation +import FigmaGenTools + +struct GenerationConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case file + case accessToken + case templates + } + + // MARK: - Instance Properties + + let file: FileConfiguration? + let accessToken: AccessTokenConfiguration? + let templates: [TemplateConfiguration]? + + // MARK: - Initializers + + init( + file: FileConfiguration?, + accessToken: AccessTokenConfiguration?, + templates: [TemplateConfiguration]? + ) { + self.file = file + self.accessToken = accessToken + self.templates = templates + } + + init(from decoder: Decoder) throws { + let base = try BaseConfiguration(from: decoder) + + file = base.file + accessToken = base.accessToken + + let container = try decoder.container(keyedBy: CodingKeys.self) + + templates = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .templates)?.templates + } + + // MARK: - Instance Methods + + func resolve(base: BaseConfiguration?) -> Self { + guard let base else { + return self + } + + return Self( + file: file ?? base.file, + accessToken: accessToken ?? base.accessToken, + templates: templates + ) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift b/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift new file mode 100644 index 0000000..10b76b7 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/ImagesConfiguration.swift @@ -0,0 +1,109 @@ +import Foundation + +struct ImagesConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case assets + case resources + case postProcessor + case format + case scales + case onlyExportables + case useAbsoluteBounds + case preserveVectorData + case renderAs + case groupByFrame + case groupByComponentSet + case namingStyle + } + + // MARK: - Instance Properties + + let generatation: GenerationConfiguration + let assets: String? + let resources: String? + let postProcessor: String? + let format: ImageFormat + let scales: [ImageScale] + let onlyExportables: Bool + let useAbsoluteBounds: Bool + let preserveVectorData: Bool + let renderAs: ImageRenderingMode? + let groupByFrame: Bool + let groupByComponentSet: Bool + let namingStyle: ImageNamingStyle + + // MARK: - Initializers + + init( + generatation: GenerationConfiguration, + assets: String?, + resources: String?, + postProcessor: String?, + format: ImageFormat, + scales: [ImageScale], + onlyExportables: Bool, + useAbsoluteBounds: Bool, + preserveVectorData: Bool, + renderAs: ImageRenderingMode?, + groupByFrame: Bool, + groupByComponentSet: Bool, + namingStyle: ImageNamingStyle + ) { + self.generatation = generatation + self.assets = assets + self.resources = resources + self.postProcessor = postProcessor + self.format = format + self.scales = scales + self.onlyExportables = onlyExportables + self.useAbsoluteBounds = useAbsoluteBounds + self.preserveVectorData = preserveVectorData + self.renderAs = renderAs + self.groupByFrame = groupByFrame + self.groupByComponentSet = groupByComponentSet + self.namingStyle = namingStyle + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + assets = try container.decodeIfPresent(forKey: .assets) + resources = try container.decodeIfPresent(forKey: .resources) + + postProcessor = try container.decodeIfPresent(forKey: .postProcessor) + format = try container.decodeIfPresent(forKey: .format) ?? .pdf + scales = try container.decodeIfPresent(forKey: .scales) ?? [.none] + onlyExportables = try container.decodeIfPresent(forKey: .onlyExportables) ?? false + useAbsoluteBounds = try container.decodeIfPresent(forKey: .useAbsoluteBounds) ?? false + preserveVectorData = try container.decodeIfPresent(forKey: .preserveVectorData) ?? false + renderAs = try container.decodeIfPresent(forKey: .renderAs) + groupByFrame = try container.decodeIfPresent(forKey: .groupByFrame) ?? false + groupByComponentSet = try container.decodeIfPresent(forKey: .groupByComponentSet) ?? false + namingStyle = try container.decodeIfPresent(forKey: .namingStyle) ?? .camelCase + + generatation = try GenerationConfiguration(from: decoder) + } + + // MARK: - Instance Methods + + func resolve(base: BaseConfiguration?) -> Self { + Self( + generatation: generatation.resolve(base: base), + assets: assets, + resources: resources, + postProcessor: postProcessor, + format: format, + scales: scales, + onlyExportables: onlyExportables, + useAbsoluteBounds: useAbsoluteBounds, + preserveVectorData: preserveVectorData, + renderAs: renderAs, + groupByFrame: groupByFrame, + groupByComponentSet: groupByComponentSet, + namingStyle: namingStyle + ) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/RemoteRepoConfiguration.swift b/Sources/FigmaGen/Models/Configuration/RemoteRepoConfiguration.swift new file mode 100644 index 0000000..fbf6887 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/RemoteRepoConfiguration.swift @@ -0,0 +1,10 @@ +import Foundation + +struct RemoteRepoConfiguration: Decodable { + + let owner: String + let repo: String + let branch: String + let filePath: String + let accessToken: AccessTokenConfiguration? +} diff --git a/Sources/FigmaGen/Models/Configuration/ShadowStylesConfiguration.swift b/Sources/FigmaGen/Models/Configuration/ShadowStylesConfiguration.swift new file mode 100644 index 0000000..28dd1a1 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/ShadowStylesConfiguration.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias ShadowStylesConfiguration = GenerationConfiguration diff --git a/Sources/FigmaGen/Models/Configuration/Templates/TemplateConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Templates/TemplateConfiguration.swift new file mode 100644 index 0000000..1e0a454 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/Templates/TemplateConfiguration.swift @@ -0,0 +1,50 @@ +import Foundation +import FigmaGenTools + +struct TemplateConfiguration { + + // MARK: - Instance Properties + + let template: String? + let templateOptions: [String: Any]? + let destination: String? + + // MARK: - Initializers + + init( + template: String?, + templateOptions: [String: Any]?, + destination: String? + ) { + self.template = template + self.templateOptions = templateOptions + self.destination = destination + } +} + +// MARK: - Decodable + +extension TemplateConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case template + case templateOptions + case destination + } + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.template = try container.decodeIfPresent(forKey: .template) + + self.templateOptions = try container + .decodeIfPresent([String: AnyCodable].self, forKey: .templateOptions)? + .mapValues { $0.value } + + self.destination = try container.decodeIfPresent(forKey: .destination) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/Templates/TemplateConfigurationWrapper.swift b/Sources/FigmaGen/Models/Configuration/Templates/TemplateConfigurationWrapper.swift new file mode 100644 index 0000000..aeb54f1 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/Templates/TemplateConfigurationWrapper.swift @@ -0,0 +1,22 @@ +import Foundation + +struct TemplateConfigurationWrapper: Decodable { + + // MARK: - Instance Properties + + let templates: [TemplateConfiguration]? + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + self.templates = nil + } else if let singleValue = try? container.decode(TemplateConfiguration.self) { + self.templates = [singleValue] + } else { + self.templates = try container.decode([TemplateConfiguration].self) + } + } +} diff --git a/Sources/FigmaGen/Models/Configuration/TextStylesConfiguration.swift b/Sources/FigmaGen/Models/Configuration/TextStylesConfiguration.swift new file mode 100644 index 0000000..6e6990e --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/TextStylesConfiguration.swift @@ -0,0 +1,3 @@ +import Foundation + +typealias TextStylesConfiguration = GenerationConfiguration diff --git a/Sources/FigmaGen/Models/Configuration/Tokens/TokenThemesConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Tokens/TokenThemesConfiguration.swift new file mode 100644 index 0000000..e721d50 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/Tokens/TokenThemesConfiguration.swift @@ -0,0 +1,11 @@ +import Foundation + +struct TokenThemesConfiguration: Codable { + let themes: [Theme] + let fallbackTheme: Theme + + init(themes: [Theme], fallbackTheme: Theme) { + self.themes = themes + self.fallbackTheme = fallbackTheme + } +} diff --git a/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift new file mode 100644 index 0000000..f2f1f09 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/Tokens/TokensConfiguration.swift @@ -0,0 +1,64 @@ +import Foundation + +struct TokensConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case templates + case remoteRepoConfig + } + + // MARK: - Instance Properties + + let file: FileConfiguration? + let remoteRepoConfig: RemoteRepoConfiguration? + let accessToken: AccessTokenConfiguration? + let themesConfiguration: TokenThemesConfiguration? + let templates: TokensTemplateConfiguration? + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + let base = try BaseConfiguration(from: decoder) + + self.file = base.file + self.accessToken = base.accessToken + + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.remoteRepoConfig = try container.decodeIfPresent(forKey: .remoteRepoConfig) + self.templates = try container.decodeIfPresent(forKey: .templates) + self.themesConfiguration = try TokenThemesConfiguration(from: decoder) + } + + init( + file: FileConfiguration?, + remoteRepoConfig: RemoteRepoConfiguration?, + accessToken: AccessTokenConfiguration?, + themesConfiguration: TokenThemesConfiguration?, + templates: TokensTemplateConfiguration? + ) { + self.file = file + self.remoteRepoConfig = remoteRepoConfig + self.accessToken = accessToken + self.themesConfiguration = themesConfiguration + self.templates = templates + } + + // MARK: - Instance Methods + + func resolve(base: BaseConfiguration?) -> Self { + guard let base else { + return self + } + + return Self( + file: file ?? base.file, + remoteRepoConfig: remoteRepoConfig, + accessToken: accessToken ?? base.accessToken, + themesConfiguration: themesConfiguration, + templates: templates + ) + } +} diff --git a/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift b/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift new file mode 100644 index 0000000..1d176e2 --- /dev/null +++ b/Sources/FigmaGen/Models/Configuration/Tokens/TokensTemplateConfiguration.swift @@ -0,0 +1,76 @@ +import Foundation +import FigmaGenTools + +struct TokensTemplateConfiguration { + + // MARK: - Instance Properties + + let colors: [TemplateConfiguration]? + let baseColors: [TemplateConfiguration]? + let fontFamilies: [TemplateConfiguration]? + let typographies: [TemplateConfiguration]? + let boxShadows: [TemplateConfiguration]? + let theme: [TemplateConfiguration]? + let spacing: [TemplateConfiguration]? + let borders: [TemplateConfiguration]? + let borderRadiuses: [TemplateConfiguration]? + let gradients: [TemplateConfiguration]? + let animationTimes: [TemplateConfiguration]? + let animationEases: [TemplateConfiguration]? +} + +// MARK: - Decodable + +extension TokensTemplateConfiguration: Decodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case colors + case baseColors + case fontFamilies + case typographies + case boxShadows + case theme + case spacing + case borders + case borderRadiuses + case gradients + case animationTimes + case animationEases + } + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + colors = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .colors)?.templates + baseColors = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .baseColors)?.templates + fontFamilies = try container.decodeIfPresent( + TemplateConfigurationWrapper.self, + forKey: .fontFamilies + )?.templates + typographies = try container.decodeIfPresent( + TemplateConfigurationWrapper.self, + forKey: .typographies + )?.templates + boxShadows = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .boxShadows)?.templates + theme = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .theme)?.templates + spacing = try container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .spacing)?.templates + borders = try? container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .borders)?.templates + gradients = try? container.decodeIfPresent(TemplateConfigurationWrapper.self, forKey: .gradients)?.templates + animationTimes = try? container.decodeIfPresent( + TemplateConfigurationWrapper.self, + forKey: .animationTimes + )?.templates + animationEases = try? container.decodeIfPresent( + TemplateConfigurationWrapper.self, + forKey: .animationEases + )?.templates + borderRadiuses = try? container.decodeIfPresent( + TemplateConfigurationWrapper.self, + forKey: .borderRadiuses + )?.templates + } +} diff --git a/Sources/FigmaGen/Models/Font.swift b/Sources/FigmaGen/Models/Font.swift new file mode 100644 index 0000000..3feb001 --- /dev/null +++ b/Sources/FigmaGen/Models/Font.swift @@ -0,0 +1,28 @@ +import Foundation + +struct Font: Codable, Hashable { + + // MARK: - Instance Properties + + let family: String + let name: String + let weight: Double + let size: Double +} + +extension Font { + + // MARK: - Instance Properties + + var isSystemFont: Bool { + name.contains(String.textSystemFontName) || name.contains(String.displaySystemFontName) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let textSystemFontName = "SFProText" + fileprivate static let displaySystemFontName = "SFProDisplay" +} diff --git a/Sources/FigmaGen/Models/Images/Image.swift b/Sources/FigmaGen/Models/Images/Image.swift new file mode 100644 index 0000000..5c9ff58 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/Image.swift @@ -0,0 +1,28 @@ +import Foundation + +struct Image: Encodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case format + case asset + case resource + } + + // MARK: - Instance Properties + + let node: ImageRenderedNode + let format: ImageFormat + let asset: ImageAsset? + let resource: ImageResource? + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + try node.encode(to: encoder) + try format.encode(to: encoder, forKey: CodingKeys.format) + try asset.encode(to: encoder, forKey: CodingKeys.asset) + try resource.encode(to: encoder, forKey: CodingKeys.resource) + } +} diff --git a/Sources/FigmaGen/Models/Images/ImageAsset.swift b/Sources/FigmaGen/Models/Images/ImageAsset.swift new file mode 100644 index 0000000..99ad451 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageAsset.swift @@ -0,0 +1,11 @@ +import Foundation + +struct ImageAsset: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let filePaths: [ImageScale: String] + let preserveVectorData: Bool + let renderAs: ImageRenderingMode? +} diff --git a/Sources/FigmaGen/Models/Images/ImageComponentSetAsset.swift b/Sources/FigmaGen/Models/Images/ImageComponentSetAsset.swift new file mode 100644 index 0000000..8de0597 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageComponentSetAsset.swift @@ -0,0 +1,11 @@ +import Foundation + +struct ImageComponentSetAsset: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let parentName: String? + let assets: [ImageRenderedNode: ImageAsset] + let nodeType: ImageNodeType +} diff --git a/Sources/FigmaGen/Models/Images/ImageComponentSetNode.swift b/Sources/FigmaGen/Models/Images/ImageComponentSetNode.swift new file mode 100644 index 0000000..8270ab4 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageComponentSetNode.swift @@ -0,0 +1,27 @@ +import Foundation + +struct ImageComponentSetNode: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let parentName: String? + let components: [ImageNode] + let type: ImageNodeType + + // MARK: - Initializers + + init(name: String, parentName: String?, components: [ImageNode]) { + self.name = name + self.parentName = parentName + self.components = components + self.type = .componentSet + } + + init(name: String, parentName: String?, component: ImageNode) { + self.name = name + self.parentName = parentName + self.components = [component] + self.type = .component + } +} diff --git a/Sources/FigmaGen/Models/Images/ImageComponentSetRenderedNode.swift b/Sources/FigmaGen/Models/Images/ImageComponentSetRenderedNode.swift new file mode 100644 index 0000000..9ffbcd0 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageComponentSetRenderedNode.swift @@ -0,0 +1,11 @@ +import Foundation + +struct ImageComponentSetRenderedNode: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let parentName: String? + let components: [ImageRenderedNode] + let type: ImageNodeType +} diff --git a/Sources/FigmaGen/Models/Images/ImageFormat.swift b/Sources/FigmaGen/Models/Images/ImageFormat.swift new file mode 100644 index 0000000..f5843e4 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageFormat.swift @@ -0,0 +1,27 @@ +import Foundation + +enum ImageFormat: String, Codable { + + // MARK: - Enumeration Cases + + case pdf + case png + case jpg + case svg + + // MARK: - Instance Properties + + var fileExtension: String { + rawValue + } + + // MARK: - Instance Methods + + func assetName(for fileName: String) -> String { + fileName + } + + func resourceName(for fileName: String) -> String { + "\(fileName).\(fileExtension)" + } +} 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/Images/ImageNode.swift b/Sources/FigmaGen/Models/Images/ImageNode.swift new file mode 100644 index 0000000..74dcc5e --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageNode.swift @@ -0,0 +1,10 @@ +import Foundation + +struct ImageNode: Encodable, Hashable { + + // MARK: - Instance Properties + + let id: String + let name: String + let description: String? +} diff --git a/Sources/FigmaGen/Models/Images/ImageNodeType.swift b/Sources/FigmaGen/Models/Images/ImageNodeType.swift new file mode 100644 index 0000000..4dbbf62 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageNodeType.swift @@ -0,0 +1,9 @@ +import Foundation + +enum ImageNodeType: Encodable, Hashable { + + // MARK: - Enumeration Cases + + case component + case componentSet +} diff --git a/Sources/FigmaGen/Models/Images/ImageRenderedNode.swift b/Sources/FigmaGen/Models/Images/ImageRenderedNode.swift new file mode 100644 index 0000000..53b7707 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageRenderedNode.swift @@ -0,0 +1,22 @@ +import Foundation + +struct ImageRenderedNode: Encodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case urls + } + + // MARK: - Instance Properties + + let base: ImageNode + let urls: [ImageScale: URL] + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + try base.encode(to: encoder) + try urls.encode(to: encoder, forKey: CodingKeys.urls) + } +} diff --git a/Sources/FigmaGen/Models/Images/ImageRenderingMode.swift b/Sources/FigmaGen/Models/Images/ImageRenderingMode.swift new file mode 100644 index 0000000..c003eb2 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageRenderingMode.swift @@ -0,0 +1,9 @@ +import Foundation + +enum ImageRenderingMode: String, Codable { + + // MARK: - Enumeration Cases + + case original + case template +} diff --git a/Sources/FigmaGen/Models/Images/ImageResource.swift b/Sources/FigmaGen/Models/Images/ImageResource.swift new file mode 100644 index 0000000..9807b98 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageResource.swift @@ -0,0 +1,10 @@ +import Foundation + +struct ImageResource: Encodable, Hashable { + + // MARK: - Instance Properties + + let fileName: String + let fileExtension: String + let filePaths: [ImageScale: String] +} diff --git a/Sources/FigmaGen/Models/Images/ImageScale.swift b/Sources/FigmaGen/Models/Images/ImageScale.swift new file mode 100644 index 0000000..b4951a4 --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageScale.swift @@ -0,0 +1,26 @@ +import Foundation + +enum ImageScale: String, Codable { + + // MARK: - Enumeration Cases + + case none = "none" + case scale1x = "1" + case scale2x = "2" + case scale3x = "3" + + // MARK: - Instance Properties + + var fileNameSuffix: String { + switch self { + case .none, .scale1x: + return "" + + case .scale2x: + return "@2x" + + case .scale3x: + return "@3x" + } + } +} diff --git a/Sources/FigmaGen/Models/Images/ImageSet.swift b/Sources/FigmaGen/Models/Images/ImageSet.swift new file mode 100644 index 0000000..1f8afdb --- /dev/null +++ b/Sources/FigmaGen/Models/Images/ImageSet.swift @@ -0,0 +1,9 @@ +import Foundation + +struct ImageSet: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let images: [Image] +} diff --git a/Sources/FigmaGen/Models/LinearGradient.swift b/Sources/FigmaGen/Models/LinearGradient.swift new file mode 100644 index 0000000..c97b708 --- /dev/null +++ b/Sources/FigmaGen/Models/LinearGradient.swift @@ -0,0 +1,34 @@ +import Foundation + +struct LinearGradient: Codable, Hashable { + + // MARK: - Nested Types + + struct LinearColorStop: Codable, Hashable { + + // MARK: - Instance Properties + + let color: Color + let percentage: String + } + + // MARK: - Instance Properties + + let angle: String + let colorStopList: [LinearColorStop] +} + +extension LinearGradient { + + var radians: Double { + if angle.hasSuffix("deg"), let deg = Double(angle.replacingOccurrences(of: "deg", with: "")) { + return deg * .pi / 180 + } + + if angle.hasSuffix("rad"), let rad = Double(angle.replacingOccurrences(of: "rad", with: "")) { + return rad + } + + return Double(angle) ?? .zero + } +} diff --git a/Sources/FigmaGen/Models/Parameters/FileParameters.swift b/Sources/FigmaGen/Models/Parameters/FileParameters.swift new file mode 100644 index 0000000..97b6485 --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/FileParameters.swift @@ -0,0 +1,10 @@ +import Foundation + +struct FileParameters { + + // MARK: - Instance Properties + + let key: String + let version: String? + let accessToken: String +} diff --git a/Sources/FigmaGen/Models/Parameters/GenerationParameters.swift b/Sources/FigmaGen/Models/Parameters/GenerationParameters.swift new file mode 100644 index 0000000..da69557 --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/GenerationParameters.swift @@ -0,0 +1,10 @@ +import Foundation + +struct GenerationParameters { + + // MARK: - Instance Properties + + let file: FileParameters + let nodes: NodesParameters + let renderParameters: [RenderParameters] +} diff --git a/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift b/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift new file mode 100644 index 0000000..e46fd58 --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/ImagesParameters.swift @@ -0,0 +1,19 @@ +import Foundation + +struct ImagesParameters { + + // MARK: - Instance Properties + + let format: ImageFormat + let scales: [ImageScale] + let assets: String? + let resources: String? + let postProcessor: String? + let onlyExportables: Bool + let useAbsoluteBounds: Bool + let preserveVectorData: Bool + let renderAs: ImageRenderingMode? + let groupByFrame: Bool + let groupByComponentSet: Bool + let namingStyle: ImageNamingStyle +} diff --git a/Sources/FigmaGen/Models/Parameters/NodesParameters.swift b/Sources/FigmaGen/Models/Parameters/NodesParameters.swift new file mode 100644 index 0000000..5e5defd --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/NodesParameters.swift @@ -0,0 +1,9 @@ +import Foundation + +struct NodesParameters { + + // MARK: - Instance Properties + + let includedIDs: [String]? + let excludedIDs: [String]? +} diff --git a/Sources/FigmaGen/Models/Parameters/RemoteFileParameters.swift b/Sources/FigmaGen/Models/Parameters/RemoteFileParameters.swift new file mode 100644 index 0000000..f9872ab --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/RemoteFileParameters.swift @@ -0,0 +1,23 @@ +import Foundation + +struct RemoteFileParameters { + let owner: String + let repo: String + let branch: String + let filePath: String + let accessToken: String + + init( + owner: String, + repo: String, + branch: String, + filePath: String, + accessToken: String + ) { + self.owner = owner + self.repo = repo + self.branch = branch + self.filePath = filePath + self.accessToken = accessToken + } +} diff --git a/Sources/FigmaGen/Models/Parameters/RenderParameters.swift b/Sources/FigmaGen/Models/Parameters/RenderParameters.swift new file mode 100644 index 0000000..d9f42ea --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/RenderParameters.swift @@ -0,0 +1,9 @@ +import Foundation + +struct RenderParameters { + + // MARK: - Instance Properties + + let template: RenderTemplate + let destination: RenderDestination +} diff --git a/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift new file mode 100644 index 0000000..360755c --- /dev/null +++ b/Sources/FigmaGen/Models/Parameters/TokensGenerationParameters.swift @@ -0,0 +1,32 @@ +import Foundation + +struct TokensGenerationParameters { + + // MARK: - Nested Types + + struct TokensParameters { + + // MARK: - Instance Properties + + let colorRenderParameters: [RenderParameters]? + let baseColorRenderParameters: [RenderParameters]? + let fontFamilyRenderParameters: [RenderParameters]? + let typographyRenderParameters: [RenderParameters]? + let boxShadowRenderParameters: [RenderParameters]? + let themeRenderParameters: [RenderParameters]? + let spacingRenderParameters: [RenderParameters]? + let bordersRenderParameters: [RenderParameters]? + let borderRadiusesRenderParameters: [RenderParameters]? + let gradientsRenderParameters: [RenderParameters]? + let animationTimesRenderParameters: [RenderParameters]? + let animationEasesRenderParameters: [RenderParameters]? + } + + // MARK: - Instance Properties + + let file: FileParameters? + let remoteFile: RemoteFileParameters? + let themes: [Theme] + let fallbackTheme: Theme + let tokens: TokensParameters +} diff --git a/Sources/FigmaGen/Models/ShadowStyle/Shadow.swift b/Sources/FigmaGen/Models/ShadowStyle/Shadow.swift new file mode 100644 index 0000000..759354c --- /dev/null +++ b/Sources/FigmaGen/Models/ShadowStyle/Shadow.swift @@ -0,0 +1,12 @@ +import Foundation + +struct Shadow: Encodable, Hashable { + + // MARK: - Instance Properties + + let type: ShadowType + let offset: Vector + let radius: Double + let color: Color + let blendMode: String? +} diff --git a/Sources/FigmaGen/Models/ShadowStyle/ShadowStyle.swift b/Sources/FigmaGen/Models/ShadowStyle/ShadowStyle.swift new file mode 100644 index 0000000..fd29a20 --- /dev/null +++ b/Sources/FigmaGen/Models/ShadowStyle/ShadowStyle.swift @@ -0,0 +1,15 @@ +import Foundation +import FigmaGenTools + +struct ShadowStyle: Encodable, Hashable { + + // MARK: - Instance Properties + + let node: ShadowStyleNode + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + try node.encode(to: encoder) + } +} diff --git a/Sources/FigmaGen/Models/ShadowStyle/ShadowStyleNode.swift b/Sources/FigmaGen/Models/ShadowStyle/ShadowStyleNode.swift new file mode 100644 index 0000000..e5699d1 --- /dev/null +++ b/Sources/FigmaGen/Models/ShadowStyle/ShadowStyleNode.swift @@ -0,0 +1,10 @@ +import Foundation + +struct ShadowStyleNode: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let description: String? + let shadows: [Shadow] +} diff --git a/Sources/FigmaGen/Models/ShadowStyle/ShadowType.swift b/Sources/FigmaGen/Models/ShadowStyle/ShadowType.swift new file mode 100644 index 0000000..558955b --- /dev/null +++ b/Sources/FigmaGen/Models/ShadowStyle/ShadowType.swift @@ -0,0 +1,6 @@ +import Foundation + +enum ShadowType: String, Encodable { + case drop + case inner +} diff --git a/Sources/FigmaGen/Models/TextStyle/TextStyle.swift b/Sources/FigmaGen/Models/TextStyle/TextStyle.swift new file mode 100644 index 0000000..e439565 --- /dev/null +++ b/Sources/FigmaGen/Models/TextStyle/TextStyle.swift @@ -0,0 +1,23 @@ +import Foundation +import FigmaGenTools + +struct TextStyle: Encodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case color + } + + // MARK: - Instance Properties + + let node: TextStyleNode + let color: TextStyleColor + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + try node.encode(to: encoder) + try color.encode(to: encoder, forKey: CodingKeys.color) + } +} diff --git a/Sources/FigmaGen/Models/TextStyle/TextStyleColor.swift b/Sources/FigmaGen/Models/TextStyle/TextStyleColor.swift new file mode 100644 index 0000000..92ff60d --- /dev/null +++ b/Sources/FigmaGen/Models/TextStyle/TextStyleColor.swift @@ -0,0 +1,9 @@ +import Foundation + +struct TextStyleColor: Encodable, Hashable { + + // MARK: - Instance Properties + + let styleName: String? + let color: Color +} diff --git a/Sources/FigmaGen/Models/TextStyle/TextStyleNode.swift b/Sources/FigmaGen/Models/TextStyle/TextStyleNode.swift new file mode 100644 index 0000000..b549178 --- /dev/null +++ b/Sources/FigmaGen/Models/TextStyle/TextStyleNode.swift @@ -0,0 +1,16 @@ +import Foundation + +struct TextStyleNode: Encodable, Hashable { + + // MARK: - Instance Properties + + let name: String + let description: String? + let font: Font + let strikethrough: Bool + let underline: Bool + let paragraphSpacing: Double? + let paragraphIndent: Double? + let lineHeight: Double? + let letterSpacing: Double? +} diff --git a/Sources/FigmaGen/Models/Token/Theme.swift b/Sources/FigmaGen/Models/Token/Theme.swift new file mode 100644 index 0000000..f45f279 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/Theme.swift @@ -0,0 +1,31 @@ +import Foundation + +struct Theme: Codable, Hashable { + + enum CodingKeys: CodingKey { + case name + } + + let name: String + + init(_ name: String) { + self.name = name + } + + init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + + if let name = try? container.decode(String.self) { + self.name = name + } else { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + } + } +} + +extension Theme { + + static let light = Theme("light") + static let dark = Theme("dark") +} diff --git a/Sources/FigmaGen/Models/Token/TokenAnimationEaseBaseValue.swift b/Sources/FigmaGen/Models/Token/TokenAnimationEaseBaseValue.swift new file mode 100644 index 0000000..dac1956 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenAnimationEaseBaseValue.swift @@ -0,0 +1,11 @@ +import Foundation + +struct TokenAnimationEaseBaseValue: Codable, Hashable { + + // MARK: - Instance Properties + + let x1: String + let y1: String + let x2: String + let y2: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenAnimationEaseSpringValue.swift b/Sources/FigmaGen/Models/Token/TokenAnimationEaseSpringValue.swift new file mode 100644 index 0000000..b137c2d --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenAnimationEaseSpringValue.swift @@ -0,0 +1,10 @@ +import Foundation + +struct TokenAnimationEaseSpringValue: Codable, Hashable { + + // MARK: - Instance Properties + + let stiffness: String + let damping: String + let mass: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenAnimationTimeValue.swift b/Sources/FigmaGen/Models/Token/TokenAnimationTimeValue.swift new file mode 100644 index 0000000..40e3bfb --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenAnimationTimeValue.swift @@ -0,0 +1,8 @@ +import Foundation + +struct TokenAnimationTimeValue: Codable, Hashable { + + // MARK: - Instance Properties + + let duration: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenAnimationValue.swift b/Sources/FigmaGen/Models/Token/TokenAnimationValue.swift new file mode 100644 index 0000000..22a1871 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenAnimationValue.swift @@ -0,0 +1,12 @@ +import Foundation + +struct TokenAnimationValue: Codable, Hashable { + + // MARK: - Instance Properties + + let x1: String + let y1: String + let x2: String + let y2: String + let duration: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenBorderValue.swift b/Sources/FigmaGen/Models/Token/TokenBorderValue.swift new file mode 100644 index 0000000..f689c33 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenBorderValue.swift @@ -0,0 +1,9 @@ +import Foundation + +struct TokenBorderValue: Codable, Hashable { + + // MARK: - Instance Properties + + let width: String + let style: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenBoxShadowValue.swift b/Sources/FigmaGen/Models/Token/TokenBoxShadowValue.swift new file mode 100644 index 0000000..e507aa7 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenBoxShadowValue.swift @@ -0,0 +1,13 @@ +import Foundation + +struct TokenBoxShadowValue: Codable, Hashable { + + // MARK: - Instance Properties + + let color: String + let type: String + let x: String + let y: String + let blur: String + let spread: String +} diff --git a/Sources/FigmaGen/Models/Token/TokenTypographyValue.swift b/Sources/FigmaGen/Models/Token/TokenTypographyValue.swift new file mode 100644 index 0000000..4121fc4 --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenTypographyValue.swift @@ -0,0 +1,17 @@ +import Foundation + +struct TokenTypographyValue: Codable, Hashable { + + // MARK: - Instance Properties + + let fontFamily: String + let fontWeight: String + let lineHeight: String + let fontSize: String + let letterSpacing: String? + let paragraphSpacing: String + let paragraphIndent: String? + let textCase: String? + let textDecoration: String? + let fontScale: String? +} diff --git a/Sources/FigmaGen/Models/Token/TokenValue.swift b/Sources/FigmaGen/Models/Token/TokenValue.swift new file mode 100644 index 0000000..82e542b --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenValue.swift @@ -0,0 +1,238 @@ +import Foundation + +struct TokenValue: Hashable { + + // MARK: - Instance Properties + + let type: TokenValueType + let name: String +} + +// MARK: - Decodable + +extension TokenValue: Decodable { + + // MARK: - Nested Types + + private enum RawType: String, Decodable { + case a11yScales + case animation + case animationEaseBase + case animationEaseSpring + case animationTime + case border + case borderRadius + case borderWidth + case boxShadow + case color + case core + case dimension + case fontFamilies + case fontSizes + case fontWeights + case letterSpacing + case lineHeights + case opacity + case paragraphSpacing + case scaling + case sizing + case spacing + case textCase + case textDecoration + case typography + + case unknown + } + + fileprivate enum CodingKeys: String, CodingKey { + case type + case name + case value + } + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + // swiftlint:disable:previous function_body_length + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.name = try container.decode(forKey: .name) + + let rawTypeValue = try container.decode(String.self, forKey: .type) + let rawType = RawType(rawValue: rawTypeValue) ?? .unknown + + switch rawType { + case .a11yScales: + self.type = .a11yScales(value: try container.decode(forKey: .value)) + + case .animation: + self.type = .animation(value: try container.decode(forKey: .value)) + + case .animationEaseBase: + self.type = .animationEaseBase(value: try container.decode(forKey: .value)) + + case .animationEaseSpring: + self.type = .animationEaseSpring(value: try container.decode(forKey: .value)) + + case .animationTime: + self.type = .animationTime(value: try container.decode(forKey: .value)) + + case .border: + self.type = .border(value: try container.decode(forKey: .value)) + + case .borderRadius: + self.type = .borderRadius(value: try container.decode(forKey: .value)) + + case .borderWidth: + self.type = .borderWidth(value: try container.decode(forKey: .value)) + + case .boxShadow: + self.type = .boxShadow(value: try container.decode(forKey: .value)) + + case .color: + self.type = .color(value: try container.decode(forKey: .value)) + + case .core: + self.type = .core(value: try container.decode(forKey: .value)) + + case .dimension: + self.type = .dimension(value: try container.decode(forKey: .value)) + + case .fontFamilies: + self.type = .fontFamilies(value: try container.decode(forKey: .value)) + + case .fontSizes: + self.type = .fontSizes(value: try container.decode(forKey: .value)) + + case .fontWeights: + self.type = .fontWeights(value: try container.decode(forKey: .value)) + + case .letterSpacing: + self.type = .letterSpacing(value: try container.decode(forKey: .value)) + + case .lineHeights: + self.type = .lineHeights(value: try container.decode(forKey: .value)) + + case .opacity: + self.type = .opacity(value: try container.decode(forKey: .value)) + + case .paragraphSpacing: + self.type = .paragraphSpacing(value: try container.decode(forKey: .value)) + + case .scaling: + self.type = .scaling(value: try container.decode(forKey: .value)) + + case .sizing: + self.type = .sizing(value: try container.decode(forKey: .value)) + + case .spacing: + self.type = .spacing(value: try container.decode(forKey: .value)) + + case .textCase: + self.type = .textCase(value: try container.decode(forKey: .value)) + + case .textDecoration: + self.type = .textDecoration(value: try container.decode(forKey: .value)) + + case .typography: + self.type = .typography(value: try container.decode(forKey: .value)) + + case .unknown: + self.type = .unknown + } + } +} + +// MARK: - Encodable + +extension TokenValue: Encodable { + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + // swiftlint:disable:previous function_body_length + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(name, forKey: .name) + + switch type { + case let .a11yScales(value): + try container.encode(value, forKey: .value) + + case let .animation(value): + try container.encode(value, forKey: .value) + + case let .animationEaseBase(value): + try container.encode(value, forKey: .value) + + case let .animationEaseSpring(value): + try container.encode(value, forKey: .value) + + case let .animationTime(value): + try container.encode(value, forKey: .value) + + case let .border(value): + try container.encode(value, forKey: .value) + + case let .borderRadius(value): + try container.encode(value, forKey: .value) + + case let .borderWidth(value): + try container.encode(value, forKey: .value) + + case let .boxShadow(value): + try container.encode(value, forKey: .value) + + case let .color(value): + try container.encode(value, forKey: .value) + + case let .core(value): + try container.encode(value, forKey: .value) + + case let .dimension(value): + try container.encode(value, forKey: .value) + + case let .fontFamilies(value): + try container.encode(value, forKey: .value) + + case let .fontSizes(value): + try container.encode(value, forKey: .value) + + case let .fontWeights(value): + try container.encode(value, forKey: .value) + + case let .letterSpacing(value): + try container.encode(value, forKey: .value) + + case let .lineHeights(value): + try container.encode(value, forKey: .value) + + case let .opacity(value): + try container.encode(value, forKey: .value) + + case let .paragraphSpacing(value): + try container.encode(value, forKey: .value) + + case let .scaling(value): + try container.encode(value, forKey: .value) + + case let .sizing(value): + try container.encode(value, forKey: .value) + + case let .spacing(value): + try container.encode(value, forKey: .value) + + case let .textCase(value): + try container.encode(value, forKey: .value) + + case let .textDecoration(value): + try container.encode(value, forKey: .value) + + case let .typography(value): + try container.encode(value, forKey: .value) + + case .unknown: + try container.encodeNil(forKey: .value) + } + } +} diff --git a/Sources/FigmaGen/Models/Token/TokenValueType.swift b/Sources/FigmaGen/Models/Token/TokenValueType.swift new file mode 100644 index 0000000..3c13ffd --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenValueType.swift @@ -0,0 +1,64 @@ +import Foundation + +enum TokenValueType: Hashable { + + // MARK: - Enumeration Cases + + case a11yScales(value: String) + case animation(value: TokenAnimationValue) + case animationEaseBase(value: TokenAnimationEaseBaseValue) + case animationEaseSpring(value: TokenAnimationEaseSpringValue) + case animationTime(value: TokenAnimationTimeValue) + case border(value: TokenBorderValue) + case borderRadius(value: String) + case borderWidth(value: String) + case boxShadow(value: TokenBoxShadowValue) + case color(value: String) + case core(value: String) + case dimension(value: String) + case fontFamilies(value: String) + case fontSizes(value: String) + case fontWeights(value: String) + case letterSpacing(value: String) + case lineHeights(value: String) + case opacity(value: String) + case paragraphSpacing(value: String) + case scaling(value: String) + case sizing(value: String) + case spacing(value: String) + case textCase(value: String) + case textDecoration(value: String) + case typography(value: TokenTypographyValue) + + case unknown + + // MARK: - Instance Properties + + var stringValue: String? { + switch self { + case let .a11yScales(value), + let .borderWidth(value), + let .borderRadius(value), + let .color(value), + let .core(value), + let .dimension(value), + let .fontFamilies(value), + let .fontSizes(value), + let .fontWeights(value), + let .letterSpacing(value), + let .lineHeights(value), + let .opacity(value), + let .paragraphSpacing(value), + let .scaling(value), + let .sizing(value), + let .spacing(value), + let .textCase(value), + let .textDecoration(value): + return value + + case .animation, .animationEaseBase, .animationEaseSpring, .animationTime, + .boxShadow, .typography, .unknown, .border: + return nil + } + } +} diff --git a/Sources/FigmaGen/Models/Token/TokenValues.swift b/Sources/FigmaGen/Models/Token/TokenValues.swift new file mode 100644 index 0000000..27b74ca --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokenValues.swift @@ -0,0 +1,67 @@ +import Foundation + +struct TokenValues: Hashable { + + // MARK: - Instance Properties + + let core: [TokenValue] + let semantic: [TokenValue] + let colors: [TokenValue] + let typography: [TokenValue] + let themedTokens: [Theme: [TokenValue]] +} + +// MARK: - Codable + +extension TokenValues: Codable { + + private enum CodingKeys: String, CodingKey, CaseIterable { + + // MARK: - Enumeration Cases + + case core + case semantic + case colors + case typography + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let staticKeys = CodingKeys.allCases.map { $0.rawValue } + let themedContainer = try decoder.container(keyedBy: AnyKey.self) + + self.core = try container.decode(forKey: .core) + self.semantic = try container.decode(forKey: .semantic) + self.colors = try container.decode(forKey: .colors) + self.typography = try container.decode(forKey: .typography) + self.themedTokens = Dictionary( + uniqueKeysWithValues: try themedContainer + .allKeys + .filter { key in + !staticKeys.contains(key.stringValue) + } + .map { key in + let tokens = try themedContainer.decode([TokenValue].self, forKey: key) + let theme = Theme(key.stringValue) + return (theme, tokens) + } + ) + } +} + +extension TokenValues { + + func tokens(for theme: Theme) -> [TokenValue] { + themedTokens[theme] ?? [] + } + + /// Возвращает набор токенов для определенной темы. + /// Для undefined возвращается полный набор токенов. Нужен для Spacer, Font и других независимых от темы параметров. + func getThemeTokenValues(theme: Theme?) -> [TokenValue] { + let allThemedTokens = themedTokens.values.flatMap { $0 } + let themeTokens = theme.map { themedTokens[$0] ?? [] } ?? allThemedTokens + let tokens = [core, semantic, colors, typography, themeTokens] + + return tokens.flatMap { $0 } + } +} diff --git a/Sources/FigmaGen/Models/Token/TokensStudioPluginData.swift b/Sources/FigmaGen/Models/Token/TokensStudioPluginData.swift new file mode 100644 index 0000000..96724eb --- /dev/null +++ b/Sources/FigmaGen/Models/Token/TokensStudioPluginData.swift @@ -0,0 +1,26 @@ +import Foundation + +struct TokensStudioPluginData: Codable, Hashable { + + // MARK: - Nested Types + + struct Tokens: Codable, Hashable { + + // MARK: - Instance Properties + + let version: String + let values: String + let usedTokenSet: String + let updatedAt: String + let activeTheme: String + let themes: String + let collapsedTokenSets: String + let checkForChanges: String? + let persistentNodesCache: String? + let storageType: String? + } + + // MARK: - Instance Properties + + let tokens: Tokens +} diff --git a/Sources/FigmaGen/Models/Vector.swift b/Sources/FigmaGen/Models/Vector.swift new file mode 100644 index 0000000..3a64479 --- /dev/null +++ b/Sources/FigmaGen/Models/Vector.swift @@ -0,0 +1,9 @@ +import Foundation + +struct Vector: Codable, Hashable { + + // MARK: - Instance Properties + + let x: Double + let y: Double +} diff --git a/Sources/FigmaGen/Providers/Assets/AssetsProvider.swift b/Sources/FigmaGen/Providers/Assets/AssetsProvider.swift new file mode 100644 index 0000000..069bbe4 --- /dev/null +++ b/Sources/FigmaGen/Providers/Assets/AssetsProvider.swift @@ -0,0 +1,10 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +protocol AssetsProvider { + + // MARK: - Instance Methods + + func saveAssetFolder(_ folder: AssetFolder, in folderPath: String) -> Promise +} diff --git a/Sources/FigmaGen/Providers/Assets/DefaultAssetsProvider.swift b/Sources/FigmaGen/Providers/Assets/DefaultAssetsProvider.swift new file mode 100644 index 0000000..1c60a20 --- /dev/null +++ b/Sources/FigmaGen/Providers/Assets/DefaultAssetsProvider.swift @@ -0,0 +1,28 @@ +import Foundation +import FigmaGenTools +import PromiseKit +import PathKit + +final class DefaultAssetsProvider: AssetsProvider { + + // MARK: - Instance Methods + + private func saveAssetFolder(_ folder: AssetFolder, in folderPath: Path) throws { + try folder.save(in: folderPath.string) + } + + // MARK: - + + func saveAssetFolder(_ folder: AssetFolder, in folderPath: String) -> Promise { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + try self.saveAssetFolder(folder, in: Path(folderPath)) + } + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let assetsExtension = "xcassets" +} diff --git a/Sources/FigmaGen/Providers/ColorStyles/Assets/ColorStyleAssetsProvider.swift b/Sources/FigmaGen/Providers/ColorStyles/Assets/ColorStyleAssetsProvider.swift new file mode 100644 index 0000000..f349c6a --- /dev/null +++ b/Sources/FigmaGen/Providers/ColorStyles/Assets/ColorStyleAssetsProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ColorStyleAssetsProvider { + + // MARK: - Instance Methods + + func saveColorStyles(nodes: [ColorStyleNode], in folderPath: String) -> Promise<[ColorStyleNode: ColorStyleAsset]> +} diff --git a/Sources/FigmaGen/Providers/ColorStyles/Assets/DefaultColorStyleAssetsProvider.swift b/Sources/FigmaGen/Providers/ColorStyles/Assets/DefaultColorStyleAssetsProvider.swift new file mode 100644 index 0000000..1de8285 --- /dev/null +++ b/Sources/FigmaGen/Providers/ColorStyles/Assets/DefaultColorStyleAssetsProvider.swift @@ -0,0 +1,76 @@ +import Foundation +import FigmaGenTools +import PromiseKit +import PathKit + +final class DefaultColorStyleAssetsProvider: ColorStyleAssetsProvider { + + // MARK: - Instance Properties + + let assetsProvider: AssetsProvider + + // MARK: - Initializers + + init(assetsProvider: AssetsProvider) { + self.assetsProvider = assetsProvider + } + + // MARK: - Instance Methods + + private func makeAsset(for node: ColorStyleNode) -> ColorStyleAsset { + ColorStyleAsset(name: node.name.camelized) + } + + private func makeAssets(for nodes: [ColorStyleNode]) -> [ColorStyleNode: ColorStyleAsset] { + var assets: [ColorStyleNode: ColorStyleAsset] = [:] + + nodes.forEach { node in + assets[node] = makeAsset(for: node) + } + + return assets + } + + private func makeAssetColorSet(for node: ColorStyleNode) -> AssetColorSet { + let color = node.color + + let assetColorComponents = AssetColorComponents( + red: color.red, + green: color.green, + blue: color.blue, + alpha: color.alpha + ) + + let assetColor = AssetColor(custom: .sRGB(components: assetColorComponents)) + + let assetColorSetContents = AssetColorSetContents( + info: .defaultFigmaGen, + colors: [assetColor] + ) + + return AssetColorSet(contents: assetColorSetContents) + } + + private func makeAssetColorSets(for assets: [ColorStyleNode: ColorStyleAsset]) -> [String: AssetColorSet] { + assets.reduce(into: [:]) { result, asset in + result[asset.value.name] = makeAssetColorSet(for: asset.key) + } + } + + // MARK: - + + func saveColorStyles(nodes: [ColorStyleNode], in folderPath: String) -> Promise<[ColorStyleNode: ColorStyleAsset]> { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + self.makeAssets(for: nodes) + }.nest { assets in + perform(on: DispatchQueue.global(qos: .userInitiated)) { + AssetFolder( + colorSets: self.makeAssetColorSets(for: assets), + contents: AssetFolderContents(info: .defaultFigmaGen) + ) + }.then { folder in + self.assetsProvider.saveAssetFolder(folder, in: folderPath) + } + } + } +} diff --git a/Sources/FigmaGen/Providers/ColorStyles/ColorStylesProvider.swift b/Sources/FigmaGen/Providers/ColorStyles/ColorStylesProvider.swift new file mode 100644 index 0000000..5dddb97 --- /dev/null +++ b/Sources/FigmaGen/Providers/ColorStyles/ColorStylesProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ColorStylesProvider { + + // MARK: - Instance Methods + + func fetchColorStyles(from file: FileParameters, nodes: NodesParameters, assets: String?) -> Promise<[ColorStyle]> +} diff --git a/Sources/FigmaGen/Providers/ColorStyles/ColorStylesProviderError.swift b/Sources/FigmaGen/Providers/ColorStyles/ColorStylesProviderError.swift new file mode 100644 index 0000000..423ef84 --- /dev/null +++ b/Sources/FigmaGen/Providers/ColorStyles/ColorStylesProviderError.swift @@ -0,0 +1,35 @@ +import Foundation + +struct ColorStylesProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case styleNotFound + case invalidStyleName + case colorNotFound + } + + // MARK: - Instance Properties + + let code: Code + let nodeID: String + let nodeName: String? + + // MARK: - CustomStringConvertible + + var description: String { + let nodeName = self.nodeName ?? "nil" + + switch code { + case .styleNotFound: + return "Figma file does not contain a valid color style for node \(nodeName) ('\(nodeID)')" + + case .invalidStyleName: + return "Style name of node \(nodeName) ('\(nodeID)') is either empty or nil" + + case .colorNotFound: + return "Style color of node \(nodeName) ('\(nodeID)') could not be found" + } + } +} diff --git a/Sources/FigmaGen/Providers/ColorStyles/DefaultColorStylesProvider.swift b/Sources/FigmaGen/Providers/ColorStyles/DefaultColorStylesProvider.swift new file mode 100644 index 0000000..877de8f --- /dev/null +++ b/Sources/FigmaGen/Providers/ColorStyles/DefaultColorStylesProvider.swift @@ -0,0 +1,111 @@ +import Foundation +import PromiseKit + +final class DefaultColorStylesProvider: ColorStylesProvider { + + // MARK: - Instance Properties + + let filesProvider: FigmaFilesProvider + let nodesProvider: FigmaNodesProvider + let colorStyleAssetsProvider: ColorStyleAssetsProvider + + // MARK: - Initializers + + init( + filesProvider: FigmaFilesProvider, + nodesProvider: FigmaNodesProvider, + colorStyleAssetsProvider: ColorStyleAssetsProvider + ) { + self.filesProvider = filesProvider + self.nodesProvider = nodesProvider + self.colorStyleAssetsProvider = colorStyleAssetsProvider + } + + // MARK: - Instance Methods + + private func extractColorStyleNode(from node: FigmaNode, styles: [String: FigmaStyle]) throws -> ColorStyleNode? { + guard let nodeInfo = node.vectorInfo, let nodeStyleID = nodeInfo.styleID(of: .fill) else { + return nil + } + + let nodeFills = nodeInfo.fills?.filter { nodeFill in + (nodeFill.isVisible ?? true) && (nodeFill.type == .solid) + } ?? [] + + guard let nodeFill = nodeFills.first, nodeFills.count == 1 else { + return nil + } + + guard let nodeFillColor = nodeFill.color else { + throw ColorStylesProviderError(code: .colorNotFound, nodeID: node.id, nodeName: node.name) + } + + guard let nodeStyle = styles[nodeStyleID], nodeStyle.type == .fill else { + throw ColorStylesProviderError(code: .styleNotFound, nodeID: node.id, nodeName: node.name) + } + + guard let nodeStyleName = nodeStyle.name, !nodeStyleName.isEmpty else { + throw ColorStylesProviderError(code: .invalidStyleName, nodeID: node.id, nodeName: node.name) + } + + return ColorStyleNode( + name: nodeStyleName, + description: nodeStyle.description, + color: Color( + red: nodeFillColor.red, + green: nodeFillColor.green, + blue: nodeFillColor.blue, + alpha: nodeFill.opacity ?? nodeFillColor.alpha + ) + ) + } + + private func extractColorStyleNodes(from nodes: [FigmaNode], of file: FigmaFile) throws -> [ColorStyleNode] { + let styles = file.styles ?? [:] + + return try nodes + .lazy + .filter { $0.isVisible ?? true } + .compactMap { try extractColorStyleNode(from: $0, styles: styles) } + .reduce(into: []) { result, node in + if !result.contains(node) { + result.append(node) + } + } + } + + private func saveAssetColorStylesIfNeeded( + nodes: [ColorStyleNode], + in assets: String? + ) -> Promise<[ColorStyleNode: ColorStyleAsset]> { + guard let folderPath = assets else { + return .value([:]) + } + + return colorStyleAssetsProvider.saveColorStyles(nodes: nodes, in: folderPath) + } + + // MARK: - + + func fetchColorStyles( + from file: FileParameters, + nodes: NodesParameters, + assets: String? + ) -> Promise<[ColorStyle]> { + firstly { + self.filesProvider.fetchFile(file) + }.then { figmaFile in + self.nodesProvider.fetchNodes(nodes, from: figmaFile).map { figmaNodes in + try self.extractColorStyleNodes(from: figmaNodes, of: figmaFile) + } + }.then { nodes in + firstly { + self.saveAssetColorStylesIfNeeded(nodes: nodes, in: assets) + }.map { assets in + nodes.map { node in + ColorStyle(node: node, asset: assets[node]) + } + } + } + } +} diff --git a/Sources/FigmaGen/Providers/Configuration/ConfigurationProvider.swift b/Sources/FigmaGen/Providers/Configuration/ConfigurationProvider.swift new file mode 100644 index 0000000..0a93f87 --- /dev/null +++ b/Sources/FigmaGen/Providers/Configuration/ConfigurationProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ConfigurationProvider { + + // MARK: - Instance Methods + + func fetchConfiguration(from configurationPath: String) -> Promise +} diff --git a/Sources/FigmaGen/Providers/Configuration/DefaultConfigurationProvider.swift b/Sources/FigmaGen/Providers/Configuration/DefaultConfigurationProvider.swift new file mode 100644 index 0000000..08009d1 --- /dev/null +++ b/Sources/FigmaGen/Providers/Configuration/DefaultConfigurationProvider.swift @@ -0,0 +1,22 @@ +import Foundation +import PromiseKit +import Yams +import PathKit + +final class DefaultConfigurationProvider: ConfigurationProvider { + + // MARK: - Instance Properties + + private let decoder = YAMLDecoder() + + // MARK: - Instance Methods + + func fetchConfiguration(from configurationPath: String) -> Promise { + Promise { seal in + let configurationPath = Path(configurationPath) + let configurationContent = try configurationPath.read(.utf8) + + seal.fulfill(try decoder.decode(Configuration.self, from: configurationContent)) + } + } +} diff --git a/Sources/FigmaGen/Providers/DataProvider/DataProvider.swift b/Sources/FigmaGen/Providers/DataProvider/DataProvider.swift new file mode 100644 index 0000000..cf8d3a0 --- /dev/null +++ b/Sources/FigmaGen/Providers/DataProvider/DataProvider.swift @@ -0,0 +1,10 @@ +import Foundation +import PromiseKit + +protocol DataProvider { + + // MARK: - Instance Methods + + func fetchData(from url: URL) -> Promise + func saveData(from url: URL, to filePath: String) -> Promise +} diff --git a/Sources/FigmaGen/Providers/DataProvider/DefaultDataProvider.swift b/Sources/FigmaGen/Providers/DataProvider/DefaultDataProvider.swift new file mode 100644 index 0000000..ea16aae --- /dev/null +++ b/Sources/FigmaGen/Providers/DataProvider/DefaultDataProvider.swift @@ -0,0 +1,40 @@ +import Foundation +import FigmaGenTools +import PromiseKit +import PathKit + +final class DefaultDataProvider: DataProvider { + + // MARK: - Instance Properties + + private let dataCache = Cache() + + // MARK: - Instance Methods + + func fetchData(from url: URL) -> Promise { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + if let data = self.dataCache.value(forKey: url) { + return data + } + + return try Data(contentsOf: url) + }.get { data in + self.dataCache.setValue(data, forKey: url) + } + } + + func saveData(from url: URL, to filePath: String) -> Promise { + firstly { + self.fetchData(from: url) + }.map(on: DispatchQueue.global(qos: .userInitiated)) { fileData in + let filePath = Path(filePath) + + if filePath.exists { + try filePath.delete() + } + + try filePath.parent().mkpath() + try filePath.write(fileData) + } + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/FigmaError.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/FigmaError.swift new file mode 100644 index 0000000..d3ba882 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/FigmaError.swift @@ -0,0 +1,22 @@ +import Foundation + +struct FigmaError: Error, Decodable, Hashable, CustomStringConvertible { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case status + case content = "err" + } + + // MARK: - Instance Properties + + let status: Int + let content: String + + // MARK: - CustomStringConvertible + + var description: String { + "\(type(of: self)).\(status)(\(content))" + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/FigmaImages.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/FigmaImages.swift new file mode 100644 index 0000000..03667d0 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/FigmaImages.swift @@ -0,0 +1,16 @@ +import Foundation + +struct FigmaImages: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case error = "err" + case urls = "images" + } + + // MARK: - Instance Properties + + let error: String? + let urls: [String: String?] +} diff --git a/Sources/Models/Figma/FigmaBlendMode.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaBlendMode.swift similarity index 73% rename from Sources/Models/Figma/FigmaBlendMode.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaBlendMode.swift index 4312c00..4419c97 100644 --- a/Sources/Models/Figma/FigmaBlendMode.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaBlendMode.swift @@ -1,14 +1,6 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// Enumeration describing how layer blends with layers below. -/// Get more info: https://www.figma.com/developers/api#blendmode-type -enum FigmaBlendMode: String, Hashable { +enum FigmaBlendMode: String { // MARK: - Enumeration Cases diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaBooleanOperationType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaBooleanOperationType.swift new file mode 100644 index 0000000..b7743fd --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaBooleanOperationType.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaBooleanOperationType: String { + + // MARK: - Enumeration Cases + + case union = "UNION" + case intersect = "INTERSECT" + case subtract = "SUBTRACT" + case exclude = "EXCLUDE" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaColor.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaColor.swift new file mode 100644 index 0000000..67115bb --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaColor.swift @@ -0,0 +1,20 @@ +import Foundation + +struct FigmaColor: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case red = "r" + case green = "g" + case blue = "b" + case alpha = "a" + } + + // MARK: - Instance Properties + + let red: Double + let green: Double + let blue: Double + let alpha: Double +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaColorStop.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaColorStop.swift new file mode 100644 index 0000000..b136930 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaColorStop.swift @@ -0,0 +1,9 @@ +import Foundation + +struct FigmaColorStop: Decodable, Hashable { + + // MARK: - Instance Properties + + let position: Double + let color: FigmaColor +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaComponent.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaComponent.swift new file mode 100644 index 0000000..c4395bf --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaComponent.swift @@ -0,0 +1,20 @@ +import Foundation + +struct FigmaComponent: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case key + case name + case description + case componentSetID = "componentSetId" + } + + // MARK: - Instance Properties + + let key: String? + let name: String? + let description: String? + let componentSetID: String? +} diff --git a/Sources/Models/Figma/FigmaConstraint.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaConstraint.swift similarity index 56% rename from Sources/Models/Figma/FigmaConstraint.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaConstraint.swift index 2c10bda..cb51a22 100644 --- a/Sources/Models/Figma/FigmaConstraint.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaConstraint.swift @@ -1,13 +1,5 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// Sizing constraint for exports. -/// Get more info: https://www.figma.com/developers/api#constraint-type struct FigmaConstraint: Decodable, Hashable { // MARK: - Nested Types @@ -19,13 +11,9 @@ struct FigmaConstraint: Decodable, Hashable { // MARK: - Instance Properties - /// Raw type of constraint to apply. let rawType: String - - /// Value of constraint to apply. let value: Double - /// Type of constraint to apply. var type: FigmaConstraintType? { FigmaConstraintType(rawValue: rawType) } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaConstraintType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaConstraintType.swift new file mode 100644 index 0000000..9fd963a --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaConstraintType.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaConstraintType: String { + + // MARK: - Enumeration Cases + + case width = "WIDTH" + case height = "HEIGHT" + case scale = "SCALE" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEasingType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEasingType.swift new file mode 100644 index 0000000..6ad4cec --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEasingType.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaEasingType: String { + + // MARK: - Enumeration Cases + + case easeIn = "EASE_IN" + case easeOut = "EASE_OUT" + case easeInOut = "EASE_IN_AND_OUT" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEffect.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEffect.swift new file mode 100644 index 0000000..81b0332 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEffect.swift @@ -0,0 +1,32 @@ +import Foundation + +struct FigmaEffect: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case rawType = "type" + case isVisible = "visible" + case radius + case color + case rawBlendMode = "blendMode" + case offset + } + + // MARK: - Instance Properties + + let rawType: String + let isVisible: Bool? + let radius: Double? + let color: FigmaColor? + let rawBlendMode: String? + let offset: FigmaVector? + + var type: FigmaEffectType? { + FigmaEffectType(rawValue: rawType) + } + + var blendMode: FigmaBlendMode? { + rawBlendMode.flatMap(FigmaBlendMode.init) + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEffectType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEffectType.swift new file mode 100644 index 0000000..600d2e8 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaEffectType.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaEffectType: String { + + // MARK: - Enumeration Cases + + case innerShadow = "INNER_SHADOW" + case dropShadow = "DROP_SHADOW" + case layerBlur = "LAYER_BLUR" + case backgroundBlur = "BACKGROUND_BLUR" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaExportSetting.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaExportSetting.swift new file mode 100644 index 0000000..5699a1c --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaExportSetting.swift @@ -0,0 +1,22 @@ +import Foundation + +struct FigmaExportSetting: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case suffix + case rawFormat = "format" + case constraint + } + + // MARK: - Instance Properties + + let suffix: String? + let rawFormat: String + let constraint: FigmaConstraint? + + var format: FigmaImageFormat? { + FigmaImageFormat(rawValue: rawFormat) + } +} diff --git a/Sources/Models/Figma/FigmaFrameOffset.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaFrameOffset.swift similarity index 54% rename from Sources/Models/Figma/FigmaFrameOffset.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaFrameOffset.swift index 958cd65..77b6d9a 100644 --- a/Sources/Models/Figma/FigmaFrameOffset.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaFrameOffset.swift @@ -1,13 +1,5 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// A relative offset within a frame. -/// Get more info: https://www.figma.com/developers/api#frameoffset-type struct FigmaFrameOffset: Decodable, Hashable { // MARK: - Nested Types @@ -19,9 +11,6 @@ struct FigmaFrameOffset: Decodable, Hashable { // MARK: - Instance Properties - /// Unique identifier specifying the frame. let nodeID: String - - /// 2D vector offset within the frame. let nodeOffset: FigmaVector } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaImageFormat.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaImageFormat.swift new file mode 100644 index 0000000..a71e8dd --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaImageFormat.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaImageFormat: String { + + // MARK: - Enumeration Cases + + case pdf = "PDF" + case png = "PNG" + case jpg = "JPG" + case svg = "SVG" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutConstraint.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutConstraint.swift new file mode 100644 index 0000000..0199904 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutConstraint.swift @@ -0,0 +1,24 @@ +import Foundation + +struct FigmaLayoutConstraint: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case rawVertical = "vertical" + case rawHorizontal = "horizontal" + } + + // MARK: - Instance Properties + + let rawVertical: String? + let rawHorizontal: String? + + var vertical: FigmaLayoutVerticalConstraint? { + rawVertical.flatMap(FigmaLayoutVerticalConstraint.init) + } + + var horizontal: FigmaLayoutHorizontalConstraint? { + rawHorizontal.flatMap(FigmaLayoutHorizontalConstraint.init) + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGrid.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGrid.swift new file mode 100644 index 0000000..8026a1a --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGrid.swift @@ -0,0 +1,36 @@ +import Foundation + +struct FigmaLayoutGrid: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case rawPattern = "pattern" + case rawAlignment = "alignment" + case sectionSize + case isVisible = "visible" + case color + case gutterSize + case offset + case count + } + + // MARK: - Instance Properties + + let rawPattern: String? + let rawAlignment: String? + let sectionSize: Double? + let isVisible: Bool? + let color: FigmaColor? + let gutterSize: Double? + let offset: Double? + let count: Double? + + var pattern: FigmaLayoutGridPattern? { + rawPattern.flatMap(FigmaLayoutGridPattern.init) + } + + var alignment: FigmaLayoutGridAlignment? { + rawAlignment.flatMap(FigmaLayoutGridAlignment.init) + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGridAlignment.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGridAlignment.swift new file mode 100644 index 0000000..3f7ecb3 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGridAlignment.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaLayoutGridAlignment: String { + + // MARK: - Enumeration Cases + + case min = "MIN" + case stretch = "STRETCH" + case center = "CENTER" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGridPattern.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGridPattern.swift new file mode 100644 index 0000000..688751e --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutGridPattern.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaLayoutGridPattern: String { + + // MARK: - Enumeration Cases + + case columns = "COLUMNS" + case rows = "ROWS" + case grid = "GRID" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutHorizontalConstraint.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutHorizontalConstraint.swift new file mode 100644 index 0000000..ac2cb5e --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutHorizontalConstraint.swift @@ -0,0 +1,12 @@ +import Foundation + +enum FigmaLayoutHorizontalConstraint: String { + + // MARK: - Enumeration Cases + + case left = "LEFT" + case right = "RIGHT" + case center = "CENTER" + case leftRight = "LEFT_RIGHT" + case scale = "SCALE" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutVerticalConstraint.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutVerticalConstraint.swift new file mode 100644 index 0000000..c78b52e --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLayoutVerticalConstraint.swift @@ -0,0 +1,12 @@ +import Foundation + +enum FigmaLayoutVerticalConstraint: String { + + // MARK: - Enumeration Cases + + case top = "TOP" + case bottom = "BOTTOM" + case center = "CENTER" + case topBottom = "TOP_BOTTOM" + case scale = "SCALE" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLineHeightUnit.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLineHeightUnit.swift new file mode 100644 index 0000000..1986ff1 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaLineHeightUnit.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaLineHeightUnit: String { + + // MARK: - Enumeration Cases + + case pixels = "PIXELS" + case fontSizePercent = "FONT_SIZE_%" + case intrinsicPercent = "INTRINSIC_%" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaPaint.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaPaint.swift new file mode 100644 index 0000000..1b6f501 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaPaint.swift @@ -0,0 +1,46 @@ +import Foundation + +struct FigmaPaint: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case rawType = "type" + case isVisible = "visible" + case opacity + case color + case rawBlendMode = "blendMode" + case gradientHandlePositions + case gradientStops + case rawScaleMode = "scaleMode" + case imageTransform + case imageRef + case gifRef + } + + // MARK: - Instance Properties + + let rawType: String + let isVisible: Bool? + let opacity: Double? + let color: FigmaColor? + let rawBlendMode: String? + let gradientHandlePositions: [FigmaVector]? + let gradientStops: [FigmaColorStop]? + let rawScaleMode: String? + let imageTransform: [[Double]]? + let imageRef: String? + let gifRef: String? + + var type: FigmaPaintType? { + FigmaPaintType(rawValue: rawType) + } + + var blendMode: FigmaBlendMode? { + rawBlendMode.flatMap(FigmaBlendMode.init) + } + + var scaleMode: FigmaScaleMode? { + rawScaleMode.flatMap(FigmaScaleMode.init) + } +} diff --git a/Sources/Models/Figma/FigmaPaintType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaPaintType.swift similarity index 59% rename from Sources/Models/Figma/FigmaPaintType.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaPaintType.swift index 921c6b1..353b4c5 100644 --- a/Sources/Models/Figma/FigmaPaintType.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaPaintType.swift @@ -1,14 +1,6 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// Enumeration of known paint types. -/// Get more info: https://www.figma.com/developers/api#paint-type -enum FigmaPaintType: String, Hashable { +enum FigmaPaintType: String { // MARK: - Enumeration Cases diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaRectangle.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaRectangle.swift new file mode 100644 index 0000000..02dcbb9 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaRectangle.swift @@ -0,0 +1,11 @@ +import Foundation + +struct FigmaRectangle: Decodable, Hashable { + + // MARK: - Instance Properties + + let x: Double? + let y: Double? + let width: Double? + let height: Double? +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaScaleMode.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaScaleMode.swift new file mode 100644 index 0000000..6013a1d --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaScaleMode.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaScaleMode: String { + + // MARK: - Enumeration Cases + + case fill = "FILL" + case fit = "FIT" + case tile = "TILE" + case stretch = "STRETCH" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeAlignment.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeAlignment.swift new file mode 100644 index 0000000..3471f00 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeAlignment.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaStrokeAlignment: String { + + // MARK: - Enumeration Cases + + case inside = "INSIDE" + case outside = "OUTSIDE" + case center = "CENTER" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeCap.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeCap.swift new file mode 100644 index 0000000..8ded6fb --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeCap.swift @@ -0,0 +1,12 @@ +import Foundation + +enum FigmaStrokeCap: String { + + // MARK: - Enumeration Cases + + case none = "NONE" + case round = "ROUND" + case square = "SQUARE" + case lineArrow = "LINE_ARROW" + case triangleArrow = "TRIANGLE_ARROW" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeJoin.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeJoin.swift new file mode 100644 index 0000000..d3d7237 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStrokeJoin.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaStrokeJoin: String { + + // MARK: - Enumeration Cases + + case miter = "MITER" + case bevel = "BEVEL" + case round = "ROUND" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStyle.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStyle.swift new file mode 100644 index 0000000..877d52f --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStyle.swift @@ -0,0 +1,24 @@ +import Foundation + +struct FigmaStyle: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case key + case rawType = "styleType" + case name + case description + } + + // MARK: - Instance Properties + + let key: String? + let rawType: String? + let name: String? + let description: String? + + var type: FigmaStyleType? { + rawType.flatMap(FigmaStyleType.init) + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStyleType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStyleType.swift new file mode 100644 index 0000000..7cbe037 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaStyleType.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaStyleType: String { + + // MARK: - Enumeration Cases + + case fill = "FILL" + case text = "TEXT" + case effect = "EFFECT" + case grid = "GRID" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextCase.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextCase.swift new file mode 100644 index 0000000..52e80f5 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextCase.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaTextCase: String { + + // MARK: - Enumeration Cases + + case original = "ORIGINAL" + case upper = "UPPER" + case lower = "LOWER" + case title = "TITLE" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextDecoration.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextDecoration.swift new file mode 100644 index 0000000..c86aa05 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextDecoration.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaTextDecoration: String { + + // MARK: - Enumeration Cases + + case none = "NONE" + case strikethrough = "STRIKETHROUGH" + case underline = "UNDERLINE" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextHorizontalAlignment.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextHorizontalAlignment.swift new file mode 100644 index 0000000..d0c5de3 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextHorizontalAlignment.swift @@ -0,0 +1,11 @@ +import Foundation + +enum FigmaTextHorizontalAlignment: String { + + // MARK: - Enumeration Cases + + case left = "LEFT" + case right = "RIGHT" + case center = "CENTER" + case justified = "JUSTIFIED" +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextVerticalAlignment.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextVerticalAlignment.swift new file mode 100644 index 0000000..0a1feb0 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTextVerticalAlignment.swift @@ -0,0 +1,10 @@ +import Foundation + +enum FigmaTextVerticalAlignment: String { + + // MARK: - Enumeration Cases + + case top = "TOP" + case center = "CENTER" + case bottom = "BOTTOM" +} diff --git a/Sources/Models/Figma/FigmaTypeStyle.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTypeStyle.swift similarity index 58% rename from Sources/Models/Figma/FigmaTypeStyle.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTypeStyle.swift index 7f8631f..36cecd8 100644 --- a/Sources/Models/Figma/FigmaTypeStyle.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaTypeStyle.swift @@ -1,13 +1,5 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// Metadata for character formatting. -/// Get more info: https://www.figma.com/developers/api#typestyle-type struct FigmaTypeStyle: Decodable, Hashable { // MARK: - Nested Types @@ -33,89 +25,47 @@ struct FigmaTypeStyle: Decodable, Hashable { // MARK: - Instance Properties - /// Font family of text (standard name). let fontFamily: String? - - /// PostScript font name. let fontPostScriptName: String? - - /// Numeric font weight. let fontWeight: Double? - - /// Font size in px. let fontSize: Double? - - /// Whether or not text is italicized? - /// Defaults to `false`. let isItalic: Bool? - - /// Space between paragraphs in px. - /// Defaults to `0`. let paragraphSpacing: Double? - - /// Paragraph indentation in px. - /// Defaults to `0`. let paragraphIndent: Double? - - /// Raw value of text casing applied to the node. - /// Defaults to `ORIGINAL`. let rawTextCase: String? - - /// Raw value of text decoration applied to the node. - /// Defaults to `NONE`. let rawTextDecoration: String? - - /// Raw value of horizontal text alignment. let rawTextHorizontalAlignment: String? - - /// Raw value of vertical text alignment. let rawTextVerticalAlignment: String? - - /// Space between characters in px. let letterSpacing: Double? - - /// Paints applied to characters. let fills: [FigmaPaint]? - - /// Line height in px. let lineHeight: Double? - - /// Line height as a percentage of the font size. - /// Defaults to `100`. let lineHeightPercentFontSize: Double? - - /// Raw value of unit type of the line height value specified by the user. let rawLineHeightUnit: String? - /// Text casing applied to the node. var textCase: FigmaTextCase? { - guard let rawTextCase = rawTextCase else { - return .original + guard let rawTextCase else { + return FigmaTextCase.original } return FigmaTextCase(rawValue: rawTextCase) } - /// Text decoration applied to the node. var textDecoration: FigmaTextDecoration? { - guard let rawTextDecoration = rawTextDecoration else { + guard let rawTextDecoration else { return FigmaTextDecoration.none } return FigmaTextDecoration(rawValue: rawTextDecoration) } - /// Horizontal text alignment. var textHorizontalAlignment: FigmaTextHorizontalAlignment? { rawTextHorizontalAlignment.flatMap(FigmaTextHorizontalAlignment.init) } - /// Vertical text alignment. var textVericalAlignment: FigmaTextVerticalAlignment? { rawTextVerticalAlignment.flatMap(FigmaTextVerticalAlignment.init) } - /// The unit of the line height value specified by the user. var lineHeightUnit: FigmaLineHeightUnit? { rawLineHeightUnit.flatMap(FigmaLineHeightUnit.init) } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaVector.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaVector.swift new file mode 100644 index 0000000..dd5fb36 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/FigmaVector.swift @@ -0,0 +1,9 @@ +import Foundation + +struct FigmaVector: Decodable, Hashable { + + // MARK: - Instance Properties + + let x: Double + let y: Double +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaBooleanOperationNodePayload.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaBooleanOperationNodePayload.swift new file mode 100644 index 0000000..c6f21b0 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaBooleanOperationNodePayload.swift @@ -0,0 +1,20 @@ +import Foundation + +struct FigmaBooleanOperationNodePayload: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case children + case rawOperationType = "booleanOperation" + } + + // MARK: - Instance Properties + + let children: [FigmaNode]? + let rawOperationType: String? + + var operationType: FigmaBooleanOperationType? { + rawOperationType.flatMap(FigmaBooleanOperationType.init) + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaCanvasNodeInfo.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaCanvasNodeInfo.swift new file mode 100644 index 0000000..a06fb80 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaCanvasNodeInfo.swift @@ -0,0 +1,11 @@ +import Foundation + +struct FigmaCanvasNodeInfo: Decodable, Hashable { + + // MARK: - Instance Properties + + let children: [FigmaNode]? + let backgroundColor: FigmaColor? + let prototypeStartNodeID: String? + let exportSettings: [FigmaExportSetting]? +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaDocumentNodeInfo.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaDocumentNodeInfo.swift new file mode 100644 index 0000000..736c290 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaDocumentNodeInfo.swift @@ -0,0 +1,8 @@ +import Foundation + +struct FigmaDocumentNodeInfo: Decodable, Hashable { + + // MARK: - Instance Properties + + let children: [FigmaNode]? +} diff --git a/Sources/Models/Figma/Nodes/FigmaFile.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaFile.swift similarity index 54% rename from Sources/Models/Figma/Nodes/FigmaFile.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaFile.swift index 4a85f88..59d28ee 100644 --- a/Sources/Models/Figma/Nodes/FigmaFile.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaFile.swift @@ -1,12 +1,5 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// File node. struct FigmaFile: Decodable, Hashable { // MARK: - Nested Types @@ -24,27 +17,12 @@ struct FigmaFile: Decodable, Hashable { // MARK: - Instance Properties - /// File name. let name: String - - /// Last modified date of the file. - let lastModified: Date - - /// Thumbnail image URL. - let thumbnailURL: URL - - /// Version of the file. - let version: String - - /// Version of the file schema. - let schemaVersion: Int - - /// Document of the file. + let lastModified: Date? + let thumbnailURL: URL? + let version: String? + let schemaVersion: Int? let document: FigmaNode - - /// Components of the file. let components: [String: FigmaComponent]? - - /// Styles of the file. let styles: [String: FigmaStyle]? } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaFrameNodeInfo.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaFrameNodeInfo.swift new file mode 100644 index 0000000..56433e3 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaFrameNodeInfo.swift @@ -0,0 +1,54 @@ +import Foundation + +struct FigmaFrameNodeInfo: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case children + case isLocked = "locked" + case background + case exportSettings + case rawBlendMode = "blendMode" + case preserveRatio + case constraints + case transitionNodeID + case transitionDuration + case rawTransitionEasing + case opacity + case absoluteBoundingBox + case clipsContent + case layoutGrids + case effects + case isMask + case isMaskOutline + } + + // MARK: - Instance Properties + + let children: [FigmaNode]? + let isLocked: Bool? + let background: [FigmaPaint]? + let exportSettings: [FigmaExportSetting]? + let rawBlendMode: String? + let preserveRatio: Bool? + let constraints: FigmaLayoutConstraint? + let transitionNodeID: String? + let transitionDuration: Double? + let rawTransitionEasing: String? + let opacity: Double? + let absoluteBoundingBox: FigmaRectangle? + let clipsContent: Bool? + let layoutGrids: [FigmaLayoutGrid]? + let effects: [FigmaEffect]? + let isMask: Bool? + let isMaskOutline: Bool? + + var blendMode: FigmaBlendMode? { + rawBlendMode.flatMap(FigmaBlendMode.init) + } + + var transitionEasing: FigmaEasingType? { + rawTransitionEasing.flatMap(FigmaEasingType.init) + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaInstanceNodePayload.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaInstanceNodePayload.swift new file mode 100644 index 0000000..1dac61a --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaInstanceNodePayload.swift @@ -0,0 +1,14 @@ +import Foundation + +struct FigmaInstanceNodePayload: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case componentID = "componentId" + } + + // MARK: - Instance Properties + + let componentID: String? +} diff --git a/Sources/Models/Figma/Nodes/FigmaNode.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaNode.swift similarity index 61% rename from Sources/Models/Figma/Nodes/FigmaNode.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaNode.swift index 3011766..172c7e9 100644 --- a/Sources/Models/Figma/Nodes/FigmaNode.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaNode.swift @@ -1,14 +1,6 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation +import FigmaGenTools -/// Properties that exist on every node. -/// A node can have additional properties associated with it depending on its node type. -/// Get more info: https://www.figma.com/developers/api#node-types struct FigmaNode: Decodable, Hashable { // MARK: - Nested Types @@ -16,8 +8,9 @@ struct FigmaNode: Decodable, Hashable { private enum CodingKeys: String, CodingKey { case id case name - case type + case rawType = "type" case isVisible = "visible" + case sharedPluginData } private enum CodingValues { @@ -35,48 +28,74 @@ struct FigmaNode: Decodable, Hashable { static let textType = "TEXT" static let sliceType = "SLICE" static let componentType = "COMPONENT" + static let componentSetType = "COMPONENT_SET" static let instanceType = "INSTANCE" } // MARK: - Instance Properties - /// A string uniquely identifying this node within the document. - let id: String + private var _parent: Indirect? - /// The name given to the node by the user in the tool. - let name: String + let id: String + let name: String? + let rawType: String + let isVisible: Bool? + let sharedPluginData: FigmaPluginData? - /// Node type. - var type: FigmaNodeType + let type: FigmaNodeType - /// Whether or not the node is visible on the canvas. - /// Defaults to `true`. - let isVisible: Bool? + var parent: Self? { + _parent?.value + } - /// An array of childs attached to the node. - var children: [FigmaNode]? { + var children: [Self]? { switch type { - case .unknown, .slice, .vector, .star, .line, .ellipse, .regularPolygon, .rectangle, .text: + case .unknown, .vector, .star, .line, .ellipse, .regularPolygon, .rectangle, .text, .slice: return nil - case let .booleanOperation(info: _, payload: payload): - return payload.children - case let .document(info: documentNodeInfo): return documentNodeInfo.children case let .canvas(info: canvasNodeInfo): - return canvasNodeInfo.children + return canvasNodeInfo.children?.map { $0.withParent(self) } case let .frame(info: frameNodeInfo), let .group(info: frameNodeInfo), let .component(info: frameNodeInfo), + let .componentSet(info: frameNodeInfo), let .instance(info: frameNodeInfo, payload: _): - return frameNodeInfo.children + return frameNodeInfo.children?.map { $0.withParent(self) } + + case let .booleanOperation(info: _, payload: payload): + return payload.children?.map { $0.withParent(self) } + } + } + + var frameInfo: FigmaFrameNodeInfo? { + switch type { + case .unknown, + .document, + .canvas, + .vector, + .booleanOperation, + .star, + .line, + .ellipse, + .regularPolygon, + .rectangle, + .text, + .slice: + return nil + + case let .frame(info: nodeInfo), + let .group(info: nodeInfo), + let .component(info: nodeInfo), + let .componentSet(info: nodeInfo), + let .instance(info: nodeInfo, payload: _): + return nodeInfo } } - /// Vector node specific proprties. var vectorInfo: FigmaVectorNodeInfo? { switch type { case .unknown, @@ -84,13 +103,14 @@ struct FigmaNode: Decodable, Hashable { .canvas, .frame, .group, - .booleanOperation, .slice, .component, + .componentSet, .instance: return nil case let .vector(info: nodeInfo), + let .booleanOperation(info: nodeInfo, payload: _), let .star(info: nodeInfo), let .line(info: nodeInfo), let .ellipse(info: nodeInfo), @@ -101,25 +121,36 @@ struct FigmaNode: Decodable, Hashable { } } + var isComponent: Bool { + if case .component = type { + return true + } + + return false + } + + var isInstance: Bool { + if case .instance = type { + return true + } + + return false + } + // MARK: - Initializers - /// Creates a new instance by decoding from the given decoder. - /// - /// This initializer throws an error if reading from the decoder fails, or - /// if the data read is corrupted or otherwise invalid. - /// - /// - Parameter decoder: The decoder to read data from. init(from decoder: Decoder) throws { // swiftlint:disable:previous function_body_length let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(forKey: .id) - name = try container.decode(forKey: .name) - + name = try container.decodeIfPresent(forKey: .name) + rawType = try container.decode(String.self, forKey: .rawType) isVisible = try container.decodeIfPresent(forKey: .isVisible) + sharedPluginData = try container.decodeIfPresent(forKey: .sharedPluginData) - switch try container.decode(String.self, forKey: .type) { + switch rawType { case CodingValues.documentType: type = .document(info: try FigmaDocumentNodeInfo(from: decoder)) @@ -171,6 +202,9 @@ struct FigmaNode: Decodable, Hashable { case CodingValues.componentType: type = .component(info: try FigmaFrameNodeInfo(from: decoder)) + case CodingValues.componentSetType: + type = .componentSet(info: try FigmaFrameNodeInfo(from: decoder)) + case CodingValues.instanceType: type = .instance( info: try FigmaFrameNodeInfo(from: decoder), @@ -181,4 +215,39 @@ struct FigmaNode: Decodable, Hashable { type = .unknown } } + + init( + id: String, + name: String?, + rawType: String, + isVisible: Bool?, + sharedPluginData: FigmaPluginData?, + type: FigmaNodeType, + parent: Self? = nil + ) { + self.id = id + self.name = name + self.rawType = rawType + self.isVisible = isVisible + self.sharedPluginData = sharedPluginData + self.type = type + self._parent = parent.map { Indirect($0) } + } +} + +extension FigmaNode { + + // MARK: - Instance Methods + + func withParent(_ parent: FigmaNode) -> Self { + Self( + id: id, + name: name, + rawType: rawType, + isVisible: isVisible, + sharedPluginData: sharedPluginData, + type: type, + parent: parent + ) + } } diff --git a/Sources/Models/Figma/Nodes/FigmaNodeType.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaNodeType.swift similarity index 71% rename from Sources/Models/Figma/Nodes/FigmaNodeType.swift rename to Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaNodeType.swift index 190707a..9ea8e8f 100644 --- a/Sources/Models/Figma/Nodes/FigmaNodeType.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaNodeType.swift @@ -1,15 +1,5 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -/// Enumeration of known node types. -/// Node type indicates what kind of node you are working with: for example, a Frame node versus a Rectangle node. -/// A node can have additional properties associated with it depending on its node type. -/// Get more info: https://www.figma.com/developers/api#node-types indirect enum FigmaNodeType: Hashable { // MARK: - Enumeration Cases @@ -29,5 +19,6 @@ indirect enum FigmaNodeType: Hashable { case text(info: FigmaVectorNodeInfo, payload: FigmaTextNodePayload) case slice(info: FigmaSliceNodeInfo) case component(info: FigmaFrameNodeInfo) + case componentSet(info: FigmaFrameNodeInfo) case instance(info: FigmaFrameNodeInfo, payload: FigmaInstanceNodePayload) } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaPluginData.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaPluginData.swift new file mode 100644 index 0000000..3149e70 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaPluginData.swift @@ -0,0 +1,27 @@ +import Foundation +import FigmaGenTools + +enum FigmaPluginData: Decodable, Hashable { + + // MARK: - Enumeration Cases + + case string(String) + case dictionary([String: AnyCodable]) + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let dictionaryValue = try? container.decode([String: AnyCodable].self) { + self = .dictionary(dictionaryValue) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "plugin data is not a string or dictionary" + ) + } + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaRectangleNodePayload.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaRectangleNodePayload.swift new file mode 100644 index 0000000..a32984b --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaRectangleNodePayload.swift @@ -0,0 +1,9 @@ +import Foundation + +struct FigmaRectangleNodePayload: Decodable, Hashable { + + // MARK: - Instance Properties + + let cornerRadius: Double? + let rectangleCornerRadii: [Double]? +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaSliceNodeInfo.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaSliceNodeInfo.swift new file mode 100644 index 0000000..376ab17 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaSliceNodeInfo.swift @@ -0,0 +1,9 @@ +import Foundation + +struct FigmaSliceNodeInfo: Decodable, Hashable { + + // MARK: - Instance Properties + + let exportSettings: [FigmaExportSetting]? + let absoluteBoundingBox: FigmaRectangle? +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaTextNodePayload.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaTextNodePayload.swift new file mode 100644 index 0000000..bcee687 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaTextNodePayload.swift @@ -0,0 +1,20 @@ +import Foundation + +struct FigmaTextNodePayload: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case text = "characters" + case style + case characterStyleOverrides + case styleOverrideTable + } + + // MARK: - Instance Properties + + let text: String? + let style: FigmaTypeStyle? + let characterStyleOverrides: [Int]? + let styleOverrideTable: [Int: FigmaTypeStyle]? +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaVectorNodeInfo.swift b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaVectorNodeInfo.swift new file mode 100644 index 0000000..52433fc --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DTOs/File/Nodes/FigmaVectorNodeInfo.swift @@ -0,0 +1,88 @@ +import Foundation + +struct FigmaVectorNodeInfo: Decodable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case isLocked = "locked" + case exportSettings + case rawBlendMode = "blendMode" + case preserveRatio + case constraints + case transitionNodeID + case transitionDuration + case rawTransitionEasing + case opacity + case absoluteBoundingBox + case effects + case isMask + case fills + case strokes + case strokeWeight + case rawStrokeCap = "strokeCap" + case rawStrokeJoin = "strokeJoin" + case strokeDashes + case strokeMiterAngle + case rawStrokeAlignment = "strokeAlign" + case styles + } + + // MARK: - Instance Properties + + let isLocked: Bool? + let exportSettings: [FigmaExportSetting]? + let rawBlendMode: String? + let preserveRatio: Bool? + let constraints: FigmaLayoutConstraint? + let transitionNodeID: String? + let transitionDuration: Double? + let rawTransitionEasing: String? + let opacity: Double? + let absoluteBoundingBox: FigmaRectangle? + let effects: [FigmaEffect]? + let isMask: Bool? + let fills: [FigmaPaint]? + let strokes: [FigmaPaint]? + let strokeWeight: Double? + let rawStrokeCap: String? + let rawStrokeJoin: String? + let strokeDashes: [Double]? + let strokeMiterAngle: Double? + let rawStrokeAlignment: String? + let styles: [String: String]? + + var blendMode: FigmaBlendMode? { + rawBlendMode.flatMap(FigmaBlendMode.init) + } + + var transitionEasing: FigmaEasingType? { + rawTransitionEasing.flatMap(FigmaEasingType.init) + } + + var strokeCap: FigmaStrokeCap? { + guard let rawStrokeCap else { + return FigmaStrokeCap.none + } + + return FigmaStrokeCap(rawValue: rawStrokeCap) + } + + var strokeJoin: FigmaStrokeJoin? { + guard let rawStrokeJoin else { + return FigmaStrokeJoin.miter + } + + return FigmaStrokeJoin(rawValue: rawStrokeJoin) + } + + var strokeAlignment: FigmaStrokeAlignment? { + rawStrokeAlignment.flatMap(FigmaStrokeAlignment.init) + } + + // MARK: - Instance Methods + + func styleID(of styleType: FigmaStyleType) -> String? { + styles?[styleType.rawValue.lowercased()] + } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/DefaultFigmaAPIProvider.swift b/Sources/FigmaGen/Providers/FigmaAPI/DefaultFigmaAPIProvider.swift new file mode 100644 index 0000000..51fce40 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/DefaultFigmaAPIProvider.swift @@ -0,0 +1,153 @@ +import Foundation +import PromiseKit +import FigmaGenTools + +final class DefaultFigmaAPIProvider: FigmaAPIProvider { + + // MARK: - Instance Properties + + private let queryEncoder: HTTPQueryEncoder + private let bodyEncoder: HTTPBodyEncoder + private let responseDecoder: HTTPResponseDecoder + + // MARK: - + + let httpService: FigmaHTTPService + + // MARK: - Initializers + + init(httpService: FigmaHTTPService) { + self.httpService = httpService + + let urlEncoder = URLEncoder(boolEncodingStrategy: .literal) + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + urlEncoder.dateEncodingStrategy = .formatted(.figmaAPI(withMilliseconds: true)) + jsonEncoder.dateEncodingStrategy = .formatted(.figmaAPI(withMilliseconds: true)) + + jsonDecoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + if let date = DateFormatter.figmaAPI(withMilliseconds: true).date(from: dateString) { + return date + } + + if let date = DateFormatter.figmaAPI(withMilliseconds: false).date(from: dateString) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Date string does not match format expected by formatter" + ) + } + + self.queryEncoder = HTTPQueryURLEncoder(urlEncoder: urlEncoder) + self.bodyEncoder = HTTPBodyJSONEncoder(jsonEncoder: jsonEncoder) + self.responseDecoder = jsonDecoder + } + + // MARK: - Instance Methods + + private func makeHTTPRoute(for route: Route) -> HTTPRoute { + let url = URL.figmaAPIServer + .appendingPathComponent(route.apiVersion.urlPath) + .appendingPathComponent(route.urlPath) + + let headers = route.accessToken.map { [HTTPHeader.figmaAccessToken($0)] } ?? [] + + return HTTPRoute( + method: route.httpMethod, + url: url, + headers: headers, + queryParameters: route.queryParameters, + queryEncoder: queryEncoder, + bodyParameters: route.bodyParameters, + bodyEncoder: bodyEncoder + ) + } + + private func handleHTTPError(_ error: HTTPError) -> Error { + guard let errorData = error.data, error.reason is HTTPStatusCode else { + return error + } + + guard let apiError = try? responseDecoder.decode(FigmaError.self, from: errorData) else { + return error + } + + return apiError + } + + // MARK: - + + func request(route: Route) -> Promise where Route.Response == FigmaAPIEmptyResponse { + Promise { seal in + let task = httpService.request(route: makeHTTPRoute(for: route)) + + task.response { response in + switch response.result { + case let .failure(error): + seal.reject(self.handleHTTPError(error)) + + case .success: + seal.fulfill(Void()) + } + } + } + } + + func request(route: Route) -> Promise { + Promise { seal in + let task = httpService.request(route: makeHTTPRoute(for: route)) + + task.responseDecodable(type: Route.Response.self, decoder: responseDecoder) { response in + switch response.result { + case let .failure(error): + seal.reject(self.handleHTTPError(error)) + + case let .success(value): + seal.fulfill(value) + } + } + } + } +} + +extension HTTPHeader { + + // MARK: - Type Methods + + fileprivate static func figmaAccessToken(_ value: String) -> HTTPHeader { + HTTPHeader(name: "X-Figma-Token", value: value) + } +} + +extension DateFormatter { + + // MARK: - Type Properties + + fileprivate static func figmaAPI(withMilliseconds: Bool) -> DateFormatter { + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + if withMilliseconds { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } else { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + } + + return dateFormatter + } +} + +extension URL { + + // MARK: - Type Properties + + fileprivate static let figmaAPIServer = URL(string: "https://api.figma.com")! +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIEmptyParameters.swift b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIEmptyParameters.swift new file mode 100644 index 0000000..9030183 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIEmptyParameters.swift @@ -0,0 +1,8 @@ +import Foundation + +struct FigmaAPIEmptyParameters: Encodable { + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { } +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIEmptyResponse.swift b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIEmptyResponse.swift new file mode 100644 index 0000000..68745c6 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIEmptyResponse.swift @@ -0,0 +1,3 @@ +import Foundation + +struct FigmaAPIEmptyResponse: Decodable { } diff --git a/Sources/Services/API/FigmaAPIProvider.swift b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIProvider.swift similarity index 60% rename from Sources/Services/API/FigmaAPIProvider.swift rename to Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIProvider.swift index b658b9f..499deb3 100644 --- a/Sources/Services/API/FigmaAPIProvider.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIProvider.swift @@ -1,9 +1,3 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation import PromiseKit @@ -11,5 +5,6 @@ protocol FigmaAPIProvider { // MARK: - Instance Methods + func request(route: Route) -> Promise where Route.Response == FigmaAPIEmptyResponse func request(route: Route) -> Promise } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIRoute.swift b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIRoute.swift new file mode 100644 index 0000000..ef8e4cc --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIRoute.swift @@ -0,0 +1,55 @@ +import Foundation +import FigmaGenTools + +protocol FigmaAPIRoute { + + // MARK: - Nested Types + + associatedtype QueryParameters: Encodable + associatedtype BodyParameters: Encodable + associatedtype Response: Decodable + + // MARK: - Instance Properties + + var apiVersion: FigmaAPIVersion { get } + var httpMethod: HTTPMethod { get } + var urlPath: String { get } + var accessToken: String? { get } + var queryParameters: QueryParameters? { get } + var bodyParameters: BodyParameters? { get } +} + +extension FigmaAPIRoute { + + // MARK: - Instance Properties + + var apiVersion: FigmaAPIVersion { + .v1 + } + + var httpMethod: HTTPMethod { + .get + } + + var accessToken: String? { + nil + } +} + +extension FigmaAPIRoute where QueryParameters == FigmaAPIEmptyParameters { + + // MARK: - Instance Properties + + var queryParameters: QueryParameters? { + nil + } +} + +extension FigmaAPIRoute where BodyParameters == FigmaAPIEmptyParameters { + + // MARK: - Instance Properties + + var bodyParameters: BodyParameters? { + nil + } +} diff --git a/Sources/Services/API/FigmaAPIVersion.swift b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIVersion.swift similarity index 78% rename from Sources/Services/API/FigmaAPIVersion.swift rename to Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIVersion.swift index 0ef27e5..f51998c 100644 --- a/Sources/Services/API/FigmaAPIVersion.swift +++ b/Sources/FigmaGen/Providers/FigmaAPI/FigmaAPIVersion.swift @@ -1,9 +1,3 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation enum FigmaAPIVersion { diff --git a/Sources/FigmaGen/Providers/FigmaAPI/FigmaHTTPService.swift b/Sources/FigmaGen/Providers/FigmaAPI/FigmaHTTPService.swift new file mode 100644 index 0000000..886a5fb --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/FigmaHTTPService.swift @@ -0,0 +1,11 @@ +import Foundation +import FigmaGenTools + +public protocol FigmaHTTPService { + + // MARK: - Instance Methods + + func request(route: HTTPRoute) -> HTTPTask +} + +extension HTTPService: FigmaHTTPService { } diff --git a/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIFileRoute.swift b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIFileRoute.swift new file mode 100644 index 0000000..4710c59 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIFileRoute.swift @@ -0,0 +1,48 @@ +import Foundation + +struct FigmaAPIFileRoute: FigmaAPIRoute { + + // MARK: - Nested Types + + typealias Response = FigmaFile + typealias QueryParameters = FigmaAPIFileRouteQueryParameters + + // MARK: - Instance Properties + + let accessToken: String? + let fileKey: String + + let queryParameters: QueryParameters? + + var urlPath: String { + "files/\(fileKey)" + } + + // MARK: - Initializers + + init( + accessToken: String, + fileKey: String, + version: String? = nil, + nodeIDs: [String]? = nil, + depth: Int? = nil, + pluginData: String? = nil + ) { + self.accessToken = accessToken + self.fileKey = fileKey + + self.queryParameters = QueryParameters( + version: version, + nodeIDs: nodeIDs?.joined(separator: .nodeIDsSeparator), + depth: depth, + pluginData: pluginData + ) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let nodeIDsSeparator = "," +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIFileRouteQueryParameters.swift b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIFileRouteQueryParameters.swift new file mode 100644 index 0000000..3cfe6d6 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIFileRouteQueryParameters.swift @@ -0,0 +1,20 @@ +import Foundation + +struct FigmaAPIFileRouteQueryParameters: Encodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case version + case nodeIDs = "ids" + case depth + case pluginData = "plugin_data" + } + + // MARK: - Instance Properties + + let version: String? + let nodeIDs: String? + let depth: Int? + let pluginData: String? +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIImagesRoute.swift b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIImagesRoute.swift new file mode 100644 index 0000000..34d37be --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIImagesRoute.swift @@ -0,0 +1,54 @@ +import Foundation + +struct FigmaAPIImagesRoute: FigmaAPIRoute { + + // MARK: - Nested Types + + typealias Response = FigmaImages + typealias QueryParameters = FigmaAPIImagesRouteQueryParameters + + // MARK: - Instance Properties + + let accessToken: String? + let fileKey: String + + let queryParameters: QueryParameters? + + var urlPath: String { + "images/\(fileKey)" + } + + // MARK: - Initializers + + init( + accessToken: String, + fileKey: String, + fileVersion: String? = nil, + nodeIDs: [String], + format: FigmaImageFormat? = nil, + scale: Double? = nil, + svgIncludeID: Bool? = nil, + svgSimplifyStroke: Bool? = nil, + useAbsoluteBounds: Bool? = nil + ) { + self.accessToken = accessToken + self.fileKey = fileKey + + self.queryParameters = QueryParameters( + fileVersion: fileVersion, + nodeIDs: nodeIDs.joined(separator: .nodeIDsSeparator), + format: format?.rawValue.lowercased(), + scale: scale, + svgIncludeID: svgIncludeID, + svgSimplifyStroke: svgSimplifyStroke, + useAbsolutBounds: useAbsoluteBounds + ) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let nodeIDsSeparator = "," +} diff --git a/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIImagesRouteQueryParameters.swift b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIImagesRouteQueryParameters.swift new file mode 100644 index 0000000..5e0bfd4 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaAPI/Routes/FigmaAPIImagesRouteQueryParameters.swift @@ -0,0 +1,26 @@ +import Foundation + +struct FigmaAPIImagesRouteQueryParameters: Encodable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case fileVersion = "version" + case nodeIDs = "ids" + case format + case scale + case svgIncludeID = "svg_include_id" + case svgSimplifyStroke = "svg_simplify_stroke" + case useAbsolutBounds = "use_absolute_bounds" + } + + // MARK: - Instance Properties + + let fileVersion: String? + let nodeIDs: String + let format: String? + let scale: Double? + let svgIncludeID: Bool? + let svgSimplifyStroke: Bool? + let useAbsolutBounds: Bool? +} diff --git a/Sources/FigmaGen/Providers/FigmaFiles/DefaultFigmaFilesProvider.swift b/Sources/FigmaGen/Providers/FigmaFiles/DefaultFigmaFilesProvider.swift new file mode 100644 index 0000000..d0bbeac --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaFiles/DefaultFigmaFilesProvider.swift @@ -0,0 +1,47 @@ +import Foundation +import PromiseKit + +final class DefaultFigmaFilesProvider: FigmaFilesProvider { + + // MARK: - Instance Properties + + private var filePromises: [String: Promise] = [:] + + // MARK: - + + let apiProvider: FigmaAPIProvider + + // MARK: - Initializers + + init(apiProvider: FigmaAPIProvider) { + self.apiProvider = apiProvider + } + + // MARK: - Instance Methods + + func fetchFile(_ file: FileParameters) -> Promise { + let fileUniqueID = "\(file.key): \(file.version ?? "nil")" + + if let filePromise = filePromises[fileUniqueID] { + return filePromise + } + + let route = FigmaAPIFileRoute( + accessToken: file.accessToken, + fileKey: file.key, + version: file.version + ) + + let promise = firstly { + apiProvider.request(route: route) + }.tap { result in + if !result.isFulfilled { + self.filePromises.removeValue(forKey: fileUniqueID) + } + } + + filePromises[fileUniqueID] = promise + + return promise + } +} diff --git a/Sources/FigmaGen/Providers/FigmaFiles/FigmaFilesProvider.swift b/Sources/FigmaGen/Providers/FigmaFiles/FigmaFilesProvider.swift new file mode 100644 index 0000000..78178f9 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaFiles/FigmaFilesProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol FigmaFilesProvider { + + // MARK: - Instance Methods + + func fetchFile(_ file: FileParameters) -> Promise +} diff --git a/Sources/FigmaGen/Providers/FigmaNodes/DefaultFigmaNodesProvider.swift b/Sources/FigmaGen/Providers/FigmaNodes/DefaultFigmaNodesProvider.swift new file mode 100644 index 0000000..63be77e --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaNodes/DefaultFigmaNodesProvider.swift @@ -0,0 +1,75 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +final class DefaultFigmaNodesProvider: FigmaNodesProvider { + + // MARK: - Instance Methods + + private func extractNodes( + from node: FigmaNode, + including includedNodeIDs: inout Set, + excluding excludedNodeIDs: inout Set, + forceInclude: Bool + ) -> [FigmaNode] { + let isIncludedNode = includedNodeIDs.remove(node.id) != nil + let isExcludedNode = excludedNodeIDs.remove(node.id) != nil + + var nodes: [FigmaNode] = [] + + guard !isExcludedNode else { + return nodes + } + + if isIncludedNode || forceInclude { + nodes.append(node) + } + + guard let children = node.children, !children.isEmpty else { + return nodes + } + + return children + .flatMap { child in + extractNodes( + from: child, + including: &includedNodeIDs, + excluding: &excludedNodeIDs, + forceInclude: forceInclude || isIncludedNode + ) + } + .prepending(contentsOf: nodes) + } + + private func resolveNodeID(_ nodeID: String) throws -> String { + guard let unescapedNodeID = nodeID.removingPercentEncoding else { + throw FigmaNodesProviderError.invalidNodeID(nodeID) + } + + return unescapedNodeID + } + + private func resolveNodeIDs(_ nodeIDs: [String]?, defaultNodeIDs: Set) throws -> Set { + guard let nodeIDs, !nodeIDs.isEmpty else { + return defaultNodeIDs + } + + return try Set(nodeIDs.map { try resolveNodeID($0) }) + } + + // MARK: - + + func fetchNodes(_ nodes: NodesParameters, from file: FigmaFile) -> Promise<[FigmaNode]> { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + var includedNodeIDs = try self.resolveNodeIDs(nodes.includedIDs, defaultNodeIDs: [file.document.id]) + var excludedNodeIDs = try self.resolveNodeIDs(nodes.excludedIDs, defaultNodeIDs: []) + + return self.extractNodes( + from: file.document, + including: &includedNodeIDs, + excluding: &excludedNodeIDs, + forceInclude: false + ) + } + } +} diff --git a/Sources/FigmaGen/Providers/FigmaNodes/FigmaNodesProvider.swift b/Sources/FigmaGen/Providers/FigmaNodes/FigmaNodesProvider.swift new file mode 100644 index 0000000..5de30b6 --- /dev/null +++ b/Sources/FigmaGen/Providers/FigmaNodes/FigmaNodesProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol FigmaNodesProvider { + + // MARK: - Instance Methods + + func fetchNodes(_ nodes: NodesParameters, from file: FigmaFile) -> Promise<[FigmaNode]> +} diff --git a/Sources/Services/Nodes/NodesError.swift b/Sources/FigmaGen/Providers/FigmaNodes/FigmaNodesProviderError.swift similarity index 58% rename from Sources/Services/Nodes/NodesError.swift rename to Sources/FigmaGen/Providers/FigmaNodes/FigmaNodesProviderError.swift index 78fad21..33a2926 100644 --- a/Sources/Services/Nodes/NodesError.swift +++ b/Sources/FigmaGen/Providers/FigmaNodes/FigmaNodesProviderError.swift @@ -1,12 +1,6 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -enum NodesError: Error, CustomStringConvertible { +enum FigmaNodesProviderError: Error, CustomStringConvertible { // MARK: - Enumeration Cases @@ -17,7 +11,7 @@ enum NodesError: Error, CustomStringConvertible { var description: String { switch self { case let .invalidNodeID(nodeID): - return "Figma node ID '\(nodeID)' is invalid" + return "Figma node ID '\(nodeID)' is invalid and cannot be used to exclude or include" } } } diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyCodable+extension.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyCodable+extension.swift new file mode 100644 index 0000000..b020aa7 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyCodable+extension.swift @@ -0,0 +1,42 @@ +import Foundation +import FigmaGenTools + +extension AnyCodable { + + func getAllGitHubTokenValues() -> [GitHubTokenValue] { + var gitHubTokenValues: [GitHubTokenValue] = [] + + guard let dictionary = self.value as? [String: Any] else { + return gitHubTokenValues + } + + guard let data = try? JSONEncoder().encode(self) else { + return [] + } + + if let value = try? JSONDecoder().decode(GitHubTokenValue.self, from: data) { + return [value] + } else if let values = try? JSONDecoder().decode([String: GitHubTokenValue].self, from: data) { + let valuesResult = values.compactMap { key, value in + let name = value.name ?? key + return value.copyWith(name: name) + } + gitHubTokenValues.append(contentsOf: valuesResult) + } else { + let results = dictionary.map { key, value in + let tokenValues = AnyCodable(value).getAllGitHubTokenValues() + return tokenValues.map { value in + var name = key + if let valueName = value.name { + name += ".\(valueName)" + } + return value.copyWith(name: name) + } + } + + gitHubTokenValues.append(contentsOf: results.flatMap { $0 }) + } + + return gitHubTokenValues + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyKey.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyKey.swift new file mode 100644 index 0000000..0c67543 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/AnyKey.swift @@ -0,0 +1,28 @@ +import Foundation + +struct AnyKey: CodingKey { + + enum Errors: Error { + case invalidKeyName + } + + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + self.intValue = Int(stringValue) + } + + init?(intValue: Int) { + self.intValue = intValue + stringValue = "\(intValue)" + } + + static func key(named name: String) throws -> Self { + guard let key = Self(stringValue: name) else { + throw Errors.invalidKeyName + } + return key + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubError.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubError.swift new file mode 100644 index 0000000..ddb09ef --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubError.swift @@ -0,0 +1,25 @@ +import FigmaGenTools +import Foundation + +struct GitHubError: Error, Hashable, CustomStringConvertible { + + // MARK: - Instance Properties + + let code: Int + let content: String + + // MARK: - CustomStringConvertible + + init(_ error: HTTPError) { + code = error.statusCode?.rawValue ?? 0 + content = error.statusCode?.httpErrorDescription ?? "" + } + + var description: String { + if 400...404 ~= code { + return "\(type(of: self)) \(content) (access token error)" + } + + return "\(type(of: self)) \(content)" + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubFile.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubFile.swift new file mode 100644 index 0000000..bcfb623 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubFile.swift @@ -0,0 +1,46 @@ +import Foundation +import FigmaGenTools + +struct GitHubFile: Codable, Hashable { + + let tokenValues: [String: [GitHubTokenValue]] + + enum CodingKeys: String, CodingKey { + case results + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: AnyKey.self) + + tokenValues = container.allKeys + .map { key -> (key: AnyKey, values: [GitHubTokenValue]?) in + let parametersForKey = try? container.decodeIfPresent([String: AnyCodable].self, forKey: key) + let tokenValues = parametersForKey? + .map { key, value in + let result: [GitHubTokenValue] = value + .getAllGitHubTokenValues() + .map { githubValue in + var name = key + if let valueName = githubValue.name { + name += ".\(valueName)" + } + return githubValue.copyWith(name: name) + } + .sorted(by: { $0.name ?? "" < $1.name ?? "" }) + return result + } + .flatMap { $0 } + + return (key: key, values: tokenValues) + } + .reduce(into: [String: [GitHubTokenValue]]()) { partialResult, mapResult in + partialResult[mapResult.key.stringValue] = mapResult.values + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(tokenValues) + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubTokenValue.swift b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubTokenValue.swift new file mode 100644 index 0000000..38c2eab --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/DTOs/GitHubTokenValue.swift @@ -0,0 +1,249 @@ +import Foundation + +struct GitHubTokenValue: Hashable { + + // MARK: - Instance Properties + + let name: String? + let type: TokenValueType + let typeName: String + + func copyWith(name: String? = nil, type: TokenValueType? = nil, typeName: String? = nil) -> Self { + Self( + name: name ?? self.name, + type: type ?? self.type, + typeName: typeName ?? self.typeName + ) + } +} + +// MARK: - Decodable + +extension GitHubTokenValue: Decodable { + + // MARK: - Nested Types + + private enum RawType: String, Decodable { + + case a11yScales + case animation + case animationTime + case animationEaseBase + case animationEaseSpring + case border + case borderRadius + case borderWidth + case boxShadow + case color + case core + case dimension + case fontFamilies + case fontSizes + case fontWeights + case letterSpacing + case lineHeights + case opacity + case paragraphSpacing + case scaling + case sizing + case spacing + case textCase + case textDecoration + case typography + + case unknown + } + + fileprivate enum CodingKeys: String, CodingKey { + case type + case name + case value + } + + // MARK: - Initializers + + init(from decoder: Decoder) throws { + // swiftlint:disable:previous function_body_length + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.name = try container.decodeIfPresent(forKey: .name) + + self.typeName = try container.decode(String.self, forKey: .type) + let rawType = RawType(rawValue: typeName) ?? .unknown + + switch rawType { + case .a11yScales: + self.type = .a11yScales(value: try container.decode(forKey: .value)) + + case .animation: + self.type = .animation(value: try container.decode(forKey: .value)) + + case .animationEaseBase: + self.type = .animationEaseBase(value: try container.decode(forKey: .value)) + + case .animationEaseSpring: + self.type = .animationEaseSpring(value: try container.decode(forKey: .value)) + + case .animationTime: + self.type = .animationTime(value: try container.decode(forKey: .value)) + + case .borderRadius: + self.type = .borderRadius(value: try container.decode(forKey: .value)) + + case .boxShadow: + self.type = .boxShadow(value: try container.decode(forKey: .value)) + + case .color: + self.type = .color(value: try container.decode(forKey: .value)) + + case .core: + self.type = .core(value: try container.decode(forKey: .value)) + + case .dimension: + self.type = .dimension(value: try container.decode(forKey: .value)) + + case .fontFamilies: + self.type = .fontFamilies(value: try container.decode(forKey: .value)) + + case .fontSizes: + self.type = .fontSizes(value: try container.decode(forKey: .value)) + + case .fontWeights: + self.type = .fontWeights(value: try container.decode(forKey: .value)) + + case .letterSpacing: + self.type = .letterSpacing(value: try container.decode(forKey: .value)) + + case .lineHeights: + self.type = .lineHeights(value: try container.decode(forKey: .value)) + + case .opacity: + self.type = .opacity(value: try container.decode(forKey: .value)) + + case .paragraphSpacing: + self.type = .paragraphSpacing(value: try container.decode(forKey: .value)) + + case .scaling: + self.type = .scaling(value: try container.decode(forKey: .value)) + + case .sizing: + self.type = .sizing(value: try container.decode(forKey: .value)) + + case .spacing: + self.type = .spacing(value: try container.decode(forKey: .value)) + + case .textCase: + self.type = .textCase(value: try container.decode(forKey: .value)) + + case .textDecoration: + self.type = .textDecoration(value: try container.decode(forKey: .value)) + + case .typography: + self.type = .typography(value: try container.decode(forKey: .value)) + + case .borderWidth: + self.type = .borderWidth(value: try container.decode(forKey: .value)) + + case .border: + self.type = .border(value: try container.decode(forKey: .value)) + + case .unknown: + self.type = .unknown + } + } +} + +// MARK: - Encodable + +extension GitHubTokenValue: Encodable { + + // MARK: - Instance Methods + + func encode(to encoder: Encoder) throws { + // swiftlint:disable:previous function_body_length + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(name, forKey: .name) + try container.encode(typeName, forKey: .type) + + switch type { + case let .a11yScales(value): + try container.encode(value, forKey: .value) + + case let .animation(value): + try container.encode(value, forKey: .value) + + case let .animationEaseBase(value): + try container.encode(value, forKey: .value) + + case let .animationEaseSpring(value): + try container.encode(value, forKey: .value) + + case let .animationTime(value): + try container.encode(value, forKey: .value) + + case let .borderRadius(value): + try container.encode(value, forKey: .value) + + case let .boxShadow(value): + try container.encode(value, forKey: .value) + + case let .color(value): + try container.encode(value, forKey: .value) + + case let .core(value): + try container.encode(value, forKey: .value) + + case let .dimension(value): + try container.encode(value, forKey: .value) + + case let .fontFamilies(value): + try container.encode(value, forKey: .value) + + case let .fontSizes(value): + try container.encode(value, forKey: .value) + + case let .fontWeights(value): + try container.encode(value, forKey: .value) + + case let .letterSpacing(value): + try container.encode(value, forKey: .value) + + case let .lineHeights(value): + try container.encode(value, forKey: .value) + + case let .opacity(value): + try container.encode(value, forKey: .value) + + case let .paragraphSpacing(value): + try container.encode(value, forKey: .value) + + case let .scaling(value): + try container.encode(value, forKey: .value) + + case let .sizing(value): + try container.encode(value, forKey: .value) + + case let .spacing(value): + try container.encode(value, forKey: .value) + + case let .textCase(value): + try container.encode(value, forKey: .value) + + case let .textDecoration(value): + try container.encode(value, forKey: .value) + + case let .typography(value): + try container.encode(value, forKey: .value) + + case let .borderWidth(value): + try container.encode(value, forKey: .value) + + case let .border(value): + try container.encode(value, forKey: .value) + + case .unknown: + try container.encodeNil(forKey: .value) + } + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIEmptyResponse.swift b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIEmptyResponse.swift new file mode 100644 index 0000000..aefca8f --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIEmptyResponse.swift @@ -0,0 +1,3 @@ +import Foundation + +struct GitHubAPIEmptyResponse: Decodable { } diff --git a/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIProvider.swift b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIProvider.swift new file mode 100644 index 0000000..a36408c --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/GitHubAPIProvider.swift @@ -0,0 +1,138 @@ +import Foundation +import PromiseKit +import FigmaGenTools + +final class GitHubAPIProvider: RemoteRepoProvider { + + // MARK: - Instance Properties + + private let queryEncoder: HTTPQueryEncoder + private let bodyEncoder: HTTPBodyEncoder + private let responseDecoder: HTTPResponseDecoder + private let baseURL = URL(string: "https://raw.githubusercontent.com")! + + // MARK: - + + let httpService: GitHubHTTPService + + init(httpService: GitHubHTTPService) { + self.httpService = httpService + + let urlEncoder = URLEncoder(boolEncodingStrategy: .literal) + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + urlEncoder.dateEncodingStrategy = .formatted(.gitHubAPI(withMilliseconds: true)) + jsonEncoder.dateEncodingStrategy = .formatted(.gitHubAPI(withMilliseconds: true)) + + jsonDecoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + if let date = DateFormatter.gitHubAPI(withMilliseconds: true).date(from: dateString) { + return date + } + + if let date = DateFormatter.gitHubAPI(withMilliseconds: false).date(from: dateString) { + return date + } + + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Date string does not match format expected by formatter" + ) + } + + self.queryEncoder = HTTPQueryURLEncoder(urlEncoder: urlEncoder) + self.bodyEncoder = HTTPBodyJSONEncoder(jsonEncoder: jsonEncoder) + self.responseDecoder = jsonDecoder + } + + private func makeHTTPRoute(for route: Route) -> HTTPRoute { + let url = baseURL + .appendingPathComponent(route.urlPath) + + let headers = route.accessToken.map { [HTTPHeader.gitHubAccessToken($0)] } ?? [] + + return HTTPRoute( + method: route.httpMethod, + url: url, + headers: headers + ) + } + + private func handleHTTPError(_ error: HTTPError) -> Error { + guard error.reason is HTTPStatusCode else { + return error + } + + return GitHubError(error) + } +} + +// MARK: - RemoteRepoProvider +extension GitHubAPIProvider { + + func request(route: Route) -> Promise { + Promise { seal in + + let task = httpService.request(route: makeHTTPRoute(for: route)) + + task.responseDecodable(type: Route.Response.self, decoder: responseDecoder) { response in + switch response.result { + case let .failure(error): + seal.reject(self.handleHTTPError(error)) + + case let .success(value): + seal.fulfill(value) + } + } + } + } + + func request(route: Route) -> Promise where Route.Response == GitHubAPIEmptyResponse { + Promise { seal in + + let task = httpService.request(route: makeHTTPRoute(for: route)) + + task.responseJSON { response in + switch response.result { + case let .failure(error): + seal.reject(self.handleHTTPError(error)) + + case .success: + seal.fulfill(Void()) + } + } + } + } +} + +extension HTTPHeader { + + // MARK: - Type Methods + + fileprivate static func gitHubAccessToken(_ value: String) -> HTTPHeader { + .authorization(bearerToken: value) + } +} + +extension DateFormatter { + + // MARK: - Type Properties + + fileprivate static func gitHubAPI(withMilliseconds: Bool) -> DateFormatter { + let dateFormatter = DateFormatter() + + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + if withMilliseconds { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + } else { + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + } + + return dateFormatter + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/GitHubHTTPService.swift b/Sources/FigmaGen/Providers/GitHubApi/GitHubHTTPService.swift new file mode 100644 index 0000000..529119d --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/GitHubHTTPService.swift @@ -0,0 +1,11 @@ +import Foundation +import FigmaGenTools + +public protocol GitHubHTTPService { + + // MARK: - Instance Methods + + func request(route: HTTPRoute) -> HTTPTask +} + +extension HTTPService: GitHubHTTPService { } diff --git a/Sources/FigmaGen/Providers/GitHubApi/RemoteRepoProvider.swift b/Sources/FigmaGen/Providers/GitHubApi/RemoteRepoProvider.swift new file mode 100644 index 0000000..bf34272 --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/RemoteRepoProvider.swift @@ -0,0 +1,10 @@ +import Foundation +import PromiseKit + +protocol RemoteRepoProvider { + + // MARK: - Instance Methods + + func request(route: Route) -> Promise + func request(route: Route) -> Promise where Route.Response == GitHubAPIEmptyResponse +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIFileRoute.swift b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIFileRoute.swift new file mode 100644 index 0000000..49b411b --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIFileRoute.swift @@ -0,0 +1,23 @@ +import Foundation +import FigmaGenTools + +struct GitHubAPIFileRoute: GitHubAPIRoute { + + typealias Response = GitHubFile + + private let owner: String + private let repo: String + private let branch: String + private let filePath: String + + let accessToken: String? + var urlPath: String { "\(owner)/\(repo)/\(branch)/\(filePath)" } + + init(owner: String, repo: String, branch: String, filePath: String, accessToken: String?) { + self.owner = owner + self.repo = repo + self.branch = branch + self.filePath = filePath + self.accessToken = accessToken + } +} diff --git a/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIRoute.swift b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIRoute.swift new file mode 100644 index 0000000..3c944ba --- /dev/null +++ b/Sources/FigmaGen/Providers/GitHubApi/Routers/GitHubAPIRoute.swift @@ -0,0 +1,28 @@ +import Foundation +import FigmaGenTools + +protocol GitHubAPIRoute { + + // MARK: - Nested Types + + associatedtype Response: Decodable + + // MARK: - Instance Properties + + var httpMethod: HTTPMethod { get } + var urlPath: String { get } + var accessToken: String? { get } +} + +extension GitHubAPIRoute { + + // MARK: - Instance Properties + + var httpMethod: HTTPMethod { + .get + } + + var accessToken: String? { + nil + } +} diff --git a/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift b/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift new file mode 100644 index 0000000..93e914e --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Assets/DefaultImageAssetsProvider.swift @@ -0,0 +1,234 @@ +import Foundation +import FigmaGenTools +import PromiseKit +import PathKit + +final class DefaultImageAssetsProvider: ImageAssetsProvider, ImagesFolderPathResolving { + + // MARK: - Instance Properties + + let assetsProvider: AssetsProvider + let dataProvider: DataProvider + + // MARK: - Initializers + + init(assetsProvider: AssetsProvider, dataProvider: DataProvider) { + self.assetsProvider = assetsProvider + self.dataProvider = dataProvider + } + + // MARK: - Instance Methods + + private func resolveName( + for node: ImageRenderedNode, + setNode: ImageComponentSetRenderedNode, + namingStyle: ImageNamingStyle + ) -> String { + let name = setNode.type == .component ? 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, + parameters: ImagesParameters, + folderPath: Path + ) -> ImageAsset { + let name = resolveName(for: node, setNode: setNode, namingStyle: parameters.namingStyle) + + let folderPath = resolveFolderPath( + groupByFrame: parameters.groupByFrame, + groupByComponentSet: parameters.groupByComponentSet, + 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: parameters.format.fileExtension) + .string + } + + return ImageAsset( + name: name, + filePaths: filePaths, + preserveVectorData: parameters.preserveVectorData, + renderAs: parameters.renderAs + ) + } + + private func makeAssets( + for nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + folderPath: Path + ) -> [ImageComponentSetAsset] { + nodes.map { setNode in + var assets: [ImageRenderedNode: ImageAsset] = [:] + + setNode.components.forEach { node in + assets[node] = makeAsset( + for: node, + setNode: setNode, + parameters: parameters, + folderPath: folderPath + ) + } + + return ImageComponentSetAsset( + name: setNode.name, + parentName: setNode.parentName, + assets: assets, + nodeType: setNode.type + ) + } + } + + private func makeAssetImageSet(for asset: ImageAsset) -> AssetImageSet { + let assetImages = asset.filePaths.map { scale, filePath in + AssetImage(fileName: Path(filePath).lastComponent, scale: scale.assetImageScale) + } + let contents = AssetImageSetContents( + info: .defaultFigmaGen, + properties: AssetImageProperties(from: asset), + images: assetImages + ) + return AssetImageSet(contents: contents) + } + + private func makeAssetImageSets(for assets: [ImageRenderedNode: ImageAsset]) -> [String: AssetImageSet] { + assets.values.reduce(into: [:]) { result, asset in + result[asset.name] = makeAssetImageSet(for: asset) + } + } + + private func saveImageFiles(node: ImageRenderedNode, asset: ImageAsset) -> Promise { + let promises = node.urls.compactMap { scale, url in + asset.filePaths[scale].map { self.dataProvider.saveData(from: url, to: $0) } + } + + return when(fulfilled: promises) + } + + private func saveAssetFolders( + assets: [ImageComponentSetAsset: AssetFolder], + groupByFrame: Bool, + groupByComponentSet: Bool, + in folderPath: String + ) throws -> Promise { + let folderPath = Path(folderPath) + + if folderPath.exists { + try folderPath.delete() + } + + let promises = assets.map { asset, folder in + assetsProvider.saveAssetFolder( + folder, + in: resolveFolderPath( + groupByFrame: groupByFrame, + groupByComponentSet: groupByComponentSet, + setAsset: asset, + folderPath: folderPath + ).string + ) + } + + return when(fulfilled: promises).asVoid() + } + + private func saveImageFiles(assets: [ImageComponentSetAsset]) -> Promise { + let promises = assets.flatMap { setAsset in + setAsset.assets.map { node, asset in + saveImageFiles(node: node, asset: asset) + } + } + + return when(fulfilled: promises) + } + + // MARK: - + + func saveImages( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + in folderPath: String + ) -> Promise<[ImageComponentSetAsset]> { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + self.makeAssets( + for: nodes, + parameters: parameters, + folderPath: Path(folderPath) + ) + }.nest { assets in + perform(on: DispatchQueue.global(qos: .userInitiated)) { + assets.reduce(into: [:]) { result, asset in + result[asset] = AssetFolder( + imageSets: self.makeAssetImageSets(for: asset.assets), + contents: AssetFolderContents(info: .defaultFigmaGen) + ) + } + }.then { assets in + try self.saveAssetFolders( + assets: assets, + groupByFrame: parameters.groupByFrame, + groupByComponentSet: parameters.groupByComponentSet, + in: folderPath + ) + }.then { + self.saveImageFiles(assets: assets) + } + } + } +} + +extension ImageScale { + + // MARK: - Instance Properties + + fileprivate var assetImageScale: AssetImageScale? { + switch self { + case .none: + return nil + + case .scale1x: + return .scale1x + + case .scale2x: + return .scale2x + + case .scale3x: + return .scale3x + } + } +} + +extension AssetImageProperties { + + fileprivate init?(from imageAsset: ImageAsset) { + self.init( + templateRenderingIntent: imageAsset.renderAs.map { AssetImageTemplateRenderingIntent(from: $0) }, + preserveVectorRepresentation: imageAsset.preserveVectorData + ) + } +} + +extension AssetImageTemplateRenderingIntent { + + fileprivate init(from renderingIntent: ImageRenderingMode) { + switch renderingIntent { + case .original: + self = .original + + case .template: + self = .template + } + } +} diff --git a/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift b/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift new file mode 100644 index 0000000..2eb7389 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Assets/ImageAssetsProvider.swift @@ -0,0 +1,13 @@ +import Foundation +import PromiseKit + +protocol ImageAssetsProvider { + + // MARK: - Instance Methods + + func saveImages( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + in folderPath: String + ) -> Promise<[ImageComponentSetAsset]> +} diff --git a/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift b/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift new file mode 100644 index 0000000..889dc92 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/DefaultImagesProvider.swift @@ -0,0 +1,245 @@ +import Foundation +import FigmaGenTools +import PromiseKit + +final class DefaultImagesProvider: ImagesProvider { + + // MARK: - Instance Properties + + let filesProvider: FigmaFilesProvider + let nodesProvider: FigmaNodesProvider + let imageRenderProvider: ImageRenderProvider + let imageAssetsProvider: ImageAssetsProvider + let imageResourcesProvider: ImageResourcesProvider + + // MARK: - Initializers + + init( + filesProvider: FigmaFilesProvider, + nodesProvider: FigmaNodesProvider, + imageRenderProvider: ImageRenderProvider, + imageAssetsProvider: ImageAssetsProvider, + imageResourcesProvider: ImageResourcesProvider + ) { + self.filesProvider = filesProvider + self.nodesProvider = nodesProvider + self.imageRenderProvider = imageRenderProvider + self.imageAssetsProvider = imageAssetsProvider + self.imageResourcesProvider = imageResourcesProvider + } + + // MARK: - Instance Methods + + private func extractImageNode( + from node: FigmaNode, + info: FigmaFrameNodeInfo, + components: [String: FigmaComponent], + onlyExportables: Bool + ) throws -> ImageNode? { + guard !onlyExportables || !info.exportSettings.isEmptyOrNil else { + return nil + } + + guard let nodeComponent = components[node.id] else { + throw ImagesProviderError(code: .componentNotFound, nodeID: node.id, nodeName: node.name) + } + + guard let nodeComponentName = nodeComponent.name, !nodeComponentName.isEmpty else { + throw ImagesProviderError(code: .invalidComponentName, nodeID: node.id, nodeName: node.name) + } + + return ImageNode( + id: node.id, + name: nodeComponentName, + description: nodeComponent.description + ) + } + + private func extractImageNode( + from node: FigmaNode, + components: [String: FigmaComponent], + onlyExportables: Bool + ) throws -> ImageNode? { + guard case .component(let info) = node.type else { + return nil + } + + return try extractImageNode( + from: node, + info: info, + components: components, + onlyExportables: onlyExportables + ) + } + + private func extractImageSetNode( + from node: FigmaNode, + components: [String: FigmaComponent], + onlyExportables: Bool + ) throws -> ImageComponentSetNode? { + switch node.type { + case .component(let info): + guard components[node.id]?.componentSetID == nil else { + return nil + } + + let imageNode = try extractImageNode( + from: node, + info: info, + components: components, + onlyExportables: onlyExportables + ) + + guard let imageNode else { + return nil + } + + return ImageComponentSetNode(name: imageNode.name, parentName: node.parent?.name, component: imageNode) + + case .componentSet: + guard let children = node.children else { + return nil + } + + let nodes = try children + .lazy + .filter { $0.isVisible ?? true } + .compactMap { node in + try extractImageNode( + from: node, + components: components, + onlyExportables: onlyExportables + ) + } + + guard let nodeComponentSetName = node.name, !nodeComponentSetName.isEmpty else { + throw ImagesProviderError(code: .invalidComponentName, nodeID: node.id, nodeName: node.name) + } + + return ImageComponentSetNode(name: nodeComponentSetName, parentName: node.parent?.name, components: nodes) + + default: + return nil + } + } + + private func extractImageSetNodes( + from nodes: [FigmaNode], + of file: FigmaFile, + onlyExportables: Bool + ) throws -> [ImageComponentSetNode] { + let components = file.components ?? [:] + + return try nodes + .lazy + .filter { $0.isVisible ?? true } + .compactMap { node in + try extractImageSetNode( + from: node, + components: components, + onlyExportables: onlyExportables + ) + } + .reduce(into: []) { result, node in + if !result.contains(node) { + result.append(node) + } + } + } + + private func saveAssetImagesIfNeeded( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + in assets: String? + ) -> Promise<[ImageComponentSetAsset]> { + assets.map { folderPath in + imageAssetsProvider.saveImages( + nodes: nodes, + parameters: parameters, + in: folderPath + ) + } ?? .value([]) + } + + private func saveResourceImagesIfNeeded( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + in resources: String? + ) -> Promise<[ImageRenderedNode: ImageResource]> { + resources.map { folderPath in + imageResourcesProvider.saveImages( + nodes: nodes, + parameters: parameters, + in: folderPath + ) + } ?? .value([:]) + } + + private func saveAssetImagesIfNeeded( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters + ) -> Promise<[ImageSet]> { + when( + fulfilled: self.saveAssetImagesIfNeeded( + nodes: nodes, + parameters: parameters, + in: parameters.assets + ), + self.saveResourceImagesIfNeeded( + nodes: nodes, + parameters: parameters, + in: parameters.resources + ) + ) + .map { assets, resources in + nodes.map { node in + ImageSet( + name: node.name, + images: node.components.map { imageNode in + Image( + node: imageNode, + format: parameters.format, + asset: assets + .first { $0.name == node.name }? + .assets[imageNode], + resource: resources[imageNode] + ) + } + ) + } + } + } + + // MARK: - + + func fetchImages( + from file: FileParameters, + nodes: NodesParameters, + parameters: ImagesParameters + ) -> Promise<[ImageSet]> { + firstly { + self.filesProvider.fetchFile(file) + }.then { figmaFile in + self.nodesProvider.fetchNodes(nodes, from: figmaFile).map { figmaNodes in + try self.extractImageSetNodes( + from: figmaNodes, + of: figmaFile, + onlyExportables: parameters.onlyExportables + ) + } + }.then { nodes in + self.imageRenderProvider.renderImages( + of: file, + nodes: nodes, + format: parameters.format, + scales: parameters.scales, + useAbsoluteBounds: parameters.useAbsoluteBounds + ) + }.then { nodes in + self.saveAssetImagesIfNeeded( + nodes: nodes, + parameters: parameters + ) + } + } +} diff --git a/Sources/FigmaGen/Providers/Images/ImagesFolderPathResolving.swift b/Sources/FigmaGen/Providers/Images/ImagesFolderPathResolving.swift new file mode 100644 index 0000000..f5819a4 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/ImagesFolderPathResolving.swift @@ -0,0 +1,74 @@ +import Foundation +import PathKit + +protocol ImagesFolderPathResolving { + + // MARK: - Instance Methods + + func resolveFolderPath( + groupByFrame: Bool, + groupByComponentSet: Bool, + parentNodeName: String?, + isSingleComponent: Bool, + nodeName: String, + folderPath: Path + ) -> Path +} + +extension ImagesFolderPathResolving { + + // MARK: - Instance Methods + + func resolveFolderPath( + groupByFrame: Bool, + groupByComponentSet: Bool, + parentNodeName: String?, + isSingleComponent: Bool, + nodeName: String, + folderPath: Path + ) -> Path { + var folderPath = folderPath + + if groupByFrame, let name = parentNodeName { + folderPath = folderPath.appending(name.camelized) + } + + if groupByComponentSet, !isSingleComponent { + folderPath = folderPath.appending(nodeName.camelized) + } + + return folderPath + } + + func resolveFolderPath( + groupByFrame: Bool, + groupByComponentSet: Bool, + setNode: ImageComponentSetRenderedNode, + folderPath: Path + ) -> Path { + resolveFolderPath( + groupByFrame: groupByFrame, + groupByComponentSet: groupByComponentSet, + parentNodeName: setNode.parentName, + isSingleComponent: setNode.type == .component, + nodeName: setNode.name, + folderPath: folderPath + ) + } + + func resolveFolderPath( + groupByFrame: Bool, + groupByComponentSet: Bool, + setAsset: ImageComponentSetAsset, + folderPath: Path + ) -> Path { + resolveFolderPath( + groupByFrame: groupByFrame, + groupByComponentSet: groupByComponentSet, + parentNodeName: setAsset.parentName, + isSingleComponent: setAsset.nodeType == .component, + nodeName: setAsset.name, + folderPath: folderPath + ) + } +} diff --git a/Sources/FigmaGen/Providers/Images/ImagesProvider.swift b/Sources/FigmaGen/Providers/Images/ImagesProvider.swift new file mode 100644 index 0000000..9097d40 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/ImagesProvider.swift @@ -0,0 +1,13 @@ +import Foundation +import PromiseKit + +protocol ImagesProvider { + + // MARK: - Instance Methods + + func fetchImages( + from file: FileParameters, + nodes: NodesParameters, + parameters: ImagesParameters + ) -> Promise<[ImageSet]> +} diff --git a/Sources/FigmaGen/Providers/Images/ImagesProviderError.swift b/Sources/FigmaGen/Providers/Images/ImagesProviderError.swift new file mode 100644 index 0000000..be869d7 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/ImagesProviderError.swift @@ -0,0 +1,31 @@ +import Foundation + +struct ImagesProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case componentNotFound + case invalidComponentName + } + + // MARK: - Instance Properties + + let code: Code + let nodeID: String + let nodeName: String? + + // MARK: - CustomStringConvertible + + var description: String { + let nodeName = self.nodeName ?? "nil" + + switch code { + case .componentNotFound: + return "Figma file does not contain a valid component for node \(nodeName) ('\(nodeID)')" + + case .invalidComponentName: + return "Component name of node \(nodeName) ('\(nodeID)') is either empty or nil" + } + } +} diff --git a/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift b/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift new file mode 100644 index 0000000..b8fbf5d --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Render/DefaultImageRenderProvider.swift @@ -0,0 +1,167 @@ +import Foundation +import PromiseKit + +final class DefaultImageRenderProvider: ImageRenderProvider { + + // MARK: - Instance Properties + + let apiProvider: FigmaAPIProvider + + // MARK: - Initializers + + init(apiProvider: FigmaAPIProvider) { + self.apiProvider = apiProvider + } + + // MARK: - Instance Methods + + private func extractImageURL(from rawURLs: [String: String?], node: ImageNode) throws -> URL { + guard let rawURL = rawURLs[node.id]?.flatMap({ $0 }) else { + throw ImageRenderProviderError(code: .invalidImage, nodeID: node.id, nodeName: node.name) + } + + guard let url = URL(string: rawURL) else { + throw ImageRenderProviderError(code: .invalidImageURL, nodeID: node.id, nodeName: node.name) + } + + return url + } + + private func extractImageURLs(from rawURLs: [String: String?], nodes: [ImageNode]) throws -> [ImageNode: URL] { + var urls: [ImageNode: URL] = [:] + + try nodes.forEach { node in + urls[node] = try self.extractImageURL(from: rawURLs, node: node) + } + + return urls + } + + private func makeImageSetRenderedNode( + for node: ImageComponentSetNode, + imageURLs: [ImageScale: [ImageNode: URL]] + ) -> ImageComponentSetRenderedNode { + let scales = imageURLs.keys + + let renderedNodes = node.components.map { imageNode in + let nodeImageURLs = scales.reduce(into: [:]) { result, scale in + result[scale] = imageURLs[scale]?[imageNode] + } + + return ImageRenderedNode(base: imageNode, urls: nodeImageURLs) + } + + return ImageComponentSetRenderedNode( + name: node.name, + parentName: node.parentName, + components: renderedNodes, + type: node.type + ) + } + + private func renderImages( + of file: FileParameters, + nodes: [ImageComponentSetNode], + format: ImageFormat, + scale: ImageScale, + useAbsoluteBounds: Bool + ) -> Promise<[ImageNode: URL]> { + let route = FigmaAPIImagesRoute( + accessToken: file.accessToken, + fileKey: file.key, + fileVersion: file.version, + nodeIDs: nodes + .flatMap { $0.components } + .map { $0.id }, + format: format.figmaFormat, + scale: scale.figmaScale, + useAbsoluteBounds: useAbsoluteBounds + ) + + return firstly { + self.apiProvider.request(route: route) + }.map(on: DispatchQueue.global(qos: .userInitiated)) { images in + try self.extractImageURLs( + from: images.urls, + nodes: nodes.flatMap { $0.components } + ) + } + } + + // MARK: - + + func renderImages( + of file: FileParameters, + nodes: [ImageComponentSetNode], + format: ImageFormat, + scales: [ImageScale], + useAbsoluteBounds: Bool + ) -> Promise<[ImageComponentSetRenderedNode]> { + guard !nodes.isEmpty else { + return .value([]) + } + + let promises = scales + .map { scale in + nodes + .chunked(size: 100) + .map { nodesPars in + renderImages(of: file, nodes: nodesPars, format: format, scale: scale, useAbsoluteBounds: useAbsoluteBounds) + .map { imageURLs in + (scale: scale, imageURLs: imageURLs) + } + } + } + .flatMap { $0 } + + return firstly { + when(fulfilled: promises) + }.map(on: DispatchQueue.global(qos: .userInitiated)) { imageURLs in + imageURLs.reduce(into: [ImageScale: [ImageNode: URL]]()) { result, next in + result[next.scale] = result[next.scale]?.merging(next.imageURLs, uniquingKeysWith: { $1 }) + ?? next.imageURLs + } + }.map(on: DispatchQueue.global(qos: .userInitiated)) { imageURLs in + nodes.map { self.makeImageSetRenderedNode(for: $0, imageURLs: imageURLs) } + } + } +} + +extension ImageFormat { + + // MARK: - Instance Properties + + fileprivate var figmaFormat: FigmaImageFormat { + switch self { + case .pdf: + return .pdf + + case .png: + return .png + + case .jpg: + return .jpg + + case .svg: + return .svg + } + } +} + +extension ImageScale { + + // MARK: - Instance Properties + + fileprivate var figmaScale: Double { + switch self { + case .none, .scale1x: + return 1.0 + + case .scale2x: + return 2.0 + + case .scale3x: + return 3.0 + } + } +} diff --git a/Sources/FigmaGen/Providers/Images/Render/ImageRenderProvider.swift b/Sources/FigmaGen/Providers/Images/Render/ImageRenderProvider.swift new file mode 100644 index 0000000..796ccd4 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Render/ImageRenderProvider.swift @@ -0,0 +1,15 @@ +import Foundation +import PromiseKit + +protocol ImageRenderProvider { + + // MARK: - Instance Methods + + func renderImages( + of file: FileParameters, + nodes: [ImageComponentSetNode], + format: ImageFormat, + scales: [ImageScale], + useAbsoluteBounds: Bool + ) -> Promise<[ImageComponentSetRenderedNode]> +} diff --git a/Sources/FigmaGen/Providers/Images/Render/ImageRenderProviderError.swift b/Sources/FigmaGen/Providers/Images/Render/ImageRenderProviderError.swift new file mode 100644 index 0000000..92971e6 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Render/ImageRenderProviderError.swift @@ -0,0 +1,31 @@ +import Foundation + +struct ImageRenderProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case invalidImage + case invalidImageURL + } + + // MARK: - Instance Properties + + let code: Code + let nodeID: String + let nodeName: String? + + // MARK: - CustomStringConvertible + + var description: String { + let nodeName = self.nodeName ?? "nil" + + switch code { + case .invalidImage: + return "Image for node \(nodeName) ('\(nodeID)') cannot be rendered" + + case .invalidImageURL: + return "Image for node \(nodeName) ('\(nodeID)') was rendered with an invalid URL" + } + } +} diff --git a/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift b/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift new file mode 100644 index 0000000..6e8fddd --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Resources/DefaultImageResourcesProvider.swift @@ -0,0 +1,138 @@ +import Foundation +import FigmaGenTools +import PromiseKit +import PathKit + +final class DefaultImageResourcesProvider: ImageResourcesProvider, ImagesFolderPathResolving { + + // MARK: - Instance Properties + + let dataProvider: DataProvider + let postProcessor: ImageResourcesPostProcessor + + // MARK: - Initializers + + init(dataProvider: DataProvider, postProcessor: ImageResourcesPostProcessor) { + self.dataProvider = dataProvider + self.postProcessor = postProcessor + } + + // MARK: - Instance Methods + + private func resolveFileName( + for node: ImageRenderedNode, + setNode: ImageComponentSetRenderedNode, + namingStyle: ImageNamingStyle + ) -> String { + let fileName = setNode.type == .component ? 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, + parameters: ImagesParameters, + folderPath: Path + ) -> ImageResource { + let fileName = resolveFileName(for: node, setNode: setNode, namingStyle: parameters.namingStyle) + + let folderPath = resolveFolderPath( + groupByFrame: parameters.groupByFrame, + groupByComponentSet: parameters.groupByComponentSet, + setNode: setNode, + folderPath: folderPath + ) + + let fileExtension = parameters.format.fileExtension + + let filePaths = node.urls.keys.reduce(into: [:]) { result, scale in + result[scale] = folderPath + .appending(fileName: fileName.appending(scale.fileNameSuffix), extension: fileExtension) + .string + } + + return ImageResource(fileName: fileName, fileExtension: fileExtension, filePaths: filePaths) + } + + private func makeResources( + for nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + folderPath: Path + ) -> [ImageRenderedNode: ImageResource] { + var resources: [ImageRenderedNode: ImageResource] = [:] + + nodes.forEach { setNode in + setNode.components.forEach { node in + resources[node] = makeResource( + for: node, + setNode: setNode, + parameters: parameters, + folderPath: folderPath + ) + } + } + + return resources + } + + private func saveImageFiles( + node: ImageRenderedNode, + resource: ImageResource, + postProcessor: String? + ) -> Promise { + let promises = node.urls.compactMap { scale, url in + resource.filePaths[scale].map { filePath in + self.dataProvider + .saveData(from: url, to: filePath) + .done { + if let postProcessor { + try self.postProcessor.execute(postProcessorPath: postProcessor, filePath: filePath) + } + } + } + } + + return when(fulfilled: promises) + } + + private func saveImageFiles( + resources: [ImageRenderedNode: ImageResource], + postProcessor: String?, + in folderPath: Path + ) throws -> Promise { + if folderPath.exists { + try folderPath.delete() + } + + let promises = resources.map { node, resource in + saveImageFiles(node: node, resource: resource, postProcessor: postProcessor) + } + + return when(fulfilled: promises) + } + + // MARK: - + + func saveImages( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + in folderPath: String + ) -> Promise<[ImageRenderedNode: ImageResource]> { + perform(on: DispatchQueue.global(qos: .userInitiated)) { + self.makeResources( + for: nodes, + parameters: parameters, + folderPath: Path(folderPath) + ) + }.nest { resources in + try self.saveImageFiles(resources: resources, postProcessor: parameters.postProcessor, in: Path(folderPath)) + } + } +} diff --git a/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift new file mode 100644 index 0000000..ea39d88 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesPostProcessor.swift @@ -0,0 +1,34 @@ +import Foundation +import PathKit + +final class ImageResourcesPostProcessor { + + // MARK: - Instance Methods + + @discardableResult + private func shell(_ command: String) throws -> String? { + let task = Process() + let pipe = Pipe() + + task.standardOutput = pipe + task.standardError = pipe + task.arguments = ["-c", command] + task.executableURL = URL(fileURLWithPath: "/bin/zsh") + task.standardInput = nil + + try task.run() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + + return String(data: data, encoding: .utf8) + } + + // 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 new file mode 100644 index 0000000..6651724 --- /dev/null +++ b/Sources/FigmaGen/Providers/Images/Resources/ImageResourcesProvider.swift @@ -0,0 +1,13 @@ +import Foundation +import PromiseKit + +protocol ImageResourcesProvider { + + // MARK: - Instance Methods + + func saveImages( + nodes: [ImageComponentSetRenderedNode], + parameters: ImagesParameters, + in folderPath: String + ) -> Promise<[ImageRenderedNode: ImageResource]> +} diff --git a/Sources/FigmaGen/Providers/ShadowStyles/DefaultShadowStylesProvider.swift b/Sources/FigmaGen/Providers/ShadowStyles/DefaultShadowStylesProvider.swift new file mode 100644 index 0000000..1eb2977 --- /dev/null +++ b/Sources/FigmaGen/Providers/ShadowStyles/DefaultShadowStylesProvider.swift @@ -0,0 +1,135 @@ +import Foundation +import PromiseKit + +final class DefaultShadowStylesProvider: ShadowStylesProvider { + + // MARK: - Instance Properties + + let filesProvider: FigmaFilesProvider + let nodesProvider: FigmaNodesProvider + + // MARK: - Initializers + + init(filesProvider: FigmaFilesProvider, nodesProvider: FigmaNodesProvider) { + self.filesProvider = filesProvider + self.nodesProvider = nodesProvider + } + + // MARK: - Instance Methods + + private func extractVector(from figmaEffect: FigmaEffect, of node: FigmaNode) throws -> Vector { + guard let effectOffset = figmaEffect.offset else { + throw ShadowStylesProviderError(code: .offsetNotFound, nodeID: node.id, nodeName: node.name) + } + + return Vector(x: effectOffset.x, y: effectOffset.y) + } + + private func extractColor(from figmaEffect: FigmaEffect, of node: FigmaNode) throws -> Color { + guard let effectColor = figmaEffect.color else { + throw ShadowStylesProviderError(code: .colorNotFound, nodeID: node.id, nodeName: node.name) + } + + return Color( + red: effectColor.red, + green: effectColor.green, + blue: effectColor.blue, + alpha: effectColor.alpha + ) + } + + private func extractShadowType(from effect: FigmaEffect) -> ShadowType? { + switch effect.type { + case .dropShadow: + return .drop + + case .innerShadow: + return .inner + + case .backgroundBlur, .layerBlur, nil: + return nil + } + } + + private func extractShadow(from effect: FigmaEffect, of node: FigmaNode) throws -> Shadow? { + guard let shadowType = extractShadowType(from: effect) else { + return nil + } + + return Shadow( + type: shadowType, + offset: try extractVector(from: effect, of: node), + radius: effect.radius ?? 0.0, + color: try extractColor(from: effect, of: node), + blendMode: effect.rawBlendMode + ) + } + + private func extractShadows(from nodeInfo: FigmaVectorNodeInfo, of node: FigmaNode) throws -> [Shadow] { + let nodeEffects = nodeInfo.effects?.filter { nodeEffect in + nodeEffect.isVisible ?? true + } ?? [] + + return try nodeEffects.compactMap { try extractShadow(from: $0, of: node) } + } + + private func extractShadowStyleNode( + from node: FigmaNode, + styles: [String: FigmaStyle] + ) throws -> ShadowStyleNode? { + guard let nodeInfo = node.vectorInfo, let nodeStyleID = nodeInfo.styleID(of: .effect) else { + return nil + } + + let shadows = try extractShadows(from: nodeInfo, of: node) + + guard !shadows.isEmpty else { + return nil + } + + guard let nodeStyle = styles[nodeStyleID], nodeStyle.type == .effect else { + throw ShadowStylesProviderError(code: .styleNotFound, nodeID: node.id, nodeName: node.name) + } + + guard let nodeStyleName = nodeStyle.name, !nodeStyleName.isEmpty else { + throw ShadowStylesProviderError(code: .invalidStyleName, nodeID: node.id, nodeName: node.name) + } + + return ShadowStyleNode( + name: nodeStyleName, + description: nodeStyle.description, + shadows: shadows + ) + } + + private func extractShadowStyleNodes( + from nodes: [FigmaNode], + of file: FigmaFile + ) throws -> [ShadowStyleNode] { + let styles = file.styles ?? [:] + + return try nodes + .lazy + .filter { $0.isVisible ?? true } + .compactMap { try extractShadowStyleNode(from: $0, styles: styles) } + .reduce(into: []) { result, node in + if !result.contains(node) { + result.append(node) + } + } + } + + // MARK: - + + func fetchShadowStyles(from file: FileParameters, nodes: NodesParameters) -> Promise<[ShadowStyle]> { + firstly { + self.filesProvider.fetchFile(file) + }.then { figmaFile in + self.nodesProvider.fetchNodes(nodes, from: figmaFile).map { figmaNodes in + try self.extractShadowStyleNodes(from: figmaNodes, of: figmaFile) + } + }.mapValues { node in + ShadowStyle(node: node) + } + } +} diff --git a/Sources/FigmaGen/Providers/ShadowStyles/ShadowStylesProvider.swift b/Sources/FigmaGen/Providers/ShadowStyles/ShadowStylesProvider.swift new file mode 100644 index 0000000..6dd710b --- /dev/null +++ b/Sources/FigmaGen/Providers/ShadowStyles/ShadowStylesProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol ShadowStylesProvider { + + // MARK: - Instance Methods + + func fetchShadowStyles(from file: FileParameters, nodes: NodesParameters) -> Promise<[ShadowStyle]> +} diff --git a/Sources/FigmaGen/Providers/ShadowStyles/ShadowStylesProviderError.swift b/Sources/FigmaGen/Providers/ShadowStyles/ShadowStylesProviderError.swift new file mode 100644 index 0000000..383d5cc --- /dev/null +++ b/Sources/FigmaGen/Providers/ShadowStyles/ShadowStylesProviderError.swift @@ -0,0 +1,48 @@ +import Foundation + +struct ShadowStylesProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case styleNotFound + case invalidStyleName + + case shadowEffectsNotFound + case colorNotFound + case offsetNotFound + } + + // MARK: - Instance Properties + + let code: Code + let nodeID: String + let nodeName: String? + + var nodeInfo: String { + let nodeName = self.nodeName ?? "nil" + + return "\(nodeName) ('\(nodeID)')" + } + + // MARK: - CustomStringConvertible + + var description: String { + switch code { + case .styleNotFound: + return "Figma file does not contain a valid style for node \(nodeInfo)" + + case .invalidStyleName: + return "Style name of node \(nodeInfo) is either empty or nil" + + case .shadowEffectsNotFound: + return "Figma file does not contain any shadow effects for node \(nodeInfo)" + + case .colorNotFound: + return "Shadow color of node \(nodeInfo) could not be found" + + case .offsetNotFound: + return "Shadow offset of node \(nodeInfo) could not be found" + } + } +} diff --git a/Sources/FigmaGen/Providers/TextStyles/DefaultTextStylesProvider.swift b/Sources/FigmaGen/Providers/TextStyles/DefaultTextStylesProvider.swift new file mode 100644 index 0000000..18b230b --- /dev/null +++ b/Sources/FigmaGen/Providers/TextStyles/DefaultTextStylesProvider.swift @@ -0,0 +1,159 @@ +import Foundation +import PromiseKit + +final class DefaultTextStylesProvider: TextStylesProvider { + + // MARK: - Instance Properties + + let filesProvider: FigmaFilesProvider + let nodesProvider: FigmaNodesProvider + + // MARK: - Initializers + + init(filesProvider: FigmaFilesProvider, nodesProvider: FigmaNodesProvider) { + self.filesProvider = filesProvider + self.nodesProvider = nodesProvider + } + + // MARK: - Instance Methods + + private func extractColorStyleName( + from nodeInfo: FigmaVectorNodeInfo, + of node: FigmaNode, + styles: [String: FigmaStyle] + ) throws -> String? { + guard let nodeStyleID = nodeInfo.styleID(of: .fill) else { + return nil + } + + guard let nodeStyle = styles[nodeStyleID], nodeStyle.type == .fill else { + throw TextStylesProviderError(code: .colorStyleNotFound, nodeID: node.id, nodeName: node.name) + } + + guard let nodeStyleName = nodeStyle.name, !nodeStyleName.isEmpty else { + throw TextStylesProviderError(code: .invalidColorStyleName, nodeID: node.id, nodeName: node.name) + } + + return nodeStyleName + } + + private func extractColor(from nodeInfo: FigmaVectorNodeInfo, of node: FigmaNode) throws -> Color { + let nodeFills = nodeInfo.fills?.filter { nodeFill in + (nodeFill.isVisible ?? true) && (nodeFill.type == .solid) + } ?? [] + + guard let nodeFill = nodeFills.first, nodeFills.count == 1 else { + throw TextStylesProviderError(code: .invalidColor, nodeID: node.id, nodeName: node.name) + } + + guard let nodeFillColor = nodeFill.color else { + throw TextStylesProviderError(code: .colorNotFound, nodeID: node.id, nodeName: node.name) + } + + return Color( + red: nodeFillColor.red, + green: nodeFillColor.green, + blue: nodeFillColor.blue, + alpha: nodeFillColor.alpha + ) + } + + private func extractFont(from typeStyle: FigmaTypeStyle, of node: FigmaNode) throws -> Font { + guard let fontFamily = typeStyle.fontFamily, !fontFamily.isEmpty else { + throw TextStylesProviderError(code: .invalidFontFamily, nodeID: node.id, nodeName: node.name) + } + + let fontName = typeStyle.fontPostScriptName ?? .regularFontName(family: fontFamily) + + guard let fontWeight = typeStyle.fontWeight else { + throw TextStylesProviderError(code: .invalidFontWeight, nodeID: node.id, nodeName: node.name) + } + + guard let fontSize = typeStyle.fontSize else { + throw TextStylesProviderError(code: .invalidFontSize, nodeID: node.id, nodeName: node.name) + } + + return Font( + family: fontFamily, + name: fontName, + weight: fontWeight.rounded(precision: 1), + size: fontSize.rounded(precision: 1) + ) + } + + private func extractTextStyle(from node: FigmaNode, styles: [String: FigmaStyle]) throws -> TextStyle? { + guard case let .text(info: nodeInfo, payload: textNodePayload) = node.type else { + return nil + } + + guard let nodeStyleID = nodeInfo.styleID(of: .text) else { + return nil + } + + guard let nodeStyle = styles[nodeStyleID], nodeStyle.type == .text else { + throw TextStylesProviderError(code: .styleNotFound, nodeID: node.id, nodeName: node.name) + } + + guard let nodeStyleName = nodeStyle.name, !nodeStyleName.isEmpty else { + throw TextStylesProviderError(code: .invalidStyleName, nodeID: node.id, nodeName: node.name) + } + + guard let nodeTypeStyle = textNodePayload.style else { + throw TextStylesProviderError(code: .typeStyleNotFound, nodeID: node.id, nodeName: node.name) + } + + let textStyleNode = TextStyleNode( + name: nodeStyleName, + description: nodeStyle.description, + font: try extractFont(from: nodeTypeStyle, of: node), + strikethrough: nodeTypeStyle.textDecoration == .strikethrough, + underline: nodeTypeStyle.textDecoration == .underline, + paragraphSpacing: nodeTypeStyle.paragraphSpacing?.rounded(precision: 4), + paragraphIndent: nodeTypeStyle.paragraphIndent?.rounded(precision: 4), + lineHeight: nodeTypeStyle.lineHeight?.rounded(precision: 4), + letterSpacing: nodeTypeStyle.letterSpacing?.rounded(precision: 4) + ) + + let textStyleColor = TextStyleColor( + styleName: try extractColorStyleName(from: nodeInfo, of: node, styles: styles), + color: try extractColor(from: nodeInfo, of: node) + ) + + return TextStyle(node: textStyleNode, color: textStyleColor) + } + + private func extractTextStyles(from nodes: [FigmaNode], of file: FigmaFile) throws -> [TextStyle] { + let styles = file.styles ?? [:] + + return try nodes + .lazy + .filter { $0.isVisible ?? true } + .compactMap { try extractTextStyle(from: $0, styles: styles) } + .reduce(into: []) { result, textStyle in + if !result.contains(where: { $0.node == textStyle.node }) { + result.append(textStyle) + } + } + } + + // MARK: - + + func fetchTextStyles(from file: FileParameters, nodes: NodesParameters) -> Promise<[TextStyle]> { + firstly { + self.filesProvider.fetchFile(file) + }.then { figmaFile in + self.nodesProvider.fetchNodes(nodes, from: figmaFile).map { figmaNodes in + try self.extractTextStyles(from: figmaNodes, of: figmaFile) + } + } + } +} + +extension String { + + // MARK: - Type Methods + + fileprivate static func regularFontName(family fontFamily: String) -> String { + "\(fontFamily)-Regular" + } +} diff --git a/Sources/FigmaGen/Providers/TextStyles/TextStylesProvider.swift b/Sources/FigmaGen/Providers/TextStyles/TextStylesProvider.swift new file mode 100644 index 0000000..339faff --- /dev/null +++ b/Sources/FigmaGen/Providers/TextStyles/TextStylesProvider.swift @@ -0,0 +1,9 @@ +import Foundation +import PromiseKit + +protocol TextStylesProvider { + + // MARK: - Instance Methods + + func fetchTextStyles(from file: FileParameters, nodes: NodesParameters) -> Promise<[TextStyle]> +} diff --git a/Sources/FigmaGen/Providers/TextStyles/TextStylesProviderError.swift b/Sources/FigmaGen/Providers/TextStyles/TextStylesProviderError.swift new file mode 100644 index 0000000..0ff4205 --- /dev/null +++ b/Sources/FigmaGen/Providers/TextStyles/TextStylesProviderError.swift @@ -0,0 +1,65 @@ +import Foundation + +struct TextStylesProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case styleNotFound + case invalidStyleName + case typeStyleNotFound + + case invalidFontFamily + case invalidFontWeight + case invalidFontSize + + case invalidColor + case colorNotFound + case colorStyleNotFound + case invalidColorStyleName + } + + // MARK: - Instance Properties + + let code: Code + let nodeID: String + let nodeName: String? + + // MARK: - CustomStringConvertible + + var description: String { + let nodeName = self.nodeName ?? "nil" + + switch code { + case .styleNotFound: + return "Figma file does not contain a valid style for node \(nodeName) ('\(nodeID)')" + + case .invalidStyleName: + return "Style name of node \(nodeName) ('\(nodeID)') is either empty or nil" + + case .typeStyleNotFound: + return "Type style of node \(nodeName) ('\(nodeID)') could not be found" + + case .invalidFontFamily: + return "Font family of node \(nodeName) ('\(nodeID)') is either empty or nil" + + case .invalidFontWeight: + return "Font weight of node \(nodeName) ('\(nodeID)') is nil" + + case .invalidFontSize: + return "Font size of node \(nodeName) ('\(nodeID)') is nil" + + case .invalidColor: + return "Color of node \(nodeName) ('\(nodeID)') cannot be resolved" + + case .colorNotFound: + return "Color of node \(nodeName) ('\(nodeID)') could not be found" + + case .colorStyleNotFound: + return "Figma file does not contain a valid color style for node \(nodeName) ('\(nodeID)')" + + case .invalidColorStyleName: + return "Color style name of node \(nodeName) ('\(nodeID)') is either empty or nil" + } + } +} diff --git a/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift b/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift new file mode 100644 index 0000000..c3457f0 --- /dev/null +++ b/Sources/FigmaGen/Providers/Tokens/DefaultTokensProvider.swift @@ -0,0 +1,107 @@ +import Foundation +import DictionaryCoder + +final class DefaultTokensProvider: TokensProvider { + + // MARK: - Instance Properties + + let figmaApiProvider: FigmaAPIProvider + let gitHubApiProvider: RemoteRepoProvider + + let dictionaryDecoder = DictionaryDecoder() + let jsonDecoder = JSONDecoder() + + // MARK: - Initializers + + init(figmaApiProvider: FigmaAPIProvider, gitHubApiProvider: RemoteRepoProvider) { + self.figmaApiProvider = figmaApiProvider + self.gitHubApiProvider = gitHubApiProvider + } + + // MARK: - Instance Methods + + private func extractTokens(from file: FigmaFile) throws -> TokenValues { + guard let sharedPluginData = file.document.sharedPluginData else { + throw TokensProviderError(code: .sharedPluginDataNotFound) + } + + guard case .dictionary(let rawPluginData) = sharedPluginData else { + throw TokensProviderError(code: .unexpectedPluginDataType) + } + + let pluginData = try dictionaryDecoder.decode( + TokensStudioPluginData.self, + from: rawPluginData.mapValues { $0.value } + ) + + guard let valuesData = pluginData.tokens.values.data(using: .utf8) else { + throw TokensProviderError(code: .failedCreateData) + } + + let json = try? JSONSerialization.jsonObject(with: valuesData, options: .mutableContainers) + let jsonData = json.flatMap { try? JSONSerialization.data(withJSONObject: $0, options: .prettyPrinted) } + + if let jsonData { + logger.debug(String(decoding: jsonData, as: UTF8.self)) + } + + return try jsonDecoder.decode(TokenValues.self, from: valuesData) + } + + private func extractTokens(from file: GitHubFile) throws -> TokenValues { + guard let valuesData = try? JSONEncoder().encode(file) else { + throw TokensProviderError(code: .failedCreateData) + } + + let json = try? JSONSerialization.jsonObject(with: valuesData, options: .mutableContainers) + + let jsonData = json.flatMap { try? JSONSerialization.data(withJSONObject: $0, options: .prettyPrinted) } + if let jsonData { + logger.debug(String(decoding: jsonData, as: UTF8.self)) + } + + return try jsonDecoder.decode(TokenValues.self, from: valuesData) + } + + private func fetchFile(_ file: FileParameters) async throws -> FigmaFile { + let route = FigmaAPIFileRoute( + accessToken: file.accessToken, + fileKey: file.key, + version: file.version, + depth: 1, + pluginData: "shared" + ) + + return try await figmaApiProvider + .request(route: route) + .async() + } + + private func fetchFile(_ file: RemoteFileParameters) async throws -> GitHubFile { + let route = GitHubAPIFileRoute( + owner: file.owner, + repo: file.repo, + branch: file.branch, + filePath: file.filePath, + accessToken: file.accessToken + ) + + return try await gitHubApiProvider + .request(route: route) + .async() + } + + // MARK: - + + func fetchTokens(from file: FileParameters) async throws -> TokenValues { + let figmaFile = try await fetchFile(file) + + return try extractTokens(from: figmaFile) + } + + func fetchTokens(from remoteFile: RemoteFileParameters) async throws -> TokenValues { + let gitHubFile = try await fetchFile(remoteFile) + + return try extractTokens(from: gitHubFile) + } +} diff --git a/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift b/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift new file mode 100644 index 0000000..eac7120 --- /dev/null +++ b/Sources/FigmaGen/Providers/Tokens/TokensProvider.swift @@ -0,0 +1,10 @@ +import Foundation + +protocol TokensProvider { + + // MARK: - Instance Methods + + func fetchTokens(from file: FileParameters) async throws -> TokenValues + + func fetchTokens(from remoteFile: RemoteFileParameters) async throws -> TokenValues +} diff --git a/Sources/FigmaGen/Providers/Tokens/TokensProviderError.swift b/Sources/FigmaGen/Providers/Tokens/TokensProviderError.swift new file mode 100644 index 0000000..41212d0 --- /dev/null +++ b/Sources/FigmaGen/Providers/Tokens/TokensProviderError.swift @@ -0,0 +1,31 @@ +import Foundation + +struct TokensProviderError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case sharedPluginDataNotFound + case unexpectedPluginDataType + case failedCreateData + } + + // MARK: - Instance Properties + + let code: Code + + // MARK: - CustomStringConvertible + + var description: String { + switch code { + case .sharedPluginDataNotFound: + return "Shared plugin data in Figma file not found" + + case .unexpectedPluginDataType: + return "Unexpected plugin data type, must be a dictionary" + + case .failedCreateData: + return "Failed to create data from json string" + } + } +} diff --git a/Sources/FigmaGen/Render/DefaultTemplateContextCoder.swift b/Sources/FigmaGen/Render/DefaultTemplateContextCoder.swift new file mode 100644 index 0000000..70373cd --- /dev/null +++ b/Sources/FigmaGen/Render/DefaultTemplateContextCoder.swift @@ -0,0 +1,20 @@ +import Foundation +import DictionaryCoder + +final class DefaultTemplateContextCoder: TemplateContextCoder { + + // MARK: - Instance Properties + + let dictionaryDecoder = DictionaryDecoder() + let dictionaryEncoder = DictionaryEncoder() + + // MARK: - Instance Methods + + func decode(_ type: T.Type, from dictionary: [String: Any]) throws -> T { + try dictionaryDecoder.decode(type, from: dictionary) + } + + func encode(_ value: T) throws -> [String: Any] { + try dictionaryEncoder.encode(value) + } +} diff --git a/Sources/FigmaGen/Render/DefaultTemplateRenderer.swift b/Sources/FigmaGen/Render/DefaultTemplateRenderer.swift new file mode 100644 index 0000000..bcac67c --- /dev/null +++ b/Sources/FigmaGen/Render/DefaultTemplateRenderer.swift @@ -0,0 +1,128 @@ +import Foundation +import Stencil +import StencilSwiftKit +import PathKit + +final class DefaultTemplateRenderer: TemplateRenderer { + + // MARK: - Instance Properties + + let contextCoder: TemplateContextCoder + let stencilExtensions: [StencilExtension] + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder, stencilExtensions: [StencilExtension]) { + self.contextCoder = contextCoder + self.stencilExtensions = stencilExtensions + } + + // MARK: - Instance Methods + + private func resolveTemplatePath(of templateType: RenderTemplateType) throws -> Path { + switch templateType { + case let .native(name: templateName): + let templateFileName = templateName.appending(String.templatesFileExtension) + + #if DEBUG + let xcodeTemplatesPath = Path.current.appending(.templatesXcodeRelativePath) + + if xcodeTemplatesPath.exists { + return xcodeTemplatesPath.appending(templateFileName) + } + #endif + + var executablePath = Path(ProcessInfo.processInfo.executablePath) + + while executablePath.isSymlink { + executablePath = try executablePath.symlinkDestination() + } + + let podsTemplatesPath = executablePath.appending(.templatesPodsRelativePath) + + if podsTemplatesPath.exists { + return podsTemplatesPath.appending(templateFileName) + } + + return executablePath + .appending(.templatesShareRelativePath) + .appending(templateFileName) + + case let .custom(path: templatePath): + return Path(templatePath) + } + } + + private func writeOutput(_ output: String, to destination: RenderDestination) throws { + switch destination { + case let .file(path: filePath): + let filePath = Path(filePath) + + try filePath.parent().mkpath() + try filePath.write(output) + + case .console: + logger.info(output) + } + } + + // MARK: - + + func renderTemplate( + _ template: RenderTemplate, + to destination: RenderDestination, + context: [String: Any] + ) throws { + let stencilExtensionRegistry = ExtensionRegistry() + + stencilExtensionRegistry.registerStencilSwiftExtensions() + + stencilExtensions.forEach { stencilExtension in + stencilExtension.register(in: stencilExtensionRegistry) + } + + let templatePath = try resolveTemplatePath(of: template.type) + + // Using deprecated StencilSwiftTemplate because + // there is no option in TrimBehavior.Trim to remove only newlines without whitespaces + let stencilEnvironment = Environment( + loader: FileSystemLoader(paths: [templatePath.parent()]), + extensions: [stencilExtensionRegistry], + templateClass: StencilSwiftTemplate.self + ) + + let stencilTemplate = StencilSwiftTemplate( + templateString: try templatePath.read(), + environment: stencilEnvironment + ) + + let templateContext = context.merging([.templateOptionsKey: template.options]) { $1 } + + let output = try stencilTemplate.render(templateContext) + + try writeOutput(output, to: destination) + } + + func renderTemplate( + _ template: RenderTemplate, + to destination: RenderDestination, + context: Context + ) throws { + try renderTemplate( + template, + to: destination, + context: try contextCoder.encode(context) + ) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let templatesFileExtension = ".stencil" + fileprivate static let templatesXcodeRelativePath = "../Templates" + fileprivate static let templatesPodsRelativePath = "../Templates" + fileprivate static let templatesShareRelativePath = "../../share/figmagen" + fileprivate static let templateOptionsKey = "options" +} diff --git a/Sources/FigmaGen/Render/RenderDestination.swift b/Sources/FigmaGen/Render/RenderDestination.swift new file mode 100644 index 0000000..7bb56cb --- /dev/null +++ b/Sources/FigmaGen/Render/RenderDestination.swift @@ -0,0 +1,9 @@ +import Foundation + +enum RenderDestination { + + // MARK: - Enumeration Cases + + case file(path: String) + case console +} diff --git a/Sources/FigmaGen/Render/RenderTemplate.swift b/Sources/FigmaGen/Render/RenderTemplate.swift new file mode 100644 index 0000000..9b6cab4 --- /dev/null +++ b/Sources/FigmaGen/Render/RenderTemplate.swift @@ -0,0 +1,9 @@ +import Foundation + +struct RenderTemplate { + + // MARK: - Instance Properties + + let type: RenderTemplateType + let options: [String: Any] +} diff --git a/Sources/FigmaGen/Render/RenderTemplateType.swift b/Sources/FigmaGen/Render/RenderTemplateType.swift new file mode 100644 index 0000000..baf38db --- /dev/null +++ b/Sources/FigmaGen/Render/RenderTemplateType.swift @@ -0,0 +1,9 @@ +import Foundation + +enum RenderTemplateType { + + // MARK: - Enumeration Cases + + case native(name: String) + case custom(path: String) +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionDropFirstModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionDropFirstModificator.swift new file mode 100644 index 0000000..b006f0c --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionDropFirstModificator.swift @@ -0,0 +1,20 @@ +import Foundation + +final class StencilCollectionDropFirstModificator: StencilModificator { + + // MARK: - Instance Properties + + let name = "dropFirst" + + // MARK: - Instance Methods + + func modify(input: Any, withArguments arguments: [Any?]) throws -> Any { + let count = arguments.first as? Int ?? 1 + + if let array = input as? [Any?] { + return Array(array.dropFirst(count)) + } + + return String(stringify(input).dropFirst(count)) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionDropLastModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionDropLastModificator.swift new file mode 100644 index 0000000..a9711c2 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionDropLastModificator.swift @@ -0,0 +1,20 @@ +import Foundation + +final class StencilCollectionDropLastModificator: StencilModificator { + + // MARK: - Instance Properties + + let name = "dropLast" + + // MARK: - Instance Methods + + func modify(input: Any, withArguments arguments: [Any?]) throws -> Any { + let count = arguments.first as? Int ?? 1 + + if let array = input as? [Any?] { + return Array(array.dropLast(count)) + } + + return String(stringify(input).dropLast(count)) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionRemovingFirstModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionRemovingFirstModificator.swift new file mode 100644 index 0000000..a62955a --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Collection/StencilCollectionRemovingFirstModificator.swift @@ -0,0 +1,24 @@ +import Foundation + +final class StencilCollectionRemovingFirstModificator: StencilModificator { + + // MARK: - Instance Properties + + let name = "removingFirst" + + // MARK: - Instance Methods + + func modify(input: Any, withArguments arguments: [Any?]) throws -> Any { + guard let element = arguments.first as? String else { + throw StencilModificatorError(code: .invalidArguments(arguments), filter: name) + } + + guard let array = input as? [Any?] else { + throw StencilModificatorError(code: .invalidValue(input), filter: name) + } + + return array + .map { stringify($0) } + .removingFirst(element) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorFilter.swift new file mode 100644 index 0000000..a4c5608 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorFilter.swift @@ -0,0 +1,48 @@ +import Foundation + +protocol StencilColorFilter: StencilFilter where Input == [String: Any], Output == ColorFilterOutput { + + // MARK: - Nested Types + + associatedtype ColorFilterOutput + + // MARK: - Instance Properties + + var contextCoder: TemplateContextCoder { get } + + // MARK: - Instance Methods + + func colorComponentHexByte(_ component: Double) -> String? + func colorComponentByte(_ component: Double) -> UInt8? + + func filter(color: Color) throws -> ColorFilterOutput +} + +extension StencilColorFilter { + + // MARK: - Instance Methods + + func colorComponentByte(_ component: Double) -> UInt8? { + let floatToByteFilter = StencilFloatToByteFilter() + + return try? floatToByteFilter.filter(input: component) + } + + func colorComponentHexByte(_ component: Double) -> String? { + guard let component = colorComponentByte(component) else { + return nil + } + + let byteToHexFilter = StencilByteToHexFilter() + + return byteToHexFilter.filter(input: component) + } + + func filter(input: [String: Any]) throws -> ColorFilterOutput { + guard let color = try? contextCoder.decode(Color.self, from: input) else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + return try filter(color: color) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorInfoFilter.swift new file mode 100644 index 0000000..aa602c5 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorInfoFilter.swift @@ -0,0 +1,28 @@ +import Foundation + +final class StencilColorInfoFilter: StencilColorFilter { + + // MARK: - Instance Properties + + let name = "colorInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(color: Color) throws -> String { + let rgbaHexInfoFilter = StencilColorRGBAHexInfoFilter(contextCoder: contextCoder) + let rgbaHexInfo = try rgbaHexInfoFilter.filter(color: color) + + let rgbaInfoFilter = StencilColorRGBAInfoFilter(contextCoder: contextCoder) + let rgbaInfo = try rgbaInfoFilter.filter(color: color) + + return "Hex \(rgbaHexInfo); rgba \(rgbaInfo)" + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBAHexInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBAHexInfoFilter.swift new file mode 100644 index 0000000..7ae1f7e --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBAHexInfoFilter.swift @@ -0,0 +1,29 @@ +import Foundation + +final class StencilColorRGBAHexInfoFilter: StencilColorFilter { + + // MARK: - Instance Properties + + let name = "colorRGBAHexInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(color: Color) throws -> String { + let rgbHexInfoFilter = StencilColorRGBHexInfoFilter(contextCoder: contextCoder) + let rgbHexInfo = try rgbHexInfoFilter.filter(color: color) + + guard let alpha = colorComponentHexByte(color.alpha) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + return "\(rgbHexInfo)\(alpha)" + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBAInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBAInfoFilter.swift new file mode 100644 index 0000000..de23ba3 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBAInfoFilter.swift @@ -0,0 +1,25 @@ +import Foundation + +final class StencilColorRGBAInfoFilter: StencilColorFilter { + + // MARK: - Instance Properties + + let name = "colorRGBAInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(color: Color) throws -> String { + let rgbInfoFilter = StencilColorRGBInfoFilter(contextCoder: contextCoder) + let rgbInfo = try rgbInfoFilter.filter(color: color) + + return "\(rgbInfo), \(Int(color.alpha * 100.0))%" + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBHexInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBHexInfoFilter.swift new file mode 100644 index 0000000..dcfa73d --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBHexInfoFilter.swift @@ -0,0 +1,34 @@ +import Foundation + +final class StencilColorRGBHexInfoFilter: StencilColorFilter { + + // MARK: - Instance Properties + + let name = "colorRGBHexInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(color: Color) throws -> String { + guard let red = colorComponentHexByte(color.red) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + guard let green = colorComponentHexByte(color.green) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + guard let blue = colorComponentHexByte(color.blue) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + return "#\(red)\(green)\(blue)" + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBInfoFilter.swift new file mode 100644 index 0000000..53fb5fc --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Color/StencilColorRGBInfoFilter.swift @@ -0,0 +1,34 @@ +import Foundation + +final class StencilColorRGBInfoFilter: StencilColorFilter { + + // MARK: - Instance Properties + + let name = "colorRGBInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(color: Color) throws -> String { + guard let red = colorComponentByte(color.red) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + guard let green = colorComponentByte(color.green) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + guard let blue = colorComponentByte(color.blue) else { + throw StencilFilterError(code: .invalidValue(color), filter: name) + } + + return "\(red) \(green) \(blue)" + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/ExtensionRegistry.swift b/Sources/FigmaGen/Render/StencilExtensions/ExtensionRegistry.swift new file mode 100644 index 0000000..06e010c --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/ExtensionRegistry.swift @@ -0,0 +1,3 @@ +import Stencil + +typealias ExtensionRegistry = Extension diff --git a/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontFilter.swift new file mode 100644 index 0000000..7b49240 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontFilter.swift @@ -0,0 +1,29 @@ +import Foundation + +protocol StencilFontFilter: StencilFilter where Input == [String: Any], Output == FontFilterOutput { + + // MARK: - Nested Types + + associatedtype FontFilterOutput + + // MARK: - Instance Properties + + var contextCoder: TemplateContextCoder { get } + + // MARK: - Instance Methods + + func filter(font: Font) throws -> FontFilterOutput +} + +extension StencilFontFilter { + + // MARK: - Instance Methods + + func filter(input: [String: Any]) throws -> FontFilterOutput { + guard let font = try? contextCoder.decode(Font.self, from: input) else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + return try filter(font: font) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontInfoFilter.swift new file mode 100644 index 0000000..50530b6 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontInfoFilter.swift @@ -0,0 +1,22 @@ +import Foundation + +final class StencilFontInfoFilter: StencilFontFilter { + + // MARK: - Instance Properties + + let name = "fontInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(font: Font) throws -> String { + "\(font.family) (\(font.name)); weight \(font.weight); size \(font.size)" + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontInitializerModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontInitializerModificator.swift new file mode 100644 index 0000000..d9775f1 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontInitializerModificator.swift @@ -0,0 +1,67 @@ +import Foundation + +final class StencilFontInitializerModificator: StencilFontModificator { + + // MARK: - Type Properties + + private static let validWeights = [ + "ultraLight", + "thin", + "light", + "regular", + "medium", + "semibold", + "bold", + "heavy", + "black" + ] + + // MARK: - Instance Properties + + let name = "initializer" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + private func validatedWeight(rawWeight: String?) -> String? { + guard let rawWeight = rawWeight?.lowercased() else { + return nil + } + + return Self.validWeights.first { rawWeight == $0 } + } + + private func usingSystemFonts(from arguments: [Any?]) throws -> Bool { + guard let usingSystemFonts = arguments.first else { + throw StencilModificatorError(code: .invalidArguments(arguments), filter: name) + } + + return usingSystemFonts as? Bool ?? false + } + + // MARK: - + + func modify(font: Font, withArguments arguments: [Any?]) throws -> String { + guard font.isSystemFont, try usingSystemFonts(from: arguments) else { + return "(name: \"\(font.name)\", size: \(font.size))" + } + + let weight = validatedWeight(rawWeight: font.name.components(separatedBy: "-").last) ?? .defaultWeight + + return ".systemFont(ofSize: \(font.size), weight: .\(weight))" + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let defaultWeight = "regular" +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontModificator.swift new file mode 100644 index 0000000..39b02cc --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontModificator.swift @@ -0,0 +1,29 @@ +import Foundation + +protocol StencilFontModificator: StencilModificator where Input == [String: Any], Output == FontModificatorOutput { + + // MARK: - Nested Types + + associatedtype FontModificatorOutput + + // MARK: - Instance Properties + + var contextCoder: TemplateContextCoder { get } + + // MARK: - Instance Methods + + func modify(font: Font, withArguments arguments: [Any?]) throws -> FontModificatorOutput +} + +extension StencilFontModificator { + + // MARK: - Instance Methods + + func modify(input: [String: Any], withArguments arguments: [Any?]) throws -> FontModificatorOutput { + guard let font = try? contextCoder.decode(Font.self, from: input) else { + throw StencilModificatorError(code: .invalidValue(input), filter: name) + } + + return try modify(font: font, withArguments: arguments) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontSystemFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontSystemFilter.swift new file mode 100644 index 0000000..7e153cd --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Font/StencilFontSystemFilter.swift @@ -0,0 +1,22 @@ +import Foundation + +final class StencilFontSystemFilter: StencilFontFilter { + + // MARK: - Instance Properties + + let name = "isSystemFont" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(font: Font) throws -> Bool { + font.isSystemFont + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift new file mode 100644 index 0000000..a917892 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilFullHexModificator.swift @@ -0,0 +1,79 @@ +import Foundation + +final class StencilFullHexModificator: StencilModificator { + + // MARK: - Nested Types + + fileprivate enum AlphaHexPosition: String { + + // MARK: - Enumeration Cases + + case start + case end + } + + // MARK: - Instance Properties + + let name = "fullHex" + + // MARK: - Instance Methods + + func modify(input: String, withArguments arguments: [Any?]) throws -> String { + let hexPosition = arguments + .first + .flatMap { $0 as? String } + .flatMap { AlphaHexPosition(rawValue: $0) } ?? .end + + guard input.hasPrefix("#") else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + let hex = String(input.uppercased().dropFirst()) + let updatedHex: String + + switch hex.count { + case .rgb: + updatedHex = Array(hex) + .map { "\($0)\($0)" } + .joined() + .appendingHexAlpha(to: hexPosition) + + case .rrggbb: + updatedHex = hex.appendingHexAlpha(to: hexPosition) + + case .rrggbbaa: + updatedHex = hex + + default: + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + return updatedHex.prepending("#") + } +} + +extension Int { + + // MARK: - Type Properties + + fileprivate static let rgb = 3 + fileprivate static let rrggbb = 6 + fileprivate static let rrggbbaa = 8 +} + +extension String { + + // MARK: - Instance Methods + + fileprivate func appendingHexAlpha(to position: StencilFullHexModificator.AlphaHexPosition) -> Self { + let alphaHex = "FF" + + switch position { + case .start: + return alphaHex + self + + case .end: + return self + alphaHex + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilHexToAlphaFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilHexToAlphaFilter.swift new file mode 100644 index 0000000..34e20a7 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Hex/StencilHexToAlphaFilter.swift @@ -0,0 +1,34 @@ +import Foundation + +final class StencilHexToAlphaFilter: StencilFilter { + + // MARK: - Instance Properties + + let name = "hexToAlpha" + + // MARK: - Instance Methods + + func filter(input: String) throws -> Double { + guard input.hasPrefix("#") else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + let hexColor = String(input.dropFirst()) + + guard hexColor.count == 8 else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + let scanner = Scanner(string: hexColor) + var hexNumber: UInt64 = 0 + + guard scanner.scanHexInt64(&hexNumber) else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + let alpha = Double(hexNumber & 0x000000ff) / 255 + let multiplier = pow(10.0, 2.0) + + return round(alpha * multiplier) / multiplier + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Number/StencilByteToFloatFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilByteToFloatFilter.swift new file mode 100644 index 0000000..e1ecb6d --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilByteToFloatFilter.swift @@ -0,0 +1,14 @@ +import Foundation + +final class StencilByteToFloatFilter: StencilFilter { + + // MARK: - Instance Properties + + let name = "byteToFloat" + + // MARK: - Instance Methods + + func filter(input: UInt8) -> Double { + Double(input) / Double(255.0) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Number/StencilByteToHexFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilByteToHexFilter.swift new file mode 100644 index 0000000..aef74aa --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilByteToHexFilter.swift @@ -0,0 +1,14 @@ +import Foundation + +final class StencilByteToHexFilter: StencilFilter { + + // MARK: - Instance Properties + + let name = "byteToHex" + + // MARK: - Instance Methods + + func filter(input: UInt8) -> String { + String(format: "%02lX", input) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Number/StencilFloatToByteFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilFloatToByteFilter.swift new file mode 100644 index 0000000..1848b63 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilFloatToByteFilter.swift @@ -0,0 +1,18 @@ +import Foundation + +final class StencilFloatToByteFilter: StencilFilter { + + // MARK: - Instance Properties + + let name = "floatToByte" + + // MARK: - Instance Methods + + func filter(input: Double) throws -> UInt8 { + guard 0.0...1.0 ~= input else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + return UInt8(input * 255.0) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Number/StencilHexToByteFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilHexToByteFilter.swift new file mode 100644 index 0000000..b4fd458 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Number/StencilHexToByteFilter.swift @@ -0,0 +1,18 @@ +import Foundation + +final class StencilHexToByteFilter: StencilFilter { + + // MARK: - Instance Properties + + let name = "hexToByte" + + // MARK: - Instance Methods + + func filter(input: String) throws -> UInt8 { + guard let number = Int(input, radix: 16), 0...255 ~= number else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + return UInt8(number) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilExtension.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilExtension.swift new file mode 100644 index 0000000..aa3182c --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilExtension.swift @@ -0,0 +1,51 @@ +import Foundation +import Stencil + +protocol StencilExtension { + + // MARK: - Instance Properties + + var name: String { get } + + // MARK: - Instance Methods + + func register(in extensionRegistry: ExtensionRegistry) +} + +extension StencilExtension { + + // MARK: - Instance Methods + + private func unwrap(_ array: [Any?]) -> [Any] { + array.map { item in + if let item { + if let items = item as? [Any?] { + return unwrap(items) + } + + return item + } + + return item as Any + } + } + + func stringify(_ result: Any?) -> String { + switch result { + case let result as String: + return result + + case let array as [Any?]: + return unwrap(array).description + + case let result as CustomStringConvertible: + return result.description + + case let result as NSObject: + return result.description + + default: + return .empty + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilFilter.swift new file mode 100644 index 0000000..57f26aa --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilFilter.swift @@ -0,0 +1,29 @@ +import Foundation +import Stencil + +protocol StencilFilter: StencilExtension { + + // MARK: - Nested Types + + associatedtype Input + associatedtype Output + + // MARK: - Instance Methods + + func filter(input: Input) throws -> Output +} + +extension StencilFilter { + + // MARK: - Instance Methods + + func register(in extensionRegistry: ExtensionRegistry) { + extensionRegistry.registerFilter(name) { value in + guard let input = value as? Input else { + throw StencilFilterError(code: .invalidValue(value), filter: self.name) + } + + return try self.filter(input: input) + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilFilterError.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilFilterError.swift new file mode 100644 index 0000000..7526d26 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilFilterError.swift @@ -0,0 +1,32 @@ +import Foundation + +struct StencilFilterError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case invalidValue(_ value: Any?) + case invalidArgument(_ argument: Any?) + } + + // MARK: - Instance Properties + + let code: Code + let filter: String + + // MARK: - Instance Properties + + var description: String { + switch code { + case let .invalidValue(value): + let valueDescription = value.map(String.init(describing:)) ?? "nil" + + return "Stencil filter '\(filter)' cannot be used for value: \(valueDescription)" + + case let .invalidArgument(argument): + let argumentDescription = argument.map(String.init(describing:)) ?? "nil" + + return "Stencil filter '\(filter)' was called with an invalid argument: \(argumentDescription)" + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilModificator.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilModificator.swift new file mode 100644 index 0000000..1acfd24 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilModificator.swift @@ -0,0 +1,29 @@ +import Foundation +import FigmaGenTools + +protocol StencilModificator: StencilExtension { + + // MARK: - Nested Types + + associatedtype Input + associatedtype Output + + // MARK: - Instance Methods + + func modify(input: Input, withArguments arguments: [Any?]) throws -> Output +} + +extension StencilModificator { + + // MARK: - Instance Methods + + func register(in extensionRegistry: ExtensionRegistry) { + extensionRegistry.registerFilter(name) { value, parameters in + guard let input = value as? Input else { + throw StencilModificatorError(code: .invalidValue(value), filter: name) + } + + return try self.modify(input: input, withArguments: parameters) + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilModificatorError.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilModificatorError.swift new file mode 100644 index 0000000..a7f3d9a --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilModificatorError.swift @@ -0,0 +1,32 @@ +import Foundation + +struct StencilModificatorError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case invalidValue(_ value: Any?) + case invalidArguments(_ argument: [Any?]) + } + + // MARK: - Instance Properties + + let code: Code + let filter: String + + // MARK: - Instance Properties + + var description: String { + switch code { + case let .invalidValue(value): + let valueDescription = value.map(String.init(describing:)) ?? "nil" + + return "Stencil filter '\(filter)' cannot be used for value: \(valueDescription)" + + case let .invalidArguments(arguments): + let argumentDescriptions = arguments.compactMap { $0 }.map(String.init(describing:)) + + return "Stencil filter '\(filter)' was called with an invalid arguments: \(argumentDescriptions)" + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilTag.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilTag.swift new file mode 100644 index 0000000..8b7e921 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilTag.swift @@ -0,0 +1,18 @@ +import Foundation +import Stencil + +protocol StencilTag: StencilExtension { + + // MARK: - Instance Methods + + func makeNode(parser: TokenParser, token: Token) throws -> NodeType +} + +extension StencilTag { + + // MARK: - Instance Methods + + func register(in extensionRegistry: ExtensionRegistry) { + extensionRegistry.registerTag(name, parser: makeNode) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilTagError.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilTagError.swift new file mode 100644 index 0000000..900fd87 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilTagError.swift @@ -0,0 +1,27 @@ +import Foundation + +struct StencilTagError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + enum Code { + case invalidArguments(_ arguments: [String]) + case invalidVariable(_ variable: String, expectedType: Any.Type) + } + // MARK: - Enumeration Cases + + let code: Code + let tag: String + + // MARK: - Instance Properties + + var description: String { + switch code { + case let .invalidArguments(arguments): + return "Stencil tag '\(tag)' received an invalid argument list: \(arguments)" + + case let .invalidVariable(variable, expectedType): + return "Stencil variable '\(variable)' cannot be resolved as \(expectedType) for tag '\(tag)'" + } + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/StencilTagNode.swift b/Sources/FigmaGen/Render/StencilExtensions/StencilTagNode.swift new file mode 100644 index 0000000..4a20420 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/StencilTagNode.swift @@ -0,0 +1,16 @@ +import Foundation +import Stencil + +struct StencilTagNode: NodeType { + + // MARK: - Instance Properties + + let token: Token? + let renderer: ((_ context: Context) throws -> String) + + // MARK: - Instance Methods + + func render(_ context: Context) throws -> String { + try renderer(context) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Tokens/StencilRecursiveTokenFindModicator.swift b/Sources/FigmaGen/Render/StencilExtensions/Tokens/StencilRecursiveTokenFindModicator.swift new file mode 100644 index 0000000..d98044b --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Tokens/StencilRecursiveTokenFindModicator.swift @@ -0,0 +1,72 @@ +import Foundation + +struct StencilRecursiveTokenFindModicator: StencilModificator { + + // MARK: - Instance Properties + + let name = "findTokenInThemes" + + // MARK: - Instance Methods + + func modify(input: Any, withArguments arguments: [Any?]) throws -> Any { + guard arguments.count > 1 else { + throw StencilModificatorError(code: .invalidArguments(arguments), filter: name) + } + + guard + let themesDict = input as? [String: [String: Any]], + let pathString = arguments[0] as? String, + let key = arguments[1] as? String + else { + return input + } + + let pathComponents = pathString.split(separator: ".").map(String.init) + var result: [String: Any] = [:] + + for (themeName, structuredDict) in themesDict { + if let token = findToken( + in: structuredDict, + name: pathString, + path: pathComponents, + key: key + ) { + result[themeName] = token + } + } + + return result + } + + private func findToken( + in dict: [String: Any], + name: String, + path: [String], + key: String + ) -> Any? { + if let tokens = dict[key] as? [TokenProtocol], let found = tokens.first(where: { $0.name == name }) { + return found + } + + guard !path.isEmpty else { + return nil + } + + guard let children = dict["children"] as? [[String: Any]] else { + return nil + } + + for child in children { + if let childName = child["name"] as? String, childName == path[0] { + return findToken( + in: child, + name: name, + path: Array(path.dropFirst()), + key: key + ) + } + } + + return nil + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Vector/StencilVectorFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Vector/StencilVectorFilter.swift new file mode 100644 index 0000000..2aee8a3 --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Vector/StencilVectorFilter.swift @@ -0,0 +1,29 @@ +import Foundation + +protocol StencilVectorFilter: StencilFilter where Input == [String: Any], Output == VectorFilterOutput { + + // MARK: - Nested Types + + associatedtype VectorFilterOutput + + // MARK: - Instance Properties + + var contextCoder: TemplateContextCoder { get } + + // MARK: - Instance Methods + + func filter(vector: Vector) throws -> VectorFilterOutput +} + +extension StencilVectorFilter { + + // MARK: - Instance Methods + + func filter(input: [String: Any]) throws -> VectorFilterOutput { + guard let vector = try? contextCoder.decode(Vector.self, from: input) else { + throw StencilFilterError(code: .invalidValue(input), filter: name) + } + + return try filter(vector: vector) + } +} diff --git a/Sources/FigmaGen/Render/StencilExtensions/Vector/StencilVectorInfoFilter.swift b/Sources/FigmaGen/Render/StencilExtensions/Vector/StencilVectorInfoFilter.swift new file mode 100644 index 0000000..dee17ed --- /dev/null +++ b/Sources/FigmaGen/Render/StencilExtensions/Vector/StencilVectorInfoFilter.swift @@ -0,0 +1,26 @@ +import Foundation + +final class StencilVectorInfoFilter: StencilVectorFilter { + + // MARK: - Nested Types + + typealias VectorFilterOutput = String + + // MARK: - Instance Properties + + let name = "vectorInfo" + + let contextCoder: TemplateContextCoder + + // MARK: - Initializers + + init(contextCoder: TemplateContextCoder) { + self.contextCoder = contextCoder + } + + // MARK: - Instance Methods + + func filter(vector: Vector) throws -> String { + "x \(vector.x); y \(vector.y)" + } +} diff --git a/Sources/FigmaGen/Render/TemplateContextCoder.swift b/Sources/FigmaGen/Render/TemplateContextCoder.swift new file mode 100644 index 0000000..60681f2 --- /dev/null +++ b/Sources/FigmaGen/Render/TemplateContextCoder.swift @@ -0,0 +1,9 @@ +import Foundation + +protocol TemplateContextCoder { + + // MARK: - Instance Methods + + func decode(_ type: T.Type, from dictionary: [String: Any]) throws -> T + func encode(_ value: T) throws -> [String: Any] +} diff --git a/Sources/FigmaGen/Render/TemplateRenderer.swift b/Sources/FigmaGen/Render/TemplateRenderer.swift new file mode 100644 index 0000000..f42705f --- /dev/null +++ b/Sources/FigmaGen/Render/TemplateRenderer.swift @@ -0,0 +1,18 @@ +import Foundation + +protocol TemplateRenderer { + + // MARK: - Instance Methods + + func renderTemplate( + _ template: RenderTemplate, + to destination: RenderDestination, + context: [String: Any] + ) throws + + func renderTemplate( + _ template: RenderTemplate, + to destination: RenderDestination, + context: Context + ) throws +} diff --git a/Sources/FigmaGen/Resolvers/AccessToken/AccessTokenResolver.swift b/Sources/FigmaGen/Resolvers/AccessToken/AccessTokenResolver.swift new file mode 100644 index 0000000..2a7a71b --- /dev/null +++ b/Sources/FigmaGen/Resolvers/AccessToken/AccessTokenResolver.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol AccessTokenResolver { + + func resolveAccessToken(from configuration: AccessTokenConfiguration?) -> String? +} diff --git a/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift b/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift new file mode 100644 index 0000000..1fc3cdf --- /dev/null +++ b/Sources/FigmaGen/Resolvers/AccessToken/DefaultAccessTokenResolver.swift @@ -0,0 +1,25 @@ +import Foundation +#if os(macOS) +import KeychainAccess +#endif + +final class DefaultAccessTokenResolver: AccessTokenResolver { + + func resolveAccessToken(from configuration: AccessTokenConfiguration?) -> String? { + if let accessToken = configuration?.value { + return accessToken + } else if let environmentVariable = configuration?.environmentVariable, + let accessToken = ProcessInfo.processInfo.environment[environmentVariable] { + return accessToken + } else if let parameters = configuration?.keychainParameters { + #if os(macOS) + let accessToken = try? Keychain(service: parameters.service).getString(parameters.key) + return accessToken + #else + return nil + #endif + } else { + return nil + } + } +} diff --git a/Sources/FigmaGen/Resolvers/RenderParameters/DefaultRenderParametersResolver.swift b/Sources/FigmaGen/Resolvers/RenderParameters/DefaultRenderParametersResolver.swift new file mode 100644 index 0000000..49d1506 --- /dev/null +++ b/Sources/FigmaGen/Resolvers/RenderParameters/DefaultRenderParametersResolver.swift @@ -0,0 +1,57 @@ +import Foundation + +final class DefaultRenderParametersResolver: RenderParametersResolver { + + // MARK: - Instance Methods + + private func resolveTemplateType( + template: TemplateConfiguration, + defaultTemplateType: RenderTemplateType + ) -> RenderTemplateType { + if let templatePath = template.template { + return .custom(path: templatePath) + } + + return defaultTemplateType + } + + private func resolveDestination( + template: TemplateConfiguration, + defaultDestination: RenderDestination + ) -> RenderDestination { + if let destinationPath = template.destination { + return .file(path: destinationPath) + } + + return defaultDestination + } + + func resolveRenderParameters( + templates: [TemplateConfiguration]?, + defaultTemplateType: RenderTemplateType, + defaultDestination: RenderDestination + ) -> [RenderParameters] { + guard let templates else { + return [] + } + + return templates.map { template in + let templateType = resolveTemplateType( + template: template, + defaultTemplateType: defaultTemplateType + ) + + let destination = resolveDestination( + template: template, + defaultDestination: defaultDestination + ) + + let template = RenderTemplate( + type: templateType, + options: template.templateOptions ?? [:] + ) + + return RenderParameters(template: template, destination: destination) + } + } +} diff --git a/Sources/FigmaGen/Resolvers/RenderParameters/RenderParametersResolver.swift b/Sources/FigmaGen/Resolvers/RenderParameters/RenderParametersResolver.swift new file mode 100644 index 0000000..7bdc90a --- /dev/null +++ b/Sources/FigmaGen/Resolvers/RenderParameters/RenderParametersResolver.swift @@ -0,0 +1,24 @@ +import Foundation + +protocol RenderParametersResolver { + + func resolveRenderParameters( + templates: [TemplateConfiguration]?, + defaultTemplateType: RenderTemplateType, + defaultDestination: RenderDestination + ) -> [RenderParameters] +} + +extension RenderParametersResolver { + + func resolveRenderParameters( + templates: [TemplateConfiguration]?, + defaultTemplateType: RenderTemplateType + ) -> [RenderParameters] { + resolveRenderParameters( + templates: templates, + defaultTemplateType: defaultTemplateType, + defaultDestination: .console + ) + } +} diff --git a/Sources/FigmaGen/main.swift b/Sources/FigmaGen/main.swift new file mode 100644 index 0000000..1c64aa0 --- /dev/null +++ b/Sources/FigmaGen/main.swift @@ -0,0 +1,27 @@ +import Foundation +import SwiftCLI +import PathKit + +#if DEBUG +Path.current = Path(#file).appending("../../../Demo") +#endif + +let version = "2.0.0-beta.24" +let logger = Logger() + +let figmagen = CLI( + name: "figmagen", + version: version, + description: "The Swift code & resources generator for your Figma files" +) + +figmagen.commands = [ + ColorStylesCommand(generator: Dependencies.colorStylesGenerator), + TextStylesCommand(generator: Dependencies.textStylesGenerator), + ImagesCommand(generator: Dependencies.imagesGenerator), + GenerateCommand(generator: Dependencies.libraryGenerator), + ShadowStylesCommand(generator: Dependencies.shadowStylesGenerator), + TokensCommand(generator: Dependencies.tokensGenerator) +] + +figmagen.goAndExitOnError() diff --git a/Sources/FigmaGenTools/.swiftlint.yml b/Sources/FigmaGenTools/.swiftlint.yml new file mode 100644 index 0000000..6247483 --- /dev/null +++ b/Sources/FigmaGenTools/.swiftlint.yml @@ -0,0 +1,6 @@ +disabled_rules: + - extension_access_modifier + +opt_in_rules: + - explicit_acl + - explicit_top_level_acl \ No newline at end of file diff --git a/Sources/FigmaGenTools/Assets/AssetAppearance.swift b/Sources/FigmaGenTools/Assets/AssetAppearance.swift new file mode 100644 index 0000000..132df60 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetAppearance.swift @@ -0,0 +1,51 @@ +import Foundation + +public enum AssetAppearance: Codable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case type = "appearance" + case value = "value" + } + + private enum CodingType: String, Codable { + case luminosity + case contrast + } + + // MARK: - Enumeration Cases + + case luminosity(AssetAppearanceLuminosity) + case contrast(AssetAppearanceContrast) + + // MARK: - Initializers + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(CodingType.self, forKey: .type) { + case .luminosity: + self = .luminosity(try container.decode(forKey: .value)) + + case .contrast: + self = .contrast(try container.decode(forKey: .value)) + } + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .luminosity(luminosity): + try container.encode(CodingType.luminosity, forKey: .type) + try container.encode(luminosity, forKey: .value) + + case let .contrast(contrast): + try container.encode(CodingType.contrast, forKey: .type) + try container.encode(contrast, forKey: .value) + } + } +} diff --git a/Sources/FigmaGenTools/Assets/AssetAppearanceContrast.swift b/Sources/FigmaGenTools/Assets/AssetAppearanceContrast.swift new file mode 100644 index 0000000..3b2bd8d --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetAppearanceContrast.swift @@ -0,0 +1,8 @@ +import Foundation + +public enum AssetAppearanceContrast: String, Codable { + + // MARK: - Enumeration Cases + + case high +} diff --git a/Sources/FigmaGenTools/Assets/AssetAppearanceLuminosity.swift b/Sources/FigmaGenTools/Assets/AssetAppearanceLuminosity.swift new file mode 100644 index 0000000..396ae1c --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetAppearanceLuminosity.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum AssetAppearanceLuminosity: String, Codable { + + // MARK: - Enumeration Cases + + case light + case dark +} diff --git a/Sources/FigmaGenTools/Assets/AssetCompressionType.swift b/Sources/FigmaGenTools/Assets/AssetCompressionType.swift new file mode 100644 index 0000000..754fa82 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetCompressionType.swift @@ -0,0 +1,12 @@ +import Foundation + +public enum AssetCompressionType: String, Codable { + + // MARK: - Enumeration Cases + + case automatic + case lossless + case basic = "lossy" + case gpuBestQuality = "gpu-optimized-best" + case gpuSmallestSize = "gpu-optimized-smallest" +} diff --git a/Sources/FigmaGenTools/Assets/AssetDisplayGamut.swift b/Sources/FigmaGenTools/Assets/AssetDisplayGamut.swift new file mode 100644 index 0000000..36795e8 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetDisplayGamut.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum AssetDisplayGamut: String, Codable { + + // MARK: - Enumeration Cases + + case sRGB = "sRGB" + case displayP3 = "display-P3" +} diff --git a/Sources/FigmaGenTools/Assets/AssetIdiom.swift b/Sources/FigmaGenTools/Assets/AssetIdiom.swift new file mode 100644 index 0000000..531b7de --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetIdiom.swift @@ -0,0 +1,92 @@ +import Foundation + +public enum AssetIdiom: Codable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case idiom + case idiomSubtype = "subtype" + case screenWidth = "screen-width" + } + + private enum CodingType: String, Codable { + case universal + case iPhone = "iphone" + case iPad = "ipad" + case mac + case watch + case tv + case car + } + + // MARK: - Enumeration Cases + + case universal + case iPhone + case iPad(subtype: AssetIdiomIPadSubtype?) + case mac + case watch(screenWidth: String?) + case tv + case car + + // MARK: - Initializers + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(CodingType.self, forKey: .idiom) { + case .universal: + self = .universal + + case .iPhone: + self = .iPhone + + case .iPad: + self = .iPad(subtype: try container.decodeIfPresent(forKey: .idiomSubtype)) + + case .mac: + self = .mac + + case .watch: + self = .watch(screenWidth: try container.decodeIfPresent(forKey: .screenWidth)) + + case .tv: + self = .tv + + case .car: + self = .car + } + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .universal: + try container.encode(CodingType.universal, forKey: .idiom) + + case .iPhone: + try container.encode(CodingType.iPhone, forKey: .idiom) + + case let .iPad(subtype): + try container.encode(CodingType.iPad, forKey: .idiom) + try container.encodeIfPresent(subtype, forKey: .idiomSubtype) + + case .mac: + try container.encode(CodingType.mac, forKey: .idiom) + + case let .watch(screenWidth): + try container.encode(CodingType.watch, forKey: .idiom) + try container.encodeIfPresent(screenWidth, forKey: .screenWidth) + + case .tv: + try container.encode(CodingType.tv, forKey: .idiom) + + case .car: + try container.encode(CodingType.car, forKey: .idiom) + } + } +} diff --git a/Sources/FigmaGenTools/Assets/AssetIdiomIPadSubtype.swift b/Sources/FigmaGenTools/Assets/AssetIdiomIPadSubtype.swift new file mode 100644 index 0000000..d3c4af7 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetIdiomIPadSubtype.swift @@ -0,0 +1,8 @@ +import Foundation + +public enum AssetIdiomIPadSubtype: String, Codable { + + // MARK: - Enumeration Cases + + case macCatalyst = "mac-catalyst" +} diff --git a/Sources/FigmaGenTools/Assets/AssetInfo.swift b/Sources/FigmaGenTools/Assets/AssetInfo.swift new file mode 100644 index 0000000..b17e0c6 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetInfo.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct AssetInfo: Codable, Hashable { + + // MARK: - Type Properties + + public static let defaultFigmaGen = Self(version: 1, author: "FigmaGen") + + // MARK: - Instance Properties + + public var version: Int? + public var author: String? + + // MARK: - Initializers + + public init(version: Int? = 1, author: String? = "xcode") { + self.version = version + self.author = author + } +} diff --git a/Sources/FigmaGenTools/Assets/AssetNode.swift b/Sources/FigmaGenTools/Assets/AssetNode.swift new file mode 100644 index 0000000..ad1d52e --- /dev/null +++ b/Sources/FigmaGenTools/Assets/AssetNode.swift @@ -0,0 +1,62 @@ +import Foundation +import PathKit + +public protocol AssetNode { + + // MARK: - Nested Types + + associatedtype Contents: Codable + + // MARK: - Type Properties + + static var pathExtension: String { get } + + // MARK: - Instance Properties + + var contents: Contents { get set } + + // MARK: - Initializers + + init(contents: Contents) + init(folderPath: String) throws + + // MARK: - Instance Methods + + func save(in folderPath: String) throws +} + +extension AssetNode { + + // MARK: - Initializers + + public init(folderPath: String) throws { + let folderPath = Path(folderPath) + + let contentsPath = folderPath.appending(.contentsPath) + let contentsData = try contentsPath.read() + let contentsDecoder = JSONDecoder() + + self.init(contents: try contentsDecoder.decode(from: contentsData)) + } + + // MARK: - Instance Methods + + public func save(in folderPath: String) throws { + let folderPath = Path(folderPath) + + try folderPath.mkpath() + + let contentsEncoder = JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys]) + let contentsData = try contentsEncoder.encode(contents) + let contentsPath = folderPath.appending(.contentsPath) + + try contentsPath.write(contentsData) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let contentsPath = "Contents.json" +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetColor.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetColor.swift new file mode 100644 index 0000000..a332c77 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetColor.swift @@ -0,0 +1,93 @@ +import Foundation + +public struct AssetColor: Codable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case content = "color" + case appearances + case displayGamut = "display-gamut" + case locale + } + + // MARK: - Instance Properties + + public var content: AssetColorContent? + public var appearances: [AssetAppearance]? + public var displayGamut: AssetDisplayGamut? + public var locale: String? + public var idiom: AssetIdiom + + // MARK: - Initializers + + public init( + content: AssetColorContent? = .custom(.sRGB(components: AssetColorComponents())), + appearances: [AssetAppearance]? = nil, + displayGamut: AssetDisplayGamut? = nil, + locale: String? = nil, + idiom: AssetIdiom = .universal + ) { + self.content = content + self.appearances = appearances + self.displayGamut = displayGamut + self.locale = locale + self.idiom = idiom + } + + public init( + custom: AssetCustomColor, + appearances: [AssetAppearance]? = nil, + displayGamut: AssetDisplayGamut? = nil, + locale: String? = nil, + idiom: AssetIdiom = .universal + ) { + self.init( + content: .custom(custom), + appearances: appearances, + displayGamut: displayGamut, + locale: locale, + idiom: idiom + ) + } + + public init( + system: AssetSystemColor, + appearances: [AssetAppearance]? = nil, + displayGamut: AssetDisplayGamut? = nil, + locale: String? = nil, + idiom: AssetIdiom = .universal + ) { + self.init( + content: .system(system), + appearances: appearances, + displayGamut: displayGamut, + locale: locale, + idiom: idiom + ) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + content = try container.decodeIfPresent(forKey: .content) + appearances = try container.decodeIfPresent(forKey: .appearances) + displayGamut = try container.decodeIfPresent(forKey: .displayGamut) + locale = try container.decodeIfPresent(forKey: .locale) + + idiom = try AssetIdiom(from: decoder) + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(content, forKey: .content) + try container.encodeIfPresent(appearances, forKey: .appearances) + try container.encodeIfPresent(displayGamut, forKey: .displayGamut) + try container.encodeIfPresent(locale, forKey: .locale) + + try idiom.encode(to: encoder) + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetColorComponents.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorComponents.swift new file mode 100644 index 0000000..ad136ba --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorComponents.swift @@ -0,0 +1,43 @@ +import Foundation + +public struct AssetColorComponents: Codable, Hashable { + + // MARK: - Instance Properties + + public var red: String? + public var green: String? + public var blue: String? + public var alpha: String? + + // MARK: - Initializers + + public init( + red: String? = "1.000", + green: String? = "1.000", + blue: String? = "1.000", + alpha: String? = "1.000" + ) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + } + + public init(red: Double, green: Double, blue: Double, alpha: Double) { + self.init( + red: String(red), + green: String(green), + blue: String(blue), + alpha: String(alpha) + ) + } + + public init(red: UInt8, green: UInt8, blue: UInt8, alpha: UInt8) { + self.init( + red: String(red), + green: String(green), + blue: String(blue), + alpha: String(Double(alpha) / 255.0) + ) + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetColorContent.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorContent.swift new file mode 100644 index 0000000..596532e --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorContent.swift @@ -0,0 +1,31 @@ +import Foundation + +public enum AssetColorContent: Codable, Hashable { + + // MARK: - Enumeration Cases + + case custom(AssetCustomColor) + case system(AssetSystemColor) + + // MARK: - Initializers + + public init(from decoder: Decoder) throws { + if let color = try? AssetCustomColor(from: decoder) { + self = .custom(color) + } else { + self = try .system(AssetSystemColor(from: decoder)) + } + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + switch self { + case let .custom(color): + try color.encode(to: encoder) + + case let .system(color): + try color.encode(to: encoder) + } + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetColorGrayscale.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorGrayscale.swift new file mode 100644 index 0000000..e9872b9 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorGrayscale.swift @@ -0,0 +1,24 @@ +import Foundation + +public struct AssetColorGrayscale: Codable, Hashable { + + // MARK: - Instance Properties + + public var white: String? + public var alpha: String? + + // MARK: - Initializers + + public init(white: String? = "1.000", alpha: String? = "1.000") { + self.white = white + self.alpha = alpha + } + + public init(white: Double, alpha: Double) { + self.init(white: String(white), alpha: String(alpha)) + } + + public init(white: UInt8, alpha: UInt8) { + self.init(white: String(white), alpha: String(Double(alpha) / 255.0)) + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetColorSet.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorSet.swift new file mode 100644 index 0000000..dc373ec --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorSet.swift @@ -0,0 +1,18 @@ +import Foundation + +public struct AssetColorSet: AssetNode { + + // MARK: - Type Properties + + public static let pathExtension = "colorset" + + // MARK: - Instance Properties + + public var contents: AssetColorSetContents + + // MARK: - Initializers + + public init(contents: AssetColorSetContents = AssetColorSetContents()) { + self.contents = contents + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetColorSetContents.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorSetContents.swift new file mode 100644 index 0000000..10bbfda --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetColorSetContents.swift @@ -0,0 +1,19 @@ +import Foundation + +public struct AssetColorSetContents: Codable, Hashable { + + // MARK: - Instance Properties + + public var info: AssetInfo? + public var colors: [AssetColor]? + + // MARK: - Initializers + + public init( + info: AssetInfo? = AssetInfo(), + colors: [AssetColor]? = [AssetColor()] + ) { + self.info = info + self.colors = colors + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetCustomColor.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetCustomColor.swift new file mode 100644 index 0000000..a37579c --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetCustomColor.swift @@ -0,0 +1,87 @@ +import Foundation + +public enum AssetCustomColor: Codable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case colorSpace = "color-space" + case components + } + + private enum CodingColorSpace: String, Codable { + case sRGB = "srgb" + case extendedSRGB = "extended-srgb" + case extendedLinearSRGB = "extended-linear-srgb" + case displayP3 = "display-p3" + case grayGamma = "gray-gamma-22" + case extendedGray = "extended-gray" + } + + // MARK: - Enumeration Cases + + case sRGB(components: AssetColorComponents?) + case extendedSRGB(components: AssetColorComponents?) + case extendedLinearSRGB(components: AssetColorComponents?) + case displayP3(components: AssetColorComponents?) + case grayGamma(grayscale: AssetColorGrayscale?) + case extendedGray(grayscale: AssetColorGrayscale?) + + // MARK: - Initializers + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + switch try container.decode(CodingColorSpace.self, forKey: .colorSpace) { + case .sRGB: + self = .sRGB(components: try container.decodeIfPresent(forKey: .components)) + + case .extendedSRGB: + self = .extendedSRGB(components: try container.decodeIfPresent(forKey: .components)) + + case .extendedLinearSRGB: + self = .extendedLinearSRGB(components: try container.decodeIfPresent(forKey: .components)) + + case .displayP3: + self = .displayP3(components: try container.decodeIfPresent(forKey: .components)) + + case .grayGamma: + self = .grayGamma(grayscale: try container.decodeIfPresent(forKey: .components)) + + case .extendedGray: + self = .extendedGray(grayscale: try container.decodeIfPresent(forKey: .components)) + } + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .sRGB(components): + try container.encode(CodingColorSpace.sRGB, forKey: .colorSpace) + try container.encodeIfPresent(components, forKey: .components) + + case let .extendedSRGB(components): + try container.encode(CodingColorSpace.extendedSRGB, forKey: .colorSpace) + try container.encodeIfPresent(components, forKey: .components) + + case let .extendedLinearSRGB(components): + try container.encode(CodingColorSpace.extendedLinearSRGB, forKey: .colorSpace) + try container.encodeIfPresent(components, forKey: .components) + + case let .displayP3(components): + try container.encode(CodingColorSpace.displayP3, forKey: .colorSpace) + try container.encodeIfPresent(components, forKey: .components) + + case let .grayGamma(grayscale): + try container.encode(CodingColorSpace.grayGamma, forKey: .colorSpace) + try container.encodeIfPresent(grayscale, forKey: .components) + + case let .extendedGray(grayscale): + try container.encode(CodingColorSpace.extendedGray, forKey: .colorSpace) + try container.encodeIfPresent(grayscale, forKey: .components) + } + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetSystemColor.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetSystemColor.swift new file mode 100644 index 0000000..7f69c17 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetSystemColor.swift @@ -0,0 +1,16 @@ +import Foundation + +public struct AssetSystemColor: Codable, Hashable { + + // MARK: - Instance Properties + + public var platform: AssetSystemColorPlatform? + public var reference: String? + + // MARK: - Initializers + + public init(platform: AssetSystemColorPlatform?, reference: String?) { + self.platform = platform + self.reference = reference + } +} diff --git a/Sources/FigmaGenTools/Assets/ColorSet/AssetSystemColorPlatform.swift b/Sources/FigmaGenTools/Assets/ColorSet/AssetSystemColorPlatform.swift new file mode 100644 index 0000000..758e8d6 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ColorSet/AssetSystemColorPlatform.swift @@ -0,0 +1,10 @@ +import Foundation + +public enum AssetSystemColorPlatform: String, Codable { + + // MARK: - Enumeration Cases + + case iOS = "ios" + case macOS = "osx" + case tvOS = "tvos" +} diff --git a/Sources/FigmaGenTools/Assets/Folder/AssetFolder.swift b/Sources/FigmaGenTools/Assets/Folder/AssetFolder.swift new file mode 100644 index 0000000..dcf27bf --- /dev/null +++ b/Sources/FigmaGenTools/Assets/Folder/AssetFolder.swift @@ -0,0 +1,112 @@ +import Foundation +import PathKit + +public struct AssetFolder { + + // MARK: - Type Methods + + public static func isValidFolder(at folderPath: String) -> Bool { + (try? Self(folderPath: folderPath)) != nil + } + + // MARK: - Instance Properties + + public var colorSets: [String: AssetColorSet] + public var imageSets: [String: AssetImageSet] + public var folders: [String: AssetFolder] + public var contents: AssetFolderContents + + // MARK: - Initializers + + public init( + colorSets: [String: AssetColorSet] = [:], + imageSets: [String: AssetImageSet] = [:], + folders: [String: Self] = [:], + contents: AssetFolderContents = AssetFolderContents() + ) { + self.colorSets = colorSets + self.imageSets = imageSets + self.folders = folders + self.contents = contents + } + + public init(folderPath: String) throws { + let folderPath = Path(folderPath) + + let contentsPath = folderPath.appending(.contentsPath) + let contentsData = try contentsPath.read() + let contentsDecoder = JSONDecoder() + + contents = try contentsDecoder.decode(from: contentsData) + + colorSets = [:] + imageSets = [:] + folders = [:] + + try folderPath + .children() + .lazy + .filter { $0.isDirectory } + .forEach { nodePath in + let nodeName = nodePath.lastComponentWithoutExtension + + switch nodePath.extension { + case AssetColorSet.pathExtension: + colorSets[nodeName] = try AssetColorSet(folderPath: nodePath.string) + + case AssetImageSet.pathExtension: + imageSets[nodeName] = try AssetImageSet(folderPath: nodePath.string) + + case nil: + folders[nodeName] = try Self(folderPath: nodePath.string) + + default: + break + } + } + } + + // MARK: - Instance Methods + + private func saveNodes(_ nodes: [String: T], in folderPath: Path) throws { + try nodes.forEach { node in + let nodePath = folderPath.appending( + fileName: node.key, + extension: T.pathExtension + ) + + try node.value.save(in: nodePath.string) + } + } + + private func saveFolders(in folderPath: Path) throws { + try folders.forEach { folder in + try folder.value.save(in: folderPath.appending(folder.key).string) + } + } + + // MARK: - + + public func save(in folderPath: String) throws { + let folderPath = Path(folderPath) + + try folderPath.mkpath() + + try saveNodes(colorSets, in: folderPath) + try saveNodes(imageSets, in: folderPath) + try saveFolders(in: folderPath) + + let contentsEncoder = JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys]) + let contentsData = try contentsEncoder.encode(contents) + let contentsPath = folderPath.appending(.contentsPath) + + try contentsPath.write(contentsData) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let contentsPath = "Contents.json" +} diff --git a/Sources/FigmaGenTools/Assets/Folder/AssetFolderContents.swift b/Sources/FigmaGenTools/Assets/Folder/AssetFolderContents.swift new file mode 100644 index 0000000..b8c50a0 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/Folder/AssetFolderContents.swift @@ -0,0 +1,16 @@ +import Foundation + +public struct AssetFolderContents: Codable, Hashable { + + // MARK: - Instance Properties + + public var info: AssetInfo? + public var properties: AssetFolderProperties? + + // MARK: - Initializers + + public init(info: AssetInfo? = AssetInfo(), properties: AssetFolderProperties? = nil) { + self.info = info + self.properties = properties + } +} diff --git a/Sources/FigmaGenTools/Assets/Folder/AssetFolderProperties.swift b/Sources/FigmaGenTools/Assets/Folder/AssetFolderProperties.swift new file mode 100644 index 0000000..0dac892 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/Folder/AssetFolderProperties.swift @@ -0,0 +1,30 @@ +import Foundation + +public struct AssetFolderProperties: Codable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case compressionType = "compression-type" + case providesNamespace = "provides-namespace" + case onDemandResourceTags = "on-demand-resource-tags" + } + + // MARK: - Instance Properties + + public var compressionType: AssetCompressionType? + public var providesNamespace: Bool? + public var onDemandResourceTags: [String]? + + // MARK: - Initializers + + public init( + compressionType: AssetCompressionType? = nil, + providesNamespace: Bool? = nil, + onDemandResourceTags: [String]? = nil + ) { + self.compressionType = compressionType + self.providesNamespace = providesNamespace + self.onDemandResourceTags = onDemandResourceTags + } +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImage.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImage.swift new file mode 100644 index 0000000..eba4ff3 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImage.swift @@ -0,0 +1,109 @@ +import Foundation + +public struct AssetImage: Codable, Hashable { + + // MARK: - Enumeration Cases + + private enum CodingKeys: String, CodingKey { + case fileName = "filename" + case scale + case appearances + case widthClass = "width-class" + case heightClass = "height-class" + case alignmentInsets = "alignment-insets" + case graphicsFeatureSet = "graphics-feature-set" + case memory = "memory" + case compressionType = "compression-type" + case languageDirection = "language-direction" + case displayGamut = "display-gamut" + case locale + } + + // MARK: - Instance Properties + + public var fileName: String? + public var scale: AssetImageScale? + public var appearances: [AssetAppearance]? + public var widthClass: AssetImageSizeClass? + public var heightClass: AssetImageSizeClass? + public var alignmentInsets: AssetImageAlignmentInsets? + public var graphicsFeatureSet: AssetImageGraphicsFeatureSet? + public var memory: AssetImageMemory? + public var compressionType: AssetCompressionType? + public var languageDirection: AssetImageLanguageDirection? + public var displayGamut: AssetDisplayGamut? + public var locale: String? + public var idiom: AssetIdiom + + // MARK: - Initializers + + public init( + fileName: String? = nil, + scale: AssetImageScale? = nil, + appearances: [AssetAppearance]? = nil, + widthClass: AssetImageSizeClass? = nil, + heightClass: AssetImageSizeClass? = nil, + alignmentInsets: AssetImageAlignmentInsets? = nil, + graphicsFeatureSet: AssetImageGraphicsFeatureSet? = nil, + memory: AssetImageMemory? = nil, + compressionType: AssetCompressionType? = nil, + languageDirection: AssetImageLanguageDirection? = nil, + displayGamut: AssetDisplayGamut? = nil, + locale: String? = nil, + idiom: AssetIdiom = .universal + ) { + self.fileName = fileName + self.scale = scale + self.appearances = appearances + self.widthClass = widthClass + self.heightClass = heightClass + self.alignmentInsets = alignmentInsets + self.graphicsFeatureSet = graphicsFeatureSet + self.memory = memory + self.compressionType = compressionType + self.languageDirection = languageDirection + self.displayGamut = displayGamut + self.locale = locale + self.idiom = idiom + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + fileName = try container.decodeIfPresent(forKey: .fileName) + scale = try container.decodeIfPresent(forKey: .scale) + appearances = try container.decodeIfPresent(forKey: .appearances) + widthClass = try container.decodeIfPresent(forKey: .widthClass) + heightClass = try container.decodeIfPresent(forKey: .heightClass) + alignmentInsets = try container.decodeIfPresent(forKey: .alignmentInsets) + graphicsFeatureSet = try container.decodeIfPresent(forKey: .graphicsFeatureSet) + memory = try container.decodeIfPresent(forKey: .memory) + compressionType = try container.decodeIfPresent(forKey: .compressionType) + languageDirection = try container.decodeIfPresent(forKey: .languageDirection) + displayGamut = try container.decodeIfPresent(forKey: .displayGamut) + locale = try container.decodeIfPresent(forKey: .locale) + + idiom = try AssetIdiom(from: decoder) + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(fileName, forKey: .fileName) + try container.encodeIfPresent(scale, forKey: .scale) + try container.encodeIfPresent(appearances, forKey: .appearances) + try container.encodeIfPresent(widthClass, forKey: .widthClass) + try container.encodeIfPresent(heightClass, forKey: .heightClass) + try container.encodeIfPresent(alignmentInsets, forKey: .alignmentInsets) + try container.encodeIfPresent(graphicsFeatureSet, forKey: .graphicsFeatureSet) + try container.encodeIfPresent(memory, forKey: .memory) + try container.encodeIfPresent(compressionType, forKey: .compressionType) + try container.encodeIfPresent(languageDirection, forKey: .languageDirection) + try container.encodeIfPresent(displayGamut, forKey: .displayGamut) + try container.encodeIfPresent(locale, forKey: .locale) + + try idiom.encode(to: encoder) + } +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageAlignmentInsets.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageAlignmentInsets.swift new file mode 100644 index 0000000..89651bb --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageAlignmentInsets.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct AssetImageAlignmentInsets: Codable, Hashable { + + // MARK: - Instance Properties + + public var top: Double + public var left: Double + public var right: Double + public var bottom: Double + + // MARK: - Initializers + + public init(top: Double, left: Double, right: Double, bottom: Double) { + self.top = top + self.left = left + self.right = right + self.bottom = bottom + } +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageAutoScaling.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageAutoScaling.swift new file mode 100644 index 0000000..25bacbc --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageAutoScaling.swift @@ -0,0 +1,8 @@ +import Foundation + +public enum AssetImageAutoScaling: String, Codable { + + // MARK: - Enumeration Cases + + case auto +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageGraphicsFeatureSet.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageGraphicsFeatureSet.swift new file mode 100644 index 0000000..cd388bd --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageGraphicsFeatureSet.swift @@ -0,0 +1,14 @@ +import Foundation + +public enum AssetImageGraphicsFeatureSet: String, Codable { + + // MARK: - Enumeration Cases + + case apple1 = "metal1v2" + case apple2 = "metal2v2" + case apple3v1 = "metal3v1" + case apple3 = "metal3v2" + case apple4 = "metal4v1" + case apple5 = "metal5v1" + case apple6 +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageLanguageDirection.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageLanguageDirection.swift new file mode 100644 index 0000000..6b58174 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageLanguageDirection.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum AssetImageLanguageDirection: String, Codable { + + // MARK: - Enumeration Cases + + case leftToRight = "left-to-right" + case rightToLeft = "right-to-left" +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageMemory.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageMemory.swift new file mode 100644 index 0000000..ce19b4d --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageMemory.swift @@ -0,0 +1,12 @@ +import Foundation + +public enum AssetImageMemory: String, Codable { + + // MARK: - Enumeration Cases + + case memory1GB = "1GB" + case memory2GB = "2GB" + case memory3GB = "3GB" + case memory4GB = "4GB" + case memory6GB = "6GB" +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageProperties.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageProperties.swift new file mode 100644 index 0000000..cffbd8d --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageProperties.swift @@ -0,0 +1,42 @@ +import Foundation + +public struct AssetImageProperties: Codable, Hashable { + + // MARK: - Nested Types + + private enum CodingKeys: String, CodingKey { + case templateRenderingIntent = "template-rendering-intent" + case preserveVectorRepresentation = "preserves-vector-representation" + case autoScaling = "auto-scaling" + case compressionType = "compression-type" + case providesNamespace = "provides-namespace" + case onDemandResourceTags = "on-demand-resource-tags" + } + + // MARK: - Instance Properties + + public var templateRenderingIntent: AssetImageTemplateRenderingIntent? + public var preserveVectorRepresentation: Bool? + public var autoScaling: AssetImageAutoScaling? + public var compressionType: AssetCompressionType? + public var providesNamespace: Bool? + public var onDemandResourceTags: [String]? + + // MARK: - Initializers + + public init( + templateRenderingIntent: AssetImageTemplateRenderingIntent? = nil, + preserveVectorRepresentation: Bool? = nil, + autoScaling: AssetImageAutoScaling? = nil, + compressionType: AssetCompressionType? = nil, + providesNamespace: Bool? = nil, + onDemandResourceTags: [String]? = nil + ) { + self.templateRenderingIntent = templateRenderingIntent + self.preserveVectorRepresentation = preserveVectorRepresentation + self.autoScaling = autoScaling + self.compressionType = compressionType + self.providesNamespace = providesNamespace + self.onDemandResourceTags = onDemandResourceTags + } +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageScale.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageScale.swift new file mode 100644 index 0000000..87635f4 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageScale.swift @@ -0,0 +1,10 @@ +import Foundation + +public enum AssetImageScale: String, Codable { + + // MARK: - Enumeration Cases + + case scale1x = "1x" + case scale2x = "2x" + case scale3x = "3x" +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSet.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSet.swift new file mode 100644 index 0000000..399f65c --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSet.swift @@ -0,0 +1,18 @@ +import Foundation + +public struct AssetImageSet: AssetNode { + + // MARK: - Type Properties + + public static let pathExtension = "imageset" + + // MARK: - Instance Properties + + public var contents: AssetImageSetContents + + // MARK: - Initializers + + public init(contents: AssetImageSetContents = AssetImageSetContents()) { + self.contents = contents + } +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSetContents.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSetContents.swift new file mode 100644 index 0000000..ae8750e --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSetContents.swift @@ -0,0 +1,22 @@ +import Foundation + +public struct AssetImageSetContents: Codable, Hashable { + + // MARK: - Instance Properties + + public var info: AssetInfo? + public var properties: AssetImageProperties? + public var images: [AssetImage]? + + // MARK: - Initializers + + public init( + info: AssetInfo? = AssetInfo(), + properties: AssetImageProperties? = nil, + images: [AssetImage]? = [AssetImage()] + ) { + self.info = info + self.properties = properties + self.images = images?.sorted { $0.scale?.rawValue ?? .empty <= $1.scale?.rawValue ?? .empty } + } +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSizeClass.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSizeClass.swift new file mode 100644 index 0000000..cf0cf7e --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageSizeClass.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum AssetImageSizeClass: String, Codable { + + // MARK: - Enumeration Cases + + case compact + case regular +} diff --git a/Sources/FigmaGenTools/Assets/ImageSet/AssetImageTemplateRenderingIntent.swift b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageTemplateRenderingIntent.swift new file mode 100644 index 0000000..44c3243 --- /dev/null +++ b/Sources/FigmaGenTools/Assets/ImageSet/AssetImageTemplateRenderingIntent.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum AssetImageTemplateRenderingIntent: String, Codable { + + // MARK: - Enumeration Cases + + case original + case template +} diff --git a/Sources/FigmaGenTools/Extensions/Array+Extensions.swift b/Sources/FigmaGenTools/Extensions/Array+Extensions.swift new file mode 100644 index 0000000..aca52fd --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Array+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Array { + + public func chunked(size: Int) -> [[Element]] { + stride(from: 0, to: count, by: size).map { + Array(self[$0 ..< Swift.min($0 + size, count)]) + } + } +} diff --git a/Sources/FigmaGenTools/Extensions/Bundle+Extensions.swift b/Sources/FigmaGenTools/Extensions/Bundle+Extensions.swift new file mode 100644 index 0000000..669d087 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Bundle+Extensions.swift @@ -0,0 +1,40 @@ +import Foundation + +extension Bundle { + + // MARK: - Instance Properties + + public var bundleName: String? { + string(forInfoDictionaryKey: "CFBundleName") + } + + public var developmentRegion: String? { + string(forInfoDictionaryKey: "CFBundleDevelopmentRegion") + } + + public var displayName: String? { + string(forInfoDictionaryKey: "CFBundleDisplayName") + } + + public var executableName: String? { + string(forInfoDictionaryKey: "CFBundleExecutable") + } + + public var version: String? { + string(forInfoDictionaryKey: "CFBundleShortVersionString") + } + + public var build: String? { + string(forInfoDictionaryKey: "CFBundleVersion") + } + + // MARK: - Instance Methods + + public func string(forInfoDictionaryKey key: String) -> String? { + object(forInfoDictionaryKey: key) as? String + } + + public func url(forInfoDictionaryKey key: String) -> URL? { + string(forInfoDictionaryKey: key).flatMap(URL.init(string:)) + } +} diff --git a/Sources/Tools/Extensions/CLI+Extensions.swift b/Sources/FigmaGenTools/Extensions/CLI+Extensions.swift similarity index 66% rename from Sources/Tools/Extensions/CLI+Extensions.swift rename to Sources/FigmaGenTools/Extensions/CLI+Extensions.swift index 29e632b..5ae2b4d 100644 --- a/Sources/Tools/Extensions/CLI+Extensions.swift +++ b/Sources/FigmaGenTools/Extensions/CLI+Extensions.swift @@ -1,9 +1,3 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation import SwiftCLI @@ -11,7 +5,7 @@ extension CLI { // MARK: - Instance Methods - func goAndExitOnError() { + public func goAndExitOnError() { let result = go() if result != EXIT_SUCCESS { diff --git a/Sources/FigmaGenTools/Extensions/Collection+Extensions.swift b/Sources/FigmaGenTools/Extensions/Collection+Extensions.swift new file mode 100644 index 0000000..196d7f1 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Collection+Extensions.swift @@ -0,0 +1,14 @@ +import Foundation + +extension Collection { + + // MARK: - Instance Methods + + public func contains(index: Index) -> Bool { + ((index >= startIndex) && (index < endIndex)) + } + + public subscript(safe index: Index) -> Element? { + contains(index: index) ? self[index] : nil + } +} diff --git a/Sources/FigmaGenTools/Extensions/Decodable+Extensions.swift b/Sources/FigmaGenTools/Extensions/Decodable+Extensions.swift new file mode 100644 index 0000000..fa88b4b --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Decodable+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Decodable { + + public init(from decoder: Decoder, forKey key: Key) throws { + let container = try decoder.container(keyedBy: Key.self) + + try self.init(from: try container.superDecoder(forKey: key)) + } +} diff --git a/Sources/FigmaGenTools/Extensions/DispatchQueue+Extensions.swift b/Sources/FigmaGenTools/Extensions/DispatchQueue+Extensions.swift new file mode 100644 index 0000000..2479b3f --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/DispatchQueue+Extensions.swift @@ -0,0 +1,14 @@ +import Foundation + +extension DispatchQueue { + + // MARK: - Instance Methods + + public func async(flags: DispatchWorkItemFlags?, _ work: @escaping() -> Void) { + if let flags { + async(flags: flags, execute: work) + } else { + async(execute: work) + } + } +} diff --git a/Sources/FigmaGenTools/Extensions/Encodable+Extensions.swift b/Sources/FigmaGenTools/Extensions/Encodable+Extensions.swift new file mode 100644 index 0000000..017c854 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Encodable+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Encodable { + + public func encode(to encoder: Encoder, forKey key: Key) throws { + var container = encoder.container(keyedBy: Key.self) + + try encode(to: container.superEncoder(forKey: key)) + } +} diff --git a/Sources/Tools/Extensions/FloatingPoint+Extensions.swift b/Sources/FigmaGenTools/Extensions/FloatingPoint+Extensions.swift similarity index 51% rename from Sources/Tools/Extensions/FloatingPoint+Extensions.swift rename to Sources/FigmaGenTools/Extensions/FloatingPoint+Extensions.swift index f3f7efc..0e04acd 100644 --- a/Sources/Tools/Extensions/FloatingPoint+Extensions.swift +++ b/Sources/FigmaGenTools/Extensions/FloatingPoint+Extensions.swift @@ -1,16 +1,10 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation extension FloatingPoint { // MARK: - Instance Methods - func rounded(precision: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self { + public func rounded(precision: Int, rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Self { let scale = Self(precision * 10) return (self * scale).rounded(rule) / scale diff --git a/Sources/FigmaGenTools/Extensions/JSONDecoder+Extensions.swift b/Sources/FigmaGenTools/Extensions/JSONDecoder+Extensions.swift new file mode 100644 index 0000000..48ff60e --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/JSONDecoder+Extensions.swift @@ -0,0 +1,28 @@ +import Foundation + +extension JSONDecoder { + + // MARK: - Initializers + + public convenience init( + dateDecodingStrategy: DateDecodingStrategy = .deferredToDate, + dataDecodingStrategy: DataDecodingStrategy = .base64, + nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw, + keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys, + userInfo: [CodingUserInfoKey: Any] = [:] + ) { + self.init() + + self.dateDecodingStrategy = dateDecodingStrategy + self.dataDecodingStrategy = dataDecodingStrategy + self.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy + self.keyDecodingStrategy = keyDecodingStrategy + self.userInfo = userInfo + } + + // MARK: - Instance Methods + + public func decode(from data: Data) throws -> T { + try decode(T.self, from: data) + } +} diff --git a/Sources/FigmaGenTools/Extensions/JSONEncoder+Extensions.swift b/Sources/FigmaGenTools/Extensions/JSONEncoder+Extensions.swift new file mode 100644 index 0000000..47fc8d7 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/JSONEncoder+Extensions.swift @@ -0,0 +1,24 @@ +import Foundation + +extension JSONEncoder { + + // MARK: - Initializers + + public convenience init( + outputFormatting: OutputFormatting = [], + dateEncodingStrategy: DateEncodingStrategy = .deferredToDate, + dataEncodingStrategy: DataEncodingStrategy = .base64, + nonConformingFloatEncodingStrategy: NonConformingFloatEncodingStrategy = .throw, + keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys, + userInfo: [CodingUserInfoKey: Any] = [:] + ) { + self.init() + + self.outputFormatting = outputFormatting + self.dateEncodingStrategy = dateEncodingStrategy + self.dataEncodingStrategy = dataEncodingStrategy + self.nonConformingFloatEncodingStrategy = nonConformingFloatEncodingStrategy + self.keyEncodingStrategy = keyEncodingStrategy + self.userInfo = userInfo + } +} diff --git a/Sources/FigmaGenTools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift b/Sources/FigmaGenTools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift new file mode 100644 index 0000000..8374090 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift @@ -0,0 +1,14 @@ +import Foundation + +extension KeyedDecodingContainerProtocol { + + // MARK: - Instance Methods + + public func decode(forKey key: Key) throws -> T { + try decode(T.self, forKey: key) + } + + public func decodeIfPresent(forKey key: Self.Key) throws -> T? { + try decodeIfPresent(T.self, forKey: key) + } +} diff --git a/Sources/FigmaGenTools/Extensions/OperatingSystemVersion+Extensions.swift b/Sources/FigmaGenTools/Extensions/OperatingSystemVersion+Extensions.swift new file mode 100644 index 0000000..261a946 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/OperatingSystemVersion+Extensions.swift @@ -0,0 +1,14 @@ +import Foundation + +extension OperatingSystemVersion { + + // MARK: - Instance Properties + + public var fullVersion: String { + guard patchVersion > 0 else { + return "\(majorVersion).\(minorVersion)" + } + + return "\(majorVersion).\(minorVersion).\(patchVersion)" + } +} diff --git a/Sources/FigmaGenTools/Extensions/Optional+Extensions.swift b/Sources/FigmaGenTools/Extensions/Optional+Extensions.swift new file mode 100644 index 0000000..32e93cf --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Optional+Extensions.swift @@ -0,0 +1,19 @@ +import Foundation + +extension Optional { + + // MARK: - Instance Properties + + public var isNil: Bool { + self == nil + } +} + +extension Optional where Wrapped: Collection { + + // MARK: - Instance Properties + + public var isEmptyOrNil: Bool { + self?.isEmpty ?? true + } +} diff --git a/Sources/FigmaGenTools/Extensions/Path+Extensions.swift b/Sources/FigmaGenTools/Extensions/Path+Extensions.swift new file mode 100644 index 0000000..ddb1327 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Path+Extensions.swift @@ -0,0 +1,19 @@ +import Foundation +import PathKit + +extension Path { + + // MARK: - Instance Methods + + public func appending(_ path: Path) -> Path { + self + path + } + + public func appending(_ path: String) -> Path { + self + path + } + + public func appending(fileName: String, `extension`: String) -> Path { + self + "\(fileName).\(`extension`)" + } +} diff --git a/Sources/FigmaGenTools/Extensions/ProcessInfo+Extensions.swift b/Sources/FigmaGenTools/Extensions/ProcessInfo+Extensions.swift new file mode 100644 index 0000000..23dc4da --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/ProcessInfo+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension ProcessInfo { + + // MARK: - Instance Properties + + public var executablePath: String { + arguments[0] + } +} diff --git a/Sources/FigmaGenTools/Extensions/Promise+Extensions.swift b/Sources/FigmaGenTools/Extensions/Promise+Extensions.swift new file mode 100644 index 0000000..49347e1 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Promise+Extensions.swift @@ -0,0 +1,66 @@ +import Foundation +import PromiseKit + +extension Promise { + + // MARK: - Type Methods + + public static func error(_ error: Error) -> Promise { + Promise(error: error) + } + + // MARK: - Initializers + + public convenience init(asyncFunc: @escaping () async throws -> T) { + self.init { resolver in + Task { + do { + let result = try await asyncFunc() + resolver.fulfill(result) + } catch { + resolver.reject(error) + } + } + } + } + + // MARK: - Instance Methods + + public func nest( + on queue: DispatchQueue? = conf.Q.map, + flags: DispatchWorkItemFlags? = nil, + _ body: @escaping(T) throws -> U + ) -> Promise { + then(on: queue, flags: flags) { value in + try body(value).map(on: nil) { _ in + value + } + } + } + + public func asOptional() -> Promise { + map(on: nil) { $0 as T? } + } +} + +public func perform( + on queue: DispatchQueue? = conf.Q.map, + flags: DispatchWorkItemFlags? = nil, + _ body: @escaping () throws -> T +) -> Promise { + Promise { seal in + let work = { + do { + seal.fulfill(try body()) + } catch { + seal.reject(error) + } + } + + if let queue { + queue.async(flags: flags, work) + } else { + work() + } + } +} diff --git a/Sources/FigmaGenTools/Extensions/RangeReplaceableCollection+Extensions.swift b/Sources/FigmaGenTools/Extensions/RangeReplaceableCollection+Extensions.swift new file mode 100644 index 0000000..7e687fd --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/RangeReplaceableCollection+Extensions.swift @@ -0,0 +1,50 @@ +import Foundation + +extension RangeReplaceableCollection { + + // MARK: - Instance Methods + + public mutating func prepend(contentsOf collection: T) where Self.Element == T.Element { + insert(contentsOf: collection, at: startIndex) + } + + public mutating func prepend(_ element: Element) { + insert(element, at: startIndex) + } + + public func prepending(contentsOf collection: T) -> Self where Self.Element == T.Element { + collection + self + } + + public func prepending(_ element: Element) -> Self { + prepending(contentsOf: [element]) + } + + public func appending(contentsOf collection: T) -> Self where Self.Element == T.Element { + self + collection + } + + public func appending(_ element: Element) -> Self { + appending(contentsOf: [element]) + } +} + +extension RangeReplaceableCollection where Element: Equatable { + + // MARK: - Instance Methods + + @discardableResult + public mutating func removeFirst(_ element: Element) -> Element? { + guard let index = firstIndex(of: element) else { + return nil + } + + return remove(at: index) + } + + public func removingFirst(_ element: Element) -> Self { + var copy = self + copy.removeFirst(element) + return copy + } +} diff --git a/Sources/FigmaGenTools/Extensions/Sequence+Extensions.swift b/Sources/FigmaGenTools/Extensions/Sequence+Extensions.swift new file mode 100644 index 0000000..13dd679 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/Sequence+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension Sequence { + + // MARK: - Instance Properties + + public var lazyFirst: Element? { + first { _ in true } + } +} diff --git a/Sources/FigmaGenTools/Extensions/SingleValueDecodingContainer+Extensions.swift b/Sources/FigmaGenTools/Extensions/SingleValueDecodingContainer+Extensions.swift new file mode 100644 index 0000000..3473459 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/SingleValueDecodingContainer+Extensions.swift @@ -0,0 +1,10 @@ +import Foundation + +extension SingleValueDecodingContainer { + + // MARK: - Instance Methods + + public func decode() throws -> T { + try decode(T.self) + } +} diff --git a/Sources/FigmaGenTools/Extensions/String+Extensions.swift b/Sources/FigmaGenTools/Extensions/String+Extensions.swift new file mode 100644 index 0000000..9fb6162 --- /dev/null +++ b/Sources/FigmaGenTools/Extensions/String+Extensions.swift @@ -0,0 +1,99 @@ +import Foundation + +extension String { + + // MARK: - Type Properties + + public static let empty = "" + + // MARK: - Instance Properties + + public var firstUppercased: String { + prefix(1).uppercased().appending(dropFirst()) + } + + public var firstLowercased: String { + prefix(1).lowercased().appending(dropFirst()) + } + + public var firstCapitalized: String { + prefix(1).capitalized.appending(dropFirst()) + } + + public var camelized: String { + components(separatedBy: CharacterSet.alphanumerics.inverted) + .map { $0.firstUppercased } + .joined() + } + + public var snakeCased: String { + components(separatedBy: CharacterSet.alphanumerics.inverted) + .filter { !$0.isEmpty } + .map { $0.lowercased() } + .joined(separator: "_") + } + + // MARK: - Instance Methods + + public func slice( + from startCharactet: Character, + to endCharacter: Character, + includingBounds: Bool + ) -> Substring? { + if includingBounds { + guard let startIndex = firstIndex(of: startCharactet) else { + return nil + } + + guard let endIndex = firstIndex(of: endCharacter) else { + return nil + } + + let substringRange = startIndex...endIndex + + return self[substringRange] + } + + guard let rangeFrom = range(of: String(startCharactet))?.upperBound else { + return nil + } + + guard let rangeTo = self[rangeFrom...].range(of: String(endCharacter))?.lowerBound else { + return nil + } + + return self[rangeFrom.. [String] { + let regex = try NSRegularExpression(pattern: pattern) + let matches = regex.matches(in: self, range: NSRange(0.. String + ) throws -> String { + let expression = try NSRegularExpression(pattern: pattern, options: []) + let matches = expression.matches(in: self, options: [], range: NSRange(startIndex..() throws -> T { + try decode(T.self) + } + + public mutating func decodeIfPresent() throws -> T? { + try decodeIfPresent(T.self) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyEncoder.swift b/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyEncoder.swift new file mode 100644 index 0000000..08ee92c --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyEncoder.swift @@ -0,0 +1,12 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public protocol HTTPBodyEncoder { + + // MARK: - Instance Methods + + func encode(request: URLRequest, parameters: T) throws -> URLRequest +} diff --git a/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyJSONEncoder.swift b/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyJSONEncoder.swift new file mode 100644 index 0000000..8582a10 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyJSONEncoder.swift @@ -0,0 +1,46 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public final class HTTPBodyJSONEncoder: HTTPBodyEncoder { + + // MARK: - Type Properties + + public static let `default` = HTTPBodyJSONEncoder(jsonEncoder: JSONEncoder()) + + // MARK: - Instance Properties + + public let jsonEncoder: JSONEncoder + + // MARK: - Initializers + + public init(jsonEncoder: JSONEncoder) { + self.jsonEncoder = jsonEncoder + } + + // MARK: - Instance Methods + + public func encode(request: URLRequest, parameters: T) throws -> URLRequest { + var request = request + + request.httpBody = try jsonEncoder.encode(parameters) + + if !request.httpBody.isEmptyOrNil { + if request.value(forHTTPHeaderField: .contentTypeHeaderField) == nil { + request.setValue(.contentTypeHeaderValue, forHTTPHeaderField: .contentTypeHeaderField) + } + } + + return request + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let contentTypeHeaderField = "Content-Type" + fileprivate static let contentTypeHeaderValue = "application/json" +} diff --git a/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyURLEncoder.swift b/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyURLEncoder.swift new file mode 100644 index 0000000..6d98d28 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/BodyEncoders/HTTPBodyURLEncoder.swift @@ -0,0 +1,44 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public final class HTTPBodyURLEncoder: HTTPBodyEncoder { + + // MARK: - Type Properties + + public static let `default` = HTTPBodyURLEncoder(urlEncoder: URLEncoder()) + + // MARK: - Instance Properties + + public let urlEncoder: URLEncoder + + // MARK: - Initializers + + public init(urlEncoder: URLEncoder) { + self.urlEncoder = urlEncoder + } + + // MARK: - Instance Methods + + public func encode(request: URLRequest, parameters: T) throws -> URLRequest { + var request = request + + request.httpBody = try urlEncoder.encode(parameters) + + if !request.httpBody.isEmptyOrNil, request.value(forHTTPHeaderField: .contentTypeHeaderField) == nil { + request.setValue(.contentTypeHeaderValue, forHTTPHeaderField: .contentTypeHeaderField) + } + + return request + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let contentTypeHeaderField = "Content-Type" + fileprivate static let contentTypeHeaderValue = "application/x-www-form-urlencoded; charset=utf-8" +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPActivityIndicator.swift b/Sources/FigmaGenTools/HTTPService/HTTPActivityIndicator.swift new file mode 100644 index 0000000..50fa279 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPActivityIndicator.swift @@ -0,0 +1,13 @@ +import Foundation + +public protocol HTTPActivityIndicator: AnyObject { + + // MARK: - Instance Properties + + var activityCount: Int { get } + + // MARK: - Instance Methods + + func incrementActivityCount() + func decrementActivityCount() +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPAnyEncodable.swift b/Sources/FigmaGenTools/HTTPService/HTTPAnyEncodable.swift new file mode 100644 index 0000000..fa3f9b3 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPAnyEncodable.swift @@ -0,0 +1,20 @@ +import Foundation + +internal struct HTTPAnyEncodable: Encodable { + + // MARK: - Instance Properties + + internal let value: Encodable + + // MARK: - Initializers + + internal init(_ value: Encodable) { + self.value = value + } + + // MARK: - Instance Methods + + internal func encode(to encoder: Encoder) throws { + try value.encode(to: encoder) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPError.swift b/Sources/FigmaGenTools/HTTPService/HTTPError.swift new file mode 100644 index 0000000..f16a27f --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPError.swift @@ -0,0 +1,184 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct HTTPError: Error, CustomStringConvertible { + + // MARK: - Nested Types + + public enum Code { + case unknown + case cancelled + case fileSystem + case tooManyRequests + case networkConnection + case secureConnection + case timedOut + case badRequest + case badResponse + case resource + case server + case access + } + + // MARK: - Instance Properties + + public let code: Code + public let reason: Any? + public let data: Data? + + public var statusCode: HTTPStatusCode? { + reason as? HTTPStatusCode + } + + // MARK: - CustomStringConvertible + + public var description: String { + var description = "\(type(of: self)).\(code)" + + if let reason { + let reasonDescription: String + + if let reason = reason as? HTTPErrorStringConvertible { + reasonDescription = reason.httpErrorDescription + } else { + reasonDescription = String(reflecting: reason) + } + + description.append("(\(reasonDescription))") + } + + return description + } + + // MARK: - Initializers + + public init(code: Code, reason: Any? = nil, data: Data? = nil) { + self.code = code + self.reason = reason + self.data = data + } + + public init(urlError error: URLError, data: Data? = nil) { + // swiftlint:disable:previous function_body_length + + let code: Code + + switch error.code { + case .cancelled: + code = .cancelled + + case .fileDoesNotExist, + .fileIsDirectory, + .cannotCreateFile, + .cannotOpenFile, + .cannotCloseFile, + .cannotWriteToFile, + .cannotRemoveFile, + .cannotMoveFile: + code = .fileSystem + + case .backgroundSessionInUseByAnotherProcess, + .backgroundSessionRequiresSharedContainer, + .backgroundSessionWasDisconnected, + .cannotLoadFromNetwork, + .cannotFindHost, + .cannotConnectToHost, + .dnsLookupFailed, + .internationalRoamingOff, + .networkConnectionLost, + .notConnectedToInternet, + .secureConnectionFailed, + .callIsActive, + .dataNotAllowed: + code = .networkConnection + + case .clientCertificateRejected, + .clientCertificateRequired, + .serverCertificateHasBadDate, + .serverCertificateHasUnknownRoot, + .serverCertificateNotYetValid, + .serverCertificateUntrusted: + code = .secureConnection + + case .timedOut: + code = .timedOut + + case .badURL, + .requestBodyStreamExhausted, + .unsupportedURL: + code = .badRequest + + case .badServerResponse, + .cannotDecodeContentData, + .cannotDecodeRawData, + .cannotParseResponse, + .downloadDecodingFailedMidStream, + .downloadDecodingFailedToComplete: + code = .badResponse + + case .httpTooManyRedirects, + .redirectToNonExistentLocation, + .resourceUnavailable, + .zeroByteResource: + code = .resource + + case .noPermissionsToReadFile, + .userAuthenticationRequired, + .userCancelledAuthentication: + code = .access + + #if !os(Linux) + + case .appTransportSecurityRequiresSecureConnection: + code = .secureConnection + + case .dataLengthExceedsMaximum: + code = .resource + #endif + + default: + code = .unknown + } + + self.init(code: code, reason: error, data: data) + } + + public init?(statusCode: HTTPStatusCode, data: Data? = nil) { + guard statusCode.state != .success else { + return nil + } + + let code: Code + + switch statusCode { + case 429: + code = .tooManyRequests + + case 511: + code = .networkConnection + + case 408, 504: + code = .timedOut + + case 400, 406, 411, 412, 413, 414, 415, 416, 417, 422, 424, 425, 426, 428, 431, 449: + code = .badRequest + + case 404, 405, 409, 410, 423, 434, 451: + code = .resource + + case 500, 501, 502, 503, 505, 506, 507, 508, 509, 510: + code = .server + + case 401, 402, 403, 407, 444: + code = .access + + default: + code = .unknown + } + + self.init(code: code, reason: statusCode, data: data) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPErrorStringConvertible.swift b/Sources/FigmaGenTools/HTTPService/HTTPErrorStringConvertible.swift new file mode 100644 index 0000000..7446178 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPErrorStringConvertible.swift @@ -0,0 +1,23 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public protocol HTTPErrorStringConvertible { + + // MARK: - Instance Properties + + var httpErrorDescription: String { get } +} + +// MARK: - + +extension URLError: HTTPErrorStringConvertible { + + // MARK: - Instance Properties + + public var httpErrorDescription: String { + "\(type(of: self)).\(self.code.rawValue)" + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPHeader.swift b/Sources/FigmaGenTools/HTTPService/HTTPHeader.swift new file mode 100644 index 0000000..dbbc7eb --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPHeader.swift @@ -0,0 +1,129 @@ +import Foundation + +public struct HTTPHeader: Equatable, CustomStringConvertible { + + // MARK: - Type Methods + + private static func qualityEncoded (_ collection: T) -> String where T.Element == String { + collection + .enumerated() + .map { "\($1);q=\(1.0 - (Double($0) * 0.1))" } + .joined(separator: ", ") + } + + // MARK: - + + public static func acceptCharset(_ value: String) -> Self { + Self(name: "Accept-Charset", value: value) + } + + public static func acceptLanguage(_ value: String) -> Self { + Self(name: "Accept-Language", value: value) + } + + public static func acceptEncoding(_ value: String) -> Self { + Self(name: "Accept-Encoding", value: value) + } + + public static func authorization(username: String, password: String) -> Self { + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + + return authorization("Basic \(credential)") + } + + public static func authorization(bearerToken: String) -> Self { + authorization("Bearer \(bearerToken)") + } + + public static func authorization(_ value: String) -> Self { + Self(name: "Authorization", value: value) + } + + public static func contentDisposition(_ value: String) -> Self { + Self(name: "Content-Disposition", value: value) + } + + public static func contentType(_ value: String) -> Self { + Self(name: "Content-Type", value: value) + } + + public static func userAgent(_ value: String) -> Self { + Self(name: "User-Agent", value: value) + } + + public static func cookie(_ value: String) -> Self { + Self(name: "Cookie", value: value) + } + + // MARK: - + + public static func defaultAcceptLanguage() -> Self { + acceptLanguage(qualityEncoded(Locale.preferredLanguages.prefix(6))) + } + + public static func defaultAcceptEncoding() -> Self { + let encodings: [String] + + if #available(iOS 11.0, macOS 10.13, tvOS 11.0, watchOS 4.0, *) { + encodings = ["br", "gzip", "deflate"] + } else { + encodings = ["gzip", "deflate"] + } + + return acceptEncoding(qualityEncoded(encodings)) + } + + public static func defaultUserAgent(bundle: Bundle = Bundle.main) -> Self { + let systemName: String + + #if os(iOS) + systemName = "iOS" + #elseif os(watchOS) + systemName = "watchOS" + #elseif os(tvOS) + systemName = "tvOS" + #elseif os(macOS) + systemName = "macOS" + #elseif os(Linux) + systemName = "Linux" + #else + systemName = "Unknown" + #endif + + let system = "\(systemName) \(ProcessInfo.processInfo.operatingSystemVersion.fullVersion)" + + let appBundleIdentifier = bundle.bundleIdentifier ?? "Unknown" + let appExecutableName = bundle.executableName ?? "Unknown" + let appVersion = bundle.version ?? "Unknown" + let appBuild = bundle.build ?? "Unknown" + + return userAgent("\(appExecutableName)/\(appVersion) (\(appBundleIdentifier); build:\(appBuild); \(system))") + } + + // MARK: - Instance Properties + + public let name: String + public let value: String + + // MARK: - CustomStringConvertible + + public var description: String { + "\(name): \(value)" + } + + // MARK: - Initializers + + public init(name: String, value: String) { + self.name = name + self.value = value + } +} + +extension Collection where Element == HTTPHeader { + + // MARK: - Instance Properties + + public var rawHTTPHeaders: [String: String] { + Dictionary(map { ($0.name, $0.value) }) { $1 } + } +} diff --git a/Sources/Services/API/FigmaAPIHTTPMethod.swift b/Sources/FigmaGenTools/HTTPService/HTTPMethod.swift similarity index 66% rename from Sources/Services/API/FigmaAPIHTTPMethod.swift rename to Sources/FigmaGenTools/HTTPService/HTTPMethod.swift index cccc8de..3f19b1c 100644 --- a/Sources/Services/API/FigmaAPIHTTPMethod.swift +++ b/Sources/FigmaGenTools/HTTPService/HTTPMethod.swift @@ -1,12 +1,6 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import Foundation -enum FigmaAPIHTTPMethod: String { +public enum HTTPMethod: String { // MARK: - Enumeration Cases diff --git a/Sources/FigmaGenTools/HTTPService/HTTPResponse.swift b/Sources/FigmaGenTools/HTTPService/HTTPResponse.swift new file mode 100644 index 0000000..98dca6d --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPResponse.swift @@ -0,0 +1,70 @@ +import Foundation + +public struct HTTPResponse: CustomStringConvertible { + + // MARK: - Instance Properties + + public let result: Result + public let statusCode: HTTPStatusCode? + public let headers: [HTTPHeader]? + + // MARK: - + + public var value: T? { + switch result { + case let .success(value): + return value + + case .failure: + return nil + } + } + + public var error: HTTPError? { + switch result { + case .success: + return nil + + case let .failure(error): + return error + } + } + + public var isSuccess: Bool { + switch result { + case .success: + return true + + case .failure: + return false + } + } + + public var isFailure: Bool { + !isSuccess + } + + // MARK: - CustomStringConvertible + + public var description: String { + switch result { + case .success: + return "\(type(of: self)).success" + + case let .failure(error): + return "\(type(of: self)).failure(\(error))" + } + } + + // MARK: - Initializers + + public init( + _ result: Result, + statusCode: HTTPStatusCode? = nil, + headers: [HTTPHeader]? = nil + ) { + self.result = result + self.statusCode = statusCode + self.headers = headers + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPRoute.swift b/Sources/FigmaGenTools/HTTPService/HTTPRoute.swift new file mode 100644 index 0000000..1a8a330 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPRoute.swift @@ -0,0 +1,80 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct HTTPRoute: CustomStringConvertible { + + // MARK: - Instance Properties + + public let method: HTTPMethod + public let url: URL + public let headers: [HTTPHeader] + + public let queryParameters: Encodable? + public let queryEncoder: HTTPQueryEncoder + + public let bodyParameters: Encodable? + public let bodyEncoder: HTTPBodyEncoder + + // MARK: - CustomStringConvertible + + public var description: String { + "\(type(of: self)).\(method)(\(url.absoluteString))" + } + + // MARK: - Initializers + + public init( + method: HTTPMethod, + url: URL, + headers: [HTTPHeader] = [], + queryParameters: Encodable? = nil, + queryEncoder: HTTPQueryEncoder = HTTPQueryURLEncoder.default, + bodyParameters: Encodable? = nil, + bodyEncoder: HTTPBodyEncoder = HTTPBodyJSONEncoder.default + ) { + self.method = method + self.url = url + self.headers = headers + + self.queryParameters = queryParameters + self.queryEncoder = queryEncoder + + self.bodyParameters = bodyParameters + self.bodyEncoder = bodyEncoder + } + + // MARK: - Instance Methods + + public func asRequest() throws -> URLRequest { + var request: URLRequest + + if let queryParameters { + let encodedURL = try queryEncoder.encode( + url: url, + parameters: HTTPAnyEncodable(queryParameters) + ) + + request = URLRequest(url: encodedURL) + } else { + request = URLRequest(url: url) + } + + request.httpMethod = method.rawValue + + for header in headers { + request.setValue(header.value, forHTTPHeaderField: header.name) + } + + if let bodyParameters { + request = try bodyEncoder.encode( + request: request, + parameters: HTTPAnyEncodable(bodyParameters) + ) + } + + return request + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPService.swift b/Sources/FigmaGenTools/HTTPService/HTTPService.swift new file mode 100644 index 0000000..1ac30f4 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPService.swift @@ -0,0 +1,68 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public final class HTTPService { + + // MARK: - Instance Properties + + private let session: URLSession + + // MARK: - + + public var sessionConfiguration: URLSessionConfiguration { + session.configuration + } + + public var activityIndicator: HTTPActivityIndicator? + + // MARK: - Initializers + + public init( + sessionConfiguration: URLSessionConfiguration = .httpServiceDefault, + activityIndicator: HTTPActivityIndicator? = nil + ) { + self.session = URLSession(configuration: sessionConfiguration) + self.activityIndicator = activityIndicator + } + + // MARK: - Instance Methods + + public func request(route: HTTPRoute) -> HTTPTask { + DispatchQueue.main.async { + self.activityIndicator?.incrementActivityCount() + } + + return HTTPServiceTask(session: session, route: route) + .launch() + .response { _ in + DispatchQueue.main.async { + self.activityIndicator?.decrementActivityCount() + } + } + } +} + +extension URLSessionConfiguration { + + // MARK: - Type Properties + + public static var httpServiceDefault: URLSessionConfiguration { + let configuration = URLSessionConfiguration.default + + let headers = [ + HTTPHeader.defaultAcceptLanguage(), + HTTPHeader.defaultAcceptEncoding(), + HTTPHeader.defaultUserAgent() + ] + + let rawHeaders = Dictionary(uniqueKeysWithValues: headers.map { ($0.name, $0.value) }) + + configuration.httpAdditionalHeaders = rawHeaders + configuration.timeoutIntervalForRequest = 45 + + return configuration + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPServiceTask.swift b/Sources/FigmaGenTools/HTTPService/HTTPServiceTask.swift new file mode 100644 index 0000000..071552c --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPServiceTask.swift @@ -0,0 +1,226 @@ +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public final class HTTPServiceTask { + + // MARK: - Instance Properties + + private var responseQueue: OperationQueue + + // MARK: - + + internal var sessionTask: SessionTask? + internal let session: URLSession + + // MARK: - + + public let route: HTTPRoute + + public private(set) var response: HTTPResponse? + + // MARK: - Initializers + + internal init(session: URLSession, route: HTTPRoute) { + self.session = session + self.route = route + + responseQueue = OperationQueue() + + responseQueue.maxConcurrentOperationCount = 1 + responseQueue.qualityOfService = .userInitiated + responseQueue.isSuspended = true + } + + // MARK: - Instance Methods + + internal func addResponseOperation(_ operation: @escaping (_ response: HTTPResponse) -> Void) { + responseQueue.addOperation { + operation(self.response ?? HTTPResponse(.failure(HTTPError(code: .unknown)))) + } + } + + internal func handleResponse(_ response: HTTPResponse) { + self.response = response + + responseQueue.isSuspended = false + } + + internal func handleResponse(_ response: URLResponse?, data: Data?, error: Error?) { + var statusCode: HTTPStatusCode? + var headers: [HTTPHeader]? + + if let response = response as? HTTPURLResponse { + statusCode = HTTPStatusCode(rawValue: response.statusCode) + + headers = response.allHeaderFields.compactMap { name, value in + guard let name = name as? String, let value = value as? String else { + return nil + } + + return HTTPHeader(name: name, value: value) + } + } + + let result: Result + + switch error { + case let error as URLError: + result = .failure(HTTPError(urlError: error, data: data)) + + case let error?: + result = .failure(HTTPError(code: .unknown, reason: error, data: data)) + + case nil: + if let error = HTTPError(statusCode: statusCode ?? 0, data: data) { + result = .failure(error) + } else { + result = .success(data) + } + } + + handleResponse(HTTPResponse(result, statusCode: statusCode, headers: headers)) + } + + internal func serializeResponse( + _ response: HTTPResponse, + serializer: Serializer + ) -> HTTPResponse { + let result: Result = response.result.flatMap { data in + do { + let statusCode = response.statusCode ?? 0 + let object: Serializer.SerializedObject + + if let data, !data.isEmpty { + object = try serializer.serialize( + data: data, + statusCode: statusCode, + method: self.route.method + ) + } else { + object = try serializer.serializeEmptyResponse( + statusCode: statusCode, + method: self.route.method + ) + } + + return .success(object) + } catch { + return .failure(HTTPError(code: .badResponse, reason: error, data: data)) + } + } + + return HTTPResponse(result, statusCode: response.statusCode, headers: response.headers) + } + + // MARK: - + + @discardableResult + public func response( + on queue: DispatchQueue, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + addResponseOperation { response in + queue.async { + completion(response) + } + } + + return self + } + + @discardableResult + public func response( + on queue: DispatchQueue, + serializer: Serializer, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + addResponseOperation { response in + let serializedResponse = self.serializeResponse(response, serializer: serializer) + + queue.async { + completion(serializedResponse) + } + } + + return self + } + + @discardableResult + public func responseData( + on queue: DispatchQueue, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + response( + on: queue, + serializer: HTTPDataResponseSerializer(), + completion: completion + ) + } + + @discardableResult + public func responseString( + on queue: DispatchQueue, + encoding: String.Encoding, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + response( + on: queue, + serializer: HTTPStringResponseSerializer(encoding: encoding), + completion: completion + ) + } + + @discardableResult + public func responseJSON( + on queue: DispatchQueue, + options: JSONSerialization.ReadingOptions, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + response( + on: queue, + serializer: HTTPJSONResponseSerializer(options: options), + completion: completion + ) + } + + @discardableResult + public func responseDecodable( + type: T.Type, + on queue: DispatchQueue, + decoder: HTTPResponseDecoder, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + response( + on: queue, + serializer: HTTPDecodableResponseSerializer(decoder: decoder), + completion: completion + ) + } + + public func cancel() { + sessionTask?.cancel() + } +} + +extension HTTPServiceTask: HTTPTask where SessionTask == URLSessionDataTask { + + // MARK: - Instance Methods + + @discardableResult + internal func launch() -> Self { + do { + sessionTask = session.dataTask(with: try route.asRequest()) { data, response, error in + self.handleResponse(response, data: data, error: error) + } + + sessionTask?.resume() + } catch { + handleResponse(HTTPResponse(.failure(HTTPError(code: .badRequest, reason: error)))) + } + + return self + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPStatusCode.swift b/Sources/FigmaGenTools/HTTPService/HTTPStatusCode.swift new file mode 100644 index 0000000..1bc50e3 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPStatusCode.swift @@ -0,0 +1,56 @@ +import Foundation + +public struct HTTPStatusCode: HTTPErrorStringConvertible, Hashable, ExpressibleByIntegerLiteral { + + // MARK: - Nested Types + + public enum State { + + // MARK: - Enumeration Cases + + case unknown + case information + case success + case redirection + case failure + } + + // MARK: - Instance Properties + + public let rawValue: Int + + public var state: State { + switch rawValue { + case 100...199: + return .information + + case 200...299: + return .success + + case 300...399: + return .redirection + + case 400...599: + return .failure + + default: + return .unknown + } + } + + // MARK: - HTTPErrorStringConvertible + + public var httpErrorDescription: String { + "\(type(of: self)).\(rawValue)" + } + + // MARK: - Initializers + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + public init(integerLiteral: Int) { + self.init(rawValue: integerLiteral) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPTask.swift b/Sources/FigmaGenTools/HTTPService/HTTPTask.swift new file mode 100644 index 0000000..5d1c0fa --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPTask.swift @@ -0,0 +1,110 @@ +import Foundation + +public protocol HTTPTask: AnyObject { + + // MARK: - Instance Properties + + var route: HTTPRoute { get } + var response: HTTPResponse? { get } + + var isFinished: Bool { get } + + // MARK: - Instance Methods + + @discardableResult + func response( + on queue: DispatchQueue, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self + + @discardableResult + func response( + on queue: DispatchQueue, + serializer: Serializer, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self + + @discardableResult + func responseData( + on queue: DispatchQueue, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self + + @discardableResult + func responseString( + on queue: DispatchQueue, + encoding: String.Encoding, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self + + @discardableResult + func responseJSON( + on queue: DispatchQueue, + options: JSONSerialization.ReadingOptions, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self + + @discardableResult + func responseDecodable( + type: T.Type, + on queue: DispatchQueue, + decoder: HTTPResponseDecoder, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self + + func cancel() +} + +extension HTTPTask { + + // MARK: - Instance Properties + + public var isFinished: Bool { + response != nil + } + + // MARK: - Instance Methods + + @discardableResult + public func response(completion: @escaping (_ response: HTTPResponse) -> Void) -> Self { + response(on: .main, completion: completion) + } + + @discardableResult + public func response( + serializer: Serializer, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + response(on: .main, serializer: serializer, completion: completion) + } + + @discardableResult + public func responseData(completion: @escaping (_ response: HTTPResponse) -> Void) -> Self { + responseData(on: .main, completion: completion) + } + + @discardableResult + public func responseString( + encoding: String.Encoding = .utf8, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + responseString(on: .main, encoding: encoding, completion: completion) + } + + @discardableResult + public func responseJSON( + options: JSONSerialization.ReadingOptions = .allowFragments, + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + responseJSON(on: .main, options: options, completion: completion) + } + + @discardableResult + public func responseDecodable( + type: T.Type, + decoder: HTTPResponseDecoder = JSONDecoder(), + completion: @escaping (_ response: HTTPResponse) -> Void + ) -> Self { + responseDecodable(type: type, on: .main, decoder: decoder, completion: completion) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/HTTPUIActivityIndicator.swift b/Sources/FigmaGenTools/HTTPService/HTTPUIActivityIndicator.swift new file mode 100644 index 0000000..d928ab5 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/HTTPUIActivityIndicator.swift @@ -0,0 +1,40 @@ +#if canImport(UIKit) +import UIKit + +public final class HTTPUIActivityIndicator: WebActivityIndicator { + + // MARK: - Type Properties + + public static let shared = WebUIActivityIndicator() + + // MARK: - Instance Properties + + public private(set) var activityCount = 0 { + didSet { + UIApplication.shared.isNetworkActivityIndicatorVisible = activityCount > 0 + } + } + + // MARK: - Initializers + + private init() { } + + // MARK: - Instance Methods + + public func resetActivityCount() { + activityCount = 0 + } + + // MARK: - WebActivityIndicator + + public func incrementActivityCount() { + activityCount += 1 + } + + public func decrementActivityCount() { + if activityCount > 0 { + activityCount -= 1 + } + } +} +#endif diff --git a/Sources/FigmaGenTools/HTTPService/QueryEncoders/HTTPQueryEncoder.swift b/Sources/FigmaGenTools/HTTPService/QueryEncoders/HTTPQueryEncoder.swift new file mode 100644 index 0000000..8a13df9 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/QueryEncoders/HTTPQueryEncoder.swift @@ -0,0 +1,8 @@ +import Foundation + +public protocol HTTPQueryEncoder { + + // MARK: - Instance Methods + + func encode(url: URL, parameters: T) throws -> URL +} diff --git a/Sources/FigmaGenTools/HTTPService/QueryEncoders/HTTPQueryURLEncoder.swift b/Sources/FigmaGenTools/HTTPService/QueryEncoders/HTTPQueryURLEncoder.swift new file mode 100644 index 0000000..392aba4 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/QueryEncoders/HTTPQueryURLEncoder.swift @@ -0,0 +1,48 @@ +import Foundation + +public final class HTTPQueryURLEncoder: HTTPQueryEncoder { + + // MARK: - Type Properties + + public static let `default` = HTTPQueryURLEncoder(urlEncoder: URLEncoder()) + + // MARK: - Instance Properties + + public let urlEncoder: URLEncoder + + // MARK: - Initializers + + public init(urlEncoder: URLEncoder) { + self.urlEncoder = urlEncoder + } + + // MARK: - Instance Methods + + public func encode(url: URL, parameters: T) throws -> URL { + guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw MessageError("Invalid URL") + } + + let query = [ + urlComponents.percentEncodedQuery, + try urlEncoder.encodeToQuery(parameters) + ] + + urlComponents.percentEncodedQuery = query + .compactMap { $0 } + .joined(separator: .querySeparator) + + guard let url = urlComponents.url else { + throw MessageError("Invalid query parameters") + } + + return url + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let querySeparator = "&" +} diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPDataResponseSerializer.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPDataResponseSerializer.swift new file mode 100644 index 0000000..a52b63f --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPDataResponseSerializer.swift @@ -0,0 +1,34 @@ +import Foundation + +public final class HTTPDataResponseSerializer: HTTPResponseSerializer { + + // MARK: - Instance Properties + + public let emptyResponseStatusCodes: Set + public let emptyResponseMethods: Set + + // MARK: - Initializers + + public init( + emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes, + emptyResponseMethods: Set = defaultEmptyResponseMethods + ) { + self.emptyResponseStatusCodes = emptyResponseStatusCodes + self.emptyResponseMethods = emptyResponseMethods + } + + // MARK: - Instance Methods + + public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> Data { + data + } +} + +extension Data: HTTPEmptyResponse { + + // MARK: - Type Methods + + public static func emptyResponseInstance() -> Data { + Data() + } +} diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPDecodableResponseSerializer.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPDecodableResponseSerializer.swift new file mode 100644 index 0000000..8fe945f --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPDecodableResponseSerializer.swift @@ -0,0 +1,32 @@ +import Foundation + +public final class HTTPDecodableResponseSerializer: HTTPResponseSerializer { + + // MARK: - Instance Properties + + public let decoder: HTTPResponseDecoder + + // MARK: - HTTPResponseSerializer + + public let emptyResponseStatusCodes: Set + public let emptyResponseMethods: Set + + // MARK: - Initializers + + public init( + decoder: HTTPResponseDecoder, + emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes, + emptyResponseMethods: Set = defaultEmptyResponseMethods + ) { + self.decoder = decoder + + self.emptyResponseStatusCodes = emptyResponseStatusCodes + self.emptyResponseMethods = emptyResponseMethods + } + + // MARK: - Instance Methods + + public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> T { + try decoder.decode(T.self, from: data) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPEmptyResponse.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPEmptyResponse.swift new file mode 100644 index 0000000..95d500c --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPEmptyResponse.swift @@ -0,0 +1,8 @@ +import Foundation + +public protocol HTTPEmptyResponse { + + // MARK: - Type Methods + + static func emptyResponseInstance() -> Self +} diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPJSONResponseSerializer.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPJSONResponseSerializer.swift new file mode 100644 index 0000000..d94d6f1 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPJSONResponseSerializer.swift @@ -0,0 +1,31 @@ +import Foundation + +public final class HTTPJSONResponseSerializer: HTTPResponseSerializer { + + // MARK: - Instance Properties + + public let options: JSONSerialization.ReadingOptions + + // MARK: - HTTPResponseSerializer + + public let emptyResponseStatusCodes: Set + public let emptyResponseMethods: Set + + // MARK: - Initializers + + public init( + options: JSONSerialization.ReadingOptions, + emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes, + emptyResponseMethods: Set = defaultEmptyResponseMethods + ) { + self.options = options + self.emptyResponseStatusCodes = emptyResponseStatusCodes + self.emptyResponseMethods = emptyResponseMethods + } + + // MARK: - Instance Methods + + public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> Any { + try JSONSerialization.jsonObject(with: data, options: options) + } +} diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPResponseDecoder.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPResponseDecoder.swift new file mode 100644 index 0000000..0a6f7e8 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPResponseDecoder.swift @@ -0,0 +1,12 @@ +import Foundation + +public protocol HTTPResponseDecoder { + + // MARK: - Instance Methods + + func decode(_ type: T.Type, from data: Data) throws -> T +} + +// MARK: - + +extension JSONDecoder: HTTPResponseDecoder { } diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPResponseSerializer.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPResponseSerializer.swift new file mode 100644 index 0000000..51fed09 --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPResponseSerializer.swift @@ -0,0 +1,49 @@ +import Foundation + +public protocol HTTPResponseSerializer { + + // MARK: - Nested Types + + associatedtype SerializedObject + + // MARK: - Instance Properties + + var emptyResponseStatusCodes: Set { get } + var emptyResponseMethods: Set { get } + + // MARK: - Instance Methods + + func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> SerializedObject + func serializeEmptyResponse(statusCode: HTTPStatusCode, method: HTTPMethod) throws -> SerializedObject +} + +extension HTTPResponseSerializer { + + // MARK: - Type Properties + + public static var defaultEmptyResponseStatusCodes: Set { + [204, 205] + } + + public static var defaultEmptyResponseMethods: Set { + [.head] + } + + // MARK: - Instance Methods + + public func serializeEmptyResponse(statusCode: HTTPStatusCode, method: HTTPMethod) throws -> SerializedObject { + guard emptyResponseStatusCodes.contains(statusCode) || emptyResponseMethods.contains(method) else { + throw MessageError("Empty response is unacceptable") + } + + guard let emptyResponseType = SerializedObject.self as? HTTPEmptyResponse.Type else { + throw MessageError("\(SerializedObject.self) cannot have an empty instance") + } + + guard let emptyResponseInstance = emptyResponseType.emptyResponseInstance() as? SerializedObject else { + throw MessageError("Empty instance of \(SerializedObject.self) has an invalid type") + } + + return emptyResponseInstance + } +} diff --git a/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPStringResponseSerializer.swift b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPStringResponseSerializer.swift new file mode 100644 index 0000000..9a1d40a --- /dev/null +++ b/Sources/FigmaGenTools/HTTPService/ResponseSerializers/HTTPStringResponseSerializer.swift @@ -0,0 +1,44 @@ +import Foundation + +public final class HTTPStringResponseSerializer: HTTPResponseSerializer { + + // MARK: - Instance Properties + + public let encoding: String.Encoding + + // MARK: - HTTPResponseSerializer + + public let emptyResponseStatusCodes: Set + public let emptyResponseMethods: Set + + // MARK: - Initializers + + public init( + encoding: String.Encoding, + emptyResponseStatusCodes: Set = defaultEmptyResponseStatusCodes, + emptyResponseMethods: Set = defaultEmptyResponseMethods + ) { + self.encoding = encoding + self.emptyResponseStatusCodes = emptyResponseStatusCodes + self.emptyResponseMethods = emptyResponseMethods + } + + // MARK: - Instance Methods + + public func serialize(data: Data, statusCode: HTTPStatusCode, method: HTTPMethod) throws -> String { + guard let string = String(data: data, encoding: encoding) else { + throw MessageError("String serialization failed with encoding: \(encoding)") + } + + return string + } +} + +extension String: HTTPEmptyResponse { + + // MARK: - Type Methods + + public static func emptyResponseInstance() -> String { + "" + } +} diff --git a/Sources/FigmaGenTools/Shared/AnyCodable.swift b/Sources/FigmaGenTools/Shared/AnyCodable.swift new file mode 100644 index 0000000..b83e1e2 --- /dev/null +++ b/Sources/FigmaGenTools/Shared/AnyCodable.swift @@ -0,0 +1,268 @@ +import Foundation + +public struct AnyCodable: Codable { + + // MARK: - Instance Properties + + public let value: Any + + // MARK: - Initializers + + public init(_ value: T?) { + self.value = value as Any + } + + // swiftlint:disable:next function_body_length + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + self.init(nil as Any?) + } else { + let string = try? container.decode(String.self) + + if let bool = try? container.decode(Bool.self) { + if let string, string != String(describing: bool) { + self.init(string) + } else { + self.init(bool) + } + } else if let int = try? container.decode(Int.self) { + if let string, string != String(describing: int) { + self.init(string) + } else { + self.init(int) + } + } else if let uint = try? container.decode(UInt.self) { + if let string, string != String(describing: uint) { + self.init(string) + } else { + self.init(uint) + } + } else if let double = try? container.decode(Double.self) { + if let string, string != String(describing: double) { + self.init(string) + } else { + self.init(double) + } + } else if let string { + self.init(string) + } else if let array = try? container.decode([AnyCodable].self) { + self.init(array.map { $0.value }) + } else if let dictionary = try? container.decode([String: AnyCodable].self) { + self.init(dictionary.mapValues { $0.value }) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "AnyCodable value cannot be decoded" + ) + } + } + } + + // MARK: - Instance Methods + + public func encode(to encoder: Encoder) throws { + // swiftlint:disable:previous function_body_length + + var container = encoder.singleValueContainer() + + switch value { + case nil as Any?: + try container.encodeNil() + + case let bool as Bool: + try container.encode(bool) + + case let int as Int: + try container.encode(int) + + case let int8 as Int8: + try container.encode(int8) + + case let int16 as Int16: + try container.encode(int16) + + case let int32 as Int32: + try container.encode(int32) + + case let int64 as Int64: + try container.encode(int64) + + case let uint as UInt: + try container.encode(uint) + + case let uint8 as UInt8: + try container.encode(uint8) + + case let uint16 as UInt16: + try container.encode(uint16) + + case let uint32 as UInt32: + try container.encode(uint32) + + case let uint64 as UInt64: + try container.encode(uint64) + + case let float as Float: + try container.encode(float) + + case let double as Double: + try container.encode(double) + + case let string as String: + try container.encode(string) + + case let date as Date: + try container.encode(date) + + case let url as URL: + try container.encode(url.absoluteString) + + case let array as [Any?]: + try container.encode(array.map(Self.init)) + + case let dictionary as [String: Any?]: + try container.encode(dictionary.mapValues(Self.init)) + + default: + let context = EncodingError.Context( + codingPath: container.codingPath, + debugDescription: "AnyCodable value cannot be encoded" + ) + + throw EncodingError.invalidValue(value, context) + } + } +} + +extension AnyCodable: Equatable { + + // MARK: - Type Methods + + public static func == (lhs: AnyCodable, rhs: AnyCodable) -> Bool { + // swiftlint:disable:previous function_body_length + + switch (lhs.value, rhs.value) { + case (nil as Any?, nil as Any?): + return true + + case let (lhs as Bool, rhs as Bool): + return lhs == rhs + + case let (lhs as Int, rhs as Int): + return lhs == rhs + + case let (lhs as Int8, rhs as Int8): + return lhs == rhs + + case let (lhs as Int16, rhs as Int16): + return lhs == rhs + + case let (lhs as Int32, rhs as Int32): + return lhs == rhs + + case let (lhs as Int64, rhs as Int64): + return lhs == rhs + + case let (lhs as UInt, rhs as UInt): + return lhs == rhs + + case let (lhs as UInt8, rhs as UInt8): + return lhs == rhs + + case let (lhs as UInt16, rhs as UInt16): + return lhs == rhs + + case let (lhs as UInt32, rhs as UInt32): + return lhs == rhs + + case let (lhs as UInt64, rhs as UInt64): + return lhs == rhs + + case let (lhs as Float, rhs as Float): + return lhs == rhs + + case let (lhs as Double, rhs as Double): + return lhs == rhs + + case let (lhs as String, rhs as String): + return lhs == rhs + + case let (lhs as Date, rhs as Date): + return lhs == rhs + + case let (lhs as URL, rhs as URL): + return lhs == rhs + + case let (lhs as [String: Any], rhs as [String: Any]): + return lhs.mapValues(AnyCodable.init) == rhs.mapValues(AnyCodable.init) + + case let (lhs as [Any], rhs as [Any]): + return lhs.map(AnyCodable.init) == rhs.map(AnyCodable.init) + + default: + return false + } + } +} + +extension AnyCodable: Hashable { + + // MARK: - Instance Methods + + public func hash(into hasher: inout Hasher) { + switch value { + case let value as Bool: + hasher.combine(value) + + case let value as Int: + hasher.combine(value) + + case let value as Int8: + hasher.combine(value) + + case let value as Int16: + hasher.combine(value) + + case let value as Int32: + hasher.combine(value) + + case let value as Int64: + hasher.combine(value) + + case let value as UInt: + hasher.combine(value) + + case let value as UInt8: + hasher.combine(value) + + case let value as UInt16: + hasher.combine(value) + + case let value as UInt32: + hasher.combine(value) + + case let value as UInt64: + hasher.combine(value) + + case let value as Float: + hasher.combine(value) + + case let value as Double: + hasher.combine(value) + + case let value as String: + hasher.combine(value) + + case let value as [String: AnyCodable]: + hasher.combine(value) + + case let value as [AnyCodable]: + hasher.combine(value) + + default: + break + } + } +} diff --git a/Sources/FigmaGenTools/Shared/AnyCodingKey.swift b/Sources/FigmaGenTools/Shared/AnyCodingKey.swift new file mode 100644 index 0000000..6e2cfd7 --- /dev/null +++ b/Sources/FigmaGenTools/Shared/AnyCodingKey.swift @@ -0,0 +1,29 @@ +import Foundation + +public struct AnyCodingKey: CodingKey { + + // MARK: - Instance Properties + + public let stringValue: String + public let intValue: Int? + + // MARK: - Initializers + + public init(_ stringValue: String) { + self.stringValue = stringValue + self.intValue = nil + } + + public init(_ intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + public init?(stringValue: String) { + self.init(stringValue) + } + + public init?(intValue: Int) { + self.init(intValue) + } +} diff --git a/Sources/FigmaGenTools/Shared/Cache.swift b/Sources/FigmaGenTools/Shared/Cache.swift new file mode 100644 index 0000000..12abb5c --- /dev/null +++ b/Sources/FigmaGenTools/Shared/Cache.swift @@ -0,0 +1,66 @@ +import Foundation + +public final class Cache { + + // MARK: - Instance Properties + + private let wrapped = NSCache, CacheValue>() + + // MARK: - Initializers + + public init() { } + + // MARK: - Instance Methods + + public func value(forKey key: Key) -> Value? { + wrapped.object(forKey: CacheKey(key))?.value + } + + public func setValue(_ value: Value, forKey key: Key) { + wrapped.setObject(CacheValue(value), forKey: CacheKey(key)) + } + + public func removeValue(forKey key: Key) { + wrapped.removeObject(forKey: CacheKey(key)) + } +} + +private final class CacheKey: NSObject { + + // MARK: - Instance Properties + + let key: T + + override var hash: Int { + key.hashValue + } + + // MARK: - Initializers + + init(_ key: T) { + self.key = key + } + + // MARK: - Instance Methods + + override func isEqual(_ object: Any?) -> Bool { + guard let object = object as? CacheKey else { + return false + } + + return key == object.key + } +} + +private final class CacheValue { + + // MARK: - Instance Properties + + let value: T + + // MARK: - Initializers + + init(_ value: T) { + self.value = value + } +} diff --git a/Sources/FigmaGenTools/Shared/Indirect.swift b/Sources/FigmaGenTools/Shared/Indirect.swift new file mode 100644 index 0000000..6fd0048 --- /dev/null +++ b/Sources/FigmaGenTools/Shared/Indirect.swift @@ -0,0 +1,66 @@ +import Foundation + +public struct Indirect { + + // MARK: - Nested Types + + fileprivate final class Wrapper { + + // MARK: - Instance Properties + + var value: Value + + // MARK: - Initializers + + init(_ value: Value) { + self.value = value + } + } + + // MARK: - Instance Properties + + private var wrapper: Wrapper + + public var value: Value { + get { wrapper.value } + set { + if isKnownUniquelyReferenced(&wrapper) { + wrapper.value = newValue + } else { + wrapper = Wrapper(newValue) + } + } + } + + // MARK: - Initializers + + public init(_ value: Value) { + self.wrapper = Wrapper(value) + } +} + +// MARK: - Equatable + +extension Indirect.Wrapper: Equatable where Value: Equatable { + + // MARK: - Type Methods + + fileprivate static func == (lhs: Indirect.Wrapper, rhs: Indirect.Wrapper) -> Bool { + lhs.value == rhs.value + } +} + +extension Indirect: Equatable where Value: Equatable { } + +// MARK: - Hashable + +extension Indirect.Wrapper: Hashable where Value: Hashable { + + // MARK: - Instance Methods + + fileprivate func hash(into hasher: inout Hasher) { + hasher.combine(value) + } +} + +extension Indirect: Hashable where Value: Hashable { } diff --git a/Sources/FigmaGenTools/Shared/MessageError.swift b/Sources/FigmaGenTools/Shared/MessageError.swift new file mode 100644 index 0000000..23295ad --- /dev/null +++ b/Sources/FigmaGenTools/Shared/MessageError.swift @@ -0,0 +1,22 @@ +import Foundation + +public struct MessageError: Error, CustomStringConvertible, Hashable { + + // MARK: - Instance Properties + + public let message: String + + public var localizedDescription: String { + message + } + + public var description: String { + "\(type(of: self))(\"\(message)\")" + } + + // MARK: - Initializers + + public init(_ message: String) { + self.message = message + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLArrayEncodingStrategy.swift b/Sources/FigmaGenTools/URLEncoder/URLArrayEncodingStrategy.swift new file mode 100644 index 0000000..a56a408 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLArrayEncodingStrategy.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum URLArrayEncodingStrategy { + + // MARK: - Enumeration Case + + case brackets + case noBrackets +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLBoolEncodingStrategy.swift b/Sources/FigmaGenTools/URLEncoder/URLBoolEncodingStrategy.swift new file mode 100644 index 0000000..5d578a4 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLBoolEncodingStrategy.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum URLBoolEncodingStrategy { + + // MARK: - Enumeration Case + + case numeric + case literal +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLDateEncodingStrategy.swift b/Sources/FigmaGenTools/URLEncoder/URLDateEncodingStrategy.swift new file mode 100644 index 0000000..6a4bafb --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLDateEncodingStrategy.swift @@ -0,0 +1,17 @@ +import Foundation + +public enum URLDateEncodingStrategy { + + // MARK: - Enumeration Cases + + case deferredToDate + + case millisecondsSince1970 + case secondsSince1970 + + @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 + + case formatted(DateFormatter) + case custom((Date) throws -> String) +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedForm.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedForm.swift new file mode 100644 index 0000000..4680d85 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedForm.swift @@ -0,0 +1,3 @@ +import Foundation + +internal typealias URLEncodedForm = [String: URLEncodedFormComponent] diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormComponent.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormComponent.swift new file mode 100644 index 0000000..ca1a038 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormComponent.swift @@ -0,0 +1,32 @@ +import Foundation + +internal enum URLEncodedFormComponent { + + // MARK: - Enumeration Cases + + case string(String) + case array([Self]) + case dictionary([String: Self]) + + // MARK: - Instance Properties + + internal var array: [Self]? { + switch self { + case let .array(array): + return array + + default: + return nil + } + } + + internal var dictionary: [String: Self]? { + switch self { + case let .dictionary(object): + return object + + default: + return nil + } + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormContext.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormContext.swift new file mode 100644 index 0000000..758b7be --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormContext.swift @@ -0,0 +1,14 @@ +import Foundation + +internal final class URLEncodedFormContext { + + // MARK: - Instance Properties + + internal var component: URLEncodedFormComponent + + // MARK: - Initializers + + internal init(component: URLEncodedFormComponent) { + self.component = component + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormEncoder.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormEncoder.swift new file mode 100644 index 0000000..b370af8 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormEncoder.swift @@ -0,0 +1,63 @@ +import Foundation + +internal final class URLEncodedFormEncoder: Encoder { + + // MARK: - Instance Properties + + internal let context: URLEncodedFormContext + internal let boolEncodingStrategy: URLBoolEncodingStrategy + internal let dateEncodingStrategy: URLDateEncodingStrategy + + // MARK: - Encoder + + internal var codingPath: [CodingKey] + + internal var userInfo: [CodingUserInfoKey: Any] { + [:] + } + + // MARK: - Initializers + + internal init( + context: URLEncodedFormContext, + boolEncodingStrategy: URLBoolEncodingStrategy, + dateEncodingStrategy: URLDateEncodingStrategy, + codingPath: [CodingKey] = [] + ) { + self.context = context + self.boolEncodingStrategy = boolEncodingStrategy + self.dateEncodingStrategy = dateEncodingStrategy + self.codingPath = codingPath + } + + // MARK: - Instance Methods + + internal func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key: CodingKey { + let container = URLEncodedFormKeyedEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + + return KeyedEncodingContainer(container) + } + + internal func unkeyedContainer() -> UnkeyedEncodingContainer { + URLEncodedFormUnkeyedEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + } + + internal func singleValueContainer() -> SingleValueEncodingContainer { + URLEncodedFormSingleValueEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormKeyedEncodingContainer.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormKeyedEncodingContainer.swift new file mode 100644 index 0000000..f08a21c --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormKeyedEncodingContainer.swift @@ -0,0 +1,91 @@ +import Foundation + +internal final class URLEncodedFormKeyedEncodingContainer: KeyedEncodingContainerProtocol { + + // MARK: - Instance Properties + + internal let context: URLEncodedFormContext + internal let boolEncodingStrategy: URLBoolEncodingStrategy + internal let dateEncodingStrategy: URLDateEncodingStrategy + + // MARK: - KeyedEncodingContainerProtocol + + internal var codingPath: [CodingKey] + + // MARK: - Initializers + + internal init( + context: URLEncodedFormContext, + boolEncodingStrategy: URLBoolEncodingStrategy, + dateEncodingStrategy: URLDateEncodingStrategy, + codingPath: [CodingKey] + ) { + self.context = context + self.boolEncodingStrategy = boolEncodingStrategy + self.dateEncodingStrategy = dateEncodingStrategy + self.codingPath = codingPath + } + + // MARK: - Instance Methods + + internal func encodeNil(forKey key: Key) throws { + let errorContext = EncodingError.Context( + codingPath: codingPath, + debugDescription: "Nil values cannot be encoded in URL" + ) + + throw EncodingError.invalidValue("nil", errorContext) + } + + internal func encode(_ value: T, forKey key: Key) throws { + let container = URLEncodedFormSingleValueEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(key) + ) + + try container.encode(value) + } + + internal func nestedContainer( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let container = URLEncodedFormKeyedEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(key) + ) + + return KeyedEncodingContainer(container) + } + + internal func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + URLEncodedFormUnkeyedEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(key) + ) + } + + internal func superEncoder() -> Encoder { + URLEncodedFormEncoder( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + } + + internal func superEncoder(forKey key: Key) -> Encoder { + URLEncodedFormEncoder( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(key) + ) + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormSerializer.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormSerializer.swift new file mode 100644 index 0000000..b644cac --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormSerializer.swift @@ -0,0 +1,108 @@ +import Foundation + +internal final class URLEncodedFormSerializer { + + // MARK: - Instance Properties + + internal let arrayEncodingStrategy: URLArrayEncodingStrategy + internal let spaceEncodingStrategy: URLSpaceEncodingStrategy + + // MARK: - Initializers + + internal init( + arrayEncodingStrategy: URLArrayEncodingStrategy, + spaceEncodingStrategy: URLSpaceEncodingStrategy + ) { + self.arrayEncodingStrategy = arrayEncodingStrategy + self.spaceEncodingStrategy = spaceEncodingStrategy + } + + // MARK: - Instance Methods + + private func escapeString(_ string: String) -> String { + var allowedCharacters = CharacterSet.urlQueryAllowed + + allowedCharacters.remove(charactersIn: .urlDelimiters) + allowedCharacters.insert(charactersIn: .urlUnescapedSpace) + + let escapedString = string.addingPercentEncoding(withAllowedCharacters: allowedCharacters) ?? string + + switch spaceEncodingStrategy { + case .percentEscaped: + return escapedString.replacingOccurrences( + of: String.urlUnescapedSpace, + with: String.urlPercentEscapedSpace + ) + + case .plusReplaced: + return escapedString.replacingOccurrences( + of: String.urlUnescapedSpace, + with: String.urlPlusReplacedSpace + ) + } + } + + private func serializeComponent(_ component: URLEncodedFormComponent, key: String) -> String { + switch component { + case let .string(value): + return .urlStringComponent(key: escapeString(key), value: escapeString(value)) + + case let .array(value): + return value + .map { element in + switch arrayEncodingStrategy { + case .brackets: + return serializeComponent(element, key: .urlBracketsArrayComponentKey(key)) + + case .noBrackets: + return serializeComponent(element, key: .urlNoBracketsArrayComponentKey(key)) + } + } + .joined(separator: .urlComponentSeparator) + + case let .dictionary(value): + return value + .map { serializeComponent($0.value, key: .urlDictionaryComponentKey(key, elementKey: $0.key)) } + .joined(separator: .urlComponentSeparator) + } + } + + // MARK: - + + internal func serialize(_ form: URLEncodedForm) -> String { + form + .map { serializeComponent($0.value, key: $0.key) } + .joined(separator: .urlComponentSeparator) + } +} + +extension String { + + // MARK: - Type Properties + + fileprivate static let urlDelimiters = ":#[]@!$&'()*+,;=" + fileprivate static let urlUnescapedSpace = " " + + fileprivate static let urlPercentEscapedSpace = "%20" + fileprivate static let urlPlusReplacedSpace = "+" + + fileprivate static let urlComponentSeparator = "&" + + // MARK: - Type Methods + + fileprivate static func urlStringComponent(key: String, value: String) -> String { + "\(key)=\(value)" + } + + fileprivate static func urlBracketsArrayComponentKey(_ key: String) -> String { + "\(key)[]" + } + + fileprivate static func urlNoBracketsArrayComponentKey(_ key: String) -> String { + key + } + + fileprivate static func urlDictionaryComponentKey(_ key: String, elementKey: String) -> String { + "\(key)[\(elementKey)]" + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormSingleValueEncodingContainer.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormSingleValueEncodingContainer.swift new file mode 100644 index 0000000..7530a9f --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormSingleValueEncodingContainer.swift @@ -0,0 +1,257 @@ +import Foundation + +internal final class URLEncodedFormSingleValueEncodingContainer: SingleValueEncodingContainer { + + // MARK: - Instance Properties + + private var canEncodeNextValue = true + + // MARK: - + + internal let context: URLEncodedFormContext + internal let boolEncodingStrategy: URLBoolEncodingStrategy + internal let dateEncodingStrategy: URLDateEncodingStrategy + + // MARK: - SingleValueEncodingContainer + + internal var codingPath: [CodingKey] + + // MARK: - Initializers + + internal init( + context: URLEncodedFormContext, + boolEncodingStrategy: URLBoolEncodingStrategy, + dateEncodingStrategy: URLDateEncodingStrategy, + codingPath: [CodingKey] + ) { + self.context = context + self.boolEncodingStrategy = boolEncodingStrategy + self.dateEncodingStrategy = dateEncodingStrategy + self.codingPath = codingPath + } + + // MARK: - Instance Methods + + private func markAsEncoded(_ value: Any?) throws { + guard canEncodeNextValue else { + let errorContext = EncodingError.Context( + codingPath: codingPath, + debugDescription: "Single value container has already encoded value" + ) + + throw EncodingError.invalidValue(value as Any, errorContext) + } + + canEncodeNextValue = false + } + + private func updatedComponent( + _ component: URLEncodedFormComponent, + with value: URLEncodedFormComponent, + at path: [CodingKey] + ) -> URLEncodedFormComponent { + guard let pathKey = path.first else { + return value + } + + var child: URLEncodedFormComponent + + switch path.count { + case 1: + child = value + + default: + if let index = pathKey.intValue { + let array = component.array ?? [] + + if index < array.count { + child = updatedComponent(array[index], with: value, at: Array(path[1...])) + } else { + child = updatedComponent(.array([]), with: value, at: Array(path[1...])) + } + } else { + child = updatedComponent( + component.dictionary?[pathKey.stringValue] ?? .dictionary([:]), + with: value, + at: Array(path[1...]) + ) + } + } + + if let childIndex = pathKey.intValue { + guard var array = component.array else { + return .array([child]) + } + + if childIndex < array.count { + array[childIndex] = child + } else { + array.append(child) + } + + return .array(array) + } + + guard var dictionary = component.dictionary else { + return .dictionary([pathKey.stringValue: child]) + } + + dictionary[pathKey.stringValue] = child + + return .dictionary(dictionary) + } + + private func encode(_ value: T, as string: String) throws { + try markAsEncoded(value) + + context.component = updatedComponent(context.component, with: .string(string), at: codingPath) + } + + private func encode(_ date: Date) throws { + switch dateEncodingStrategy { + case .deferredToDate: + try markAsEncoded(date) + + let encoder = URLEncodedFormEncoder( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + + try date.encode(to: encoder) + + case .millisecondsSince1970: + try encode(date, as: String(date.timeIntervalSince1970 * 1000.0)) + + case .secondsSince1970: + try encode(date, as: String(date.timeIntervalSince1970)) + + case .iso8601: + guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + + let formattedDate = ISO8601DateFormatter.string( + from: date, + timeZone: Contants.iso8601TimeZone, + formatOptions: .withInternetDateTime + ) + + try encode(date, as: formattedDate) + + case let .formatted(dateFormatter): + try encode(date, as: dateFormatter.string(from: date)) + + case let .custom(closure): + try encode(date, as: try closure(date)) + } + } + + // MARK: - SingleValueEncodingContainer + + internal func encodeNil() throws { + try markAsEncoded(nil) + + let errorContext = EncodingError.Context( + codingPath: codingPath, + debugDescription: "Nil values cannot be encoded in URL" + ) + + throw EncodingError.invalidValue("nil", errorContext) + } + + internal func encode(_ value: Bool) throws { + switch boolEncodingStrategy { + case .numeric: + try encode(value, as: value ? Contants.numericTrue : Contants.numericFalse) + + case .literal: + try encode(value, as: value ? Contants.literalTrue : Contants.literalFalse) + } + } + + internal func encode(_ value: String) throws { + try encode(value, as: value) + } + + internal func encode(_ value: Double) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: Float) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: Int) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: Int8) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: Int16) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: Int32) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: Int64) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: UInt) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: UInt8) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: UInt16) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: UInt32) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: UInt64) throws { + try encode(value, as: String(value)) + } + + internal func encode(_ value: T) throws { + switch value { + case let date as Date: + try encode(date) + + default: + try markAsEncoded(value) + + let encoder = URLEncodedFormEncoder( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + + try value.encode(to: encoder) + } + } +} + +private enum Contants { + + // MARK: - Type Properties + + static let numericTrue = "1" + static let numericFalse = "0" + + static let literalTrue = "true" + static let literalFalse = "false" + + static let iso8601TimeZone = TimeZone(secondsFromGMT: 0)! +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncodedFormUnkeyedEncodingContainer.swift b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormUnkeyedEncodingContainer.swift new file mode 100644 index 0000000..d1d5c13 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncodedFormUnkeyedEncodingContainer.swift @@ -0,0 +1,98 @@ +import Foundation + +internal final class URLEncodedFormUnkeyedEncodingContainer: UnkeyedEncodingContainer { + + // MARK: - Instance Properties + + internal let context: URLEncodedFormContext + internal let boolEncodingStrategy: URLBoolEncodingStrategy + internal let dateEncodingStrategy: URLDateEncodingStrategy + + // MARK: - UnkeyedEncodingContainer + + internal var codingPath: [CodingKey] + internal var count = 0 + + // MARK: - Initializers + + internal init( + context: URLEncodedFormContext, + boolEncodingStrategy: URLBoolEncodingStrategy, + dateEncodingStrategy: URLDateEncodingStrategy, + codingPath: [CodingKey] + ) { + self.context = context + self.boolEncodingStrategy = boolEncodingStrategy + self.dateEncodingStrategy = dateEncodingStrategy + self.codingPath = codingPath + } + + // MARK: - Instance Methods + + internal func encodeNil() throws { + let errorContext = EncodingError.Context( + codingPath: codingPath, + debugDescription: "Nil values cannot be encoded in URL" + ) + + throw EncodingError.invalidValue("nil", errorContext) + } + + internal func encode(_ value: T) throws { + defer { + count += 1 + } + + let container = URLEncodedFormSingleValueEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(AnyCodingKey(count)) + ) + + try container.encode(value) + } + + internal func nestedContainer( + keyedBy keyType: NestedKey.Type + ) -> KeyedEncodingContainer { + defer { + count += 1 + } + + let container = URLEncodedFormKeyedEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(AnyCodingKey(count)) + ) + + return KeyedEncodingContainer(container) + } + + internal func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + defer { + count += 1 + } + + return URLEncodedFormUnkeyedEncodingContainer( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath.appending(AnyCodingKey(count)) + ) + } + + internal func superEncoder() -> Encoder { + defer { + count += 1 + } + + return URLEncodedFormEncoder( + context: context, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy, + codingPath: codingPath + ) + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLEncoder.swift b/Sources/FigmaGenTools/URLEncoder/URLEncoder.swift new file mode 100644 index 0000000..42d9207 --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLEncoder.swift @@ -0,0 +1,65 @@ +import Foundation + +public final class URLEncoder { + + // MARK: - Type Properties + + public static let `default` = URLEncoder() + + // MARK: - Instance Properties + + public var boolEncodingStrategy: URLBoolEncodingStrategy + public var dateEncodingStrategy: URLDateEncodingStrategy + public var arrayEncodingStrategy: URLArrayEncodingStrategy + public var spaceEncodingStrategy: URLSpaceEncodingStrategy + + // MARK: - Initializers + + public init( + boolEncodingStrategy: URLBoolEncodingStrategy = .numeric, + dateEncodingStrategy: URLDateEncodingStrategy = .deferredToDate, + arrayEncodingStrategy: URLArrayEncodingStrategy = .brackets, + spaceEncodingStrategy: URLSpaceEncodingStrategy = .percentEscaped + ) { + self.boolEncodingStrategy = boolEncodingStrategy + self.dateEncodingStrategy = dateEncodingStrategy + self.arrayEncodingStrategy = arrayEncodingStrategy + self.spaceEncodingStrategy = spaceEncodingStrategy + } + + // MARK: - Instance Methods + + public func encodeToQuery(_ value: T) throws -> String { + let urlEncodedFormContext = URLEncodedFormContext(component: .dictionary([:])) + + let urlEncodedFormEncoder = URLEncodedFormEncoder( + context: urlEncodedFormContext, + boolEncodingStrategy: boolEncodingStrategy, + dateEncodingStrategy: dateEncodingStrategy + ) + + try value.encode(to: urlEncodedFormEncoder) + + guard case let .dictionary(urlEncodedForm) = urlEncodedFormContext.component else { + let errorContext = EncodingError.Context( + codingPath: [], + debugDescription: "Root component cannot be encoded in URL" + ) + + throw EncodingError.invalidValue("\(value)", errorContext) + } + + let serializer = URLEncodedFormSerializer( + arrayEncodingStrategy: arrayEncodingStrategy, + spaceEncodingStrategy: spaceEncodingStrategy + ) + + return serializer.serialize(urlEncodedForm) + } + + public func encode(_ value: T) throws -> Data { + let query = try encodeToQuery(value) + + return Data(query.utf8) + } +} diff --git a/Sources/FigmaGenTools/URLEncoder/URLSpaceEncodingStrategy.swift b/Sources/FigmaGenTools/URLEncoder/URLSpaceEncodingStrategy.swift new file mode 100644 index 0000000..d9371bc --- /dev/null +++ b/Sources/FigmaGenTools/URLEncoder/URLSpaceEncodingStrategy.swift @@ -0,0 +1,9 @@ +import Foundation + +public enum URLSpaceEncodingStrategy { + + // MARK: - Enumeration Cases + + case percentEscaped + case plusReplaced +} diff --git a/Sources/Models/Color.swift b/Sources/Models/Color.swift deleted file mode 100644 index 856cc31..0000000 --- a/Sources/Models/Color.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -struct Color: Hashable { - - // MARK: - Instance Properties - - let name: String? - let red: Double - let green: Double - let blue: Double - let alpha: Double -} diff --git a/Sources/Models/Configuration/BaseConfiguration.swift b/Sources/Models/Configuration/BaseConfiguration.swift deleted file mode 100644 index d42562f..0000000 --- a/Sources/Models/Configuration/BaseConfiguration.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -struct BaseConfiguration: Decodable { - - // MARK: - Instance Properties - - let fileKey: String? - let accessToken: String? -} diff --git a/Sources/Models/Configuration/Configuration.swift b/Sources/Models/Configuration/Configuration.swift deleted file mode 100644 index 9c253ad..0000000 --- a/Sources/Models/Configuration/Configuration.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -struct Configuration: Decodable { - - // MARK: - Instance Properties - - let base: BaseConfiguration? - - let colors: StepConfiguration? - let textStyles: StepConfiguration? - let spacings: StepConfiguration? - - // MARK: - Instance Methods - - func resolveColorsConfiguration() -> StepConfiguration? { - return colors?.resolve(baseConfiguration: base) - } - - func resolveTextStylesConfiguration() -> StepConfiguration? { - return textStyles?.resolve(baseConfiguration: base) - } - - func resolveSpacingsConfiguration() -> StepConfiguration? { - return spacings?.resolve(baseConfiguration: base) - } -} diff --git a/Sources/Models/Configuration/ConfigurationError.swift b/Sources/Models/Configuration/ConfigurationError.swift deleted file mode 100644 index 265bac0..0000000 --- a/Sources/Models/Configuration/ConfigurationError.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -enum ConfigurationError: Error, CustomStringConvertible { - - // MARK: - Enumeration Cases - - case invalidFileKey - case invalidAccessToken - - // MARK: - Instance Properties - - var description: String { - switch self { - case .invalidFileKey: - return "Figma file key is either empty or nil" - - case .invalidAccessToken: - return "Figma access token is either empty or nil" - } - } -} diff --git a/Sources/Models/Configuration/StepConfiguration.swift b/Sources/Models/Configuration/StepConfiguration.swift deleted file mode 100644 index e036f41..0000000 --- a/Sources/Models/Configuration/StepConfiguration.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -struct StepConfiguration: Decodable { - - // MARK: - Instance Properties - - let fileKey: String? - let accessToken: String? - let includingNodes: [String]? - let excludingNodes: [String]? - let templatePath: String? - let destinationPath: String? - - // MARK: - Instance Methods - - func resolve(baseConfiguration: BaseConfiguration?) -> StepConfiguration { - guard let baseConfiguration = baseConfiguration else { - return self - } - - return StepConfiguration( - fileKey: fileKey ?? baseConfiguration.fileKey, - accessToken: accessToken ?? baseConfiguration.accessToken, - includingNodes: includingNodes, - excludingNodes: excludingNodes, - templatePath: templatePath, - destinationPath: destinationPath - ) - } -} diff --git a/Sources/Models/Figma/FigmaBooleanOperationType.swift b/Sources/Models/Figma/FigmaBooleanOperationType.swift deleted file mode 100644 index 7c4d79f..0000000 --- a/Sources/Models/Figma/FigmaBooleanOperationType.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known types of Boolean operations. -/// Get more info: https://www.figma.com/developers/api#boolean_operation-props -enum FigmaBooleanOperationType: String, Hashable { - - // MARK: - Enumeration Cases - - case union = "UNION" - case intersect = "INTERSECT" - case subtract = "SUBTRACT" - case exclude = "EXCLUDE" -} diff --git a/Sources/Models/Figma/FigmaColor.swift b/Sources/Models/Figma/FigmaColor.swift deleted file mode 100644 index d1dddf6..0000000 --- a/Sources/Models/Figma/FigmaColor.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// An RGBA color. -/// Get more info: https://www.figma.com/developers/api#color-type -struct FigmaColor: Decodable, Hashable { - - // MARK: - Instance Properties - - /// Red channel value, between 0 and 1. - let r: Double - - /// Green channel value, between 0 and 1. - let g: Double - - /// Blue channel value, between 0 and 1. - let b: Double - - /// Alpha channel value, between 0 and 1. - let a: Double -} diff --git a/Sources/Models/Figma/FigmaColorStop.swift b/Sources/Models/Figma/FigmaColorStop.swift deleted file mode 100644 index e9c009f..0000000 --- a/Sources/Models/Figma/FigmaColorStop.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A position color pair representing a gradient stop. -/// Get more info: https://www.figma.com/developers/api#colorstop-type -struct FigmaColorStop: Decodable, Hashable { - - // MARK: - Instance Properties - - /// Value between 0 and 1 representing position along gradient axis. - let position: Double - - /// Color attached to corresponding position. - let color: FigmaColor -} diff --git a/Sources/Models/Figma/FigmaComponent.swift b/Sources/Models/Figma/FigmaComponent.swift deleted file mode 100644 index 4c123d7..0000000 --- a/Sources/Models/Figma/FigmaComponent.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A description of a master component. -/// Helps you identify which component instances are attached to. -/// Get more info: https://www.figma.com/developers/api#component-type -struct FigmaComponent: Decodable, Hashable { - - // MARK: - Instance Properties - - /// The key of the component. - let key: String - - /// The name of the component. - let name: String - - /// The description of the component as entered in the editor. - let description: String? -} diff --git a/Sources/Models/Figma/FigmaConstraintType.swift b/Sources/Models/Figma/FigmaConstraintType.swift deleted file mode 100644 index a3752b6..0000000 --- a/Sources/Models/Figma/FigmaConstraintType.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known constraint types. -/// Get more info: https://www.figma.com/developers/api#constraint-type -enum FigmaConstraintType: String, Hashable { - - // MARK: - Enumeration Cases - - /// Scale proportionally and set width to value of constraint. - case width = "WIDTH" - - /// Scale proportionally and set height to value of constraint. - case height = "HEIGHT" - - /// Scale by value of constraint. - case scale = "SCALE" -} diff --git a/Sources/Models/Figma/FigmaEasingType.swift b/Sources/Models/Figma/FigmaEasingType.swift deleted file mode 100644 index 7ed443c..0000000 --- a/Sources/Models/Figma/FigmaEasingType.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration describing animation easing curves. -/// Get more info: https://www.figma.com/developers/api#easingtype-type -enum FigmaEasingType: String, Hashable { - - // MARK: - Enumeration Cases - - /// Ease in with an animation curve similar to CSS ease-in. - case easeIn = "EASE_IN" - - /// Ease out with an animation curve similar to CSS ease-out. - case easeOut = "EASE_OUT" - - /// Ease in and then out with an animation curve similar to CSS ease-in-out. - case easeInOut = "EASE_IN_AND_OUT" -} diff --git a/Sources/Models/Figma/FigmaEffect.swift b/Sources/Models/Figma/FigmaEffect.swift deleted file mode 100644 index 841acc8..0000000 --- a/Sources/Models/Figma/FigmaEffect.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A visual effect such as a shadow or blur. -/// Get more info: https://www.figma.com/developers/api#effect-type -struct FigmaEffect: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case rawType = "type" - case isVisible = "visible" - case radius - case color - case rawBlendMode = "blendMode" - case offset - } - - // MARK: - Instance Properties - - /// Raw type of effect. - let rawType: String - - /// Is the effect active? - /// Defaults to `true`. - let isVisible: Bool? - - /// Radius of the blur effect (applies to shadows as well) - let radius: Double - - /// The color of the shadow. - /// Relevant only for shadows. - let color: FigmaColor? - - /// Raw value of blend mode of the shadow. - /// Relevant only for shadows. - let rawBlendMode: String? - - /// How far the shadow is projected in the x and y directions. - /// Relevant only for shadows. - let offset: FigmaVector? - - /// Type of effect. - var type: FigmaEffectType? { - FigmaEffectType(rawValue: rawType) - } - - /// Blend mode of the shadow. - /// Relevant only for shadows. - var blendMode: FigmaBlendMode? { - rawBlendMode.flatMap(FigmaBlendMode.init) - } -} diff --git a/Sources/Models/Figma/FigmaEffectType.swift b/Sources/Models/Figma/FigmaEffectType.swift deleted file mode 100644 index e8853c5..0000000 --- a/Sources/Models/Figma/FigmaEffectType.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known effect types. -/// Get more info: https://www.figma.com/developers/api#effect-type -enum FigmaEffectType: String, Hashable { - - // MARK: - Enumeration Cases - - case innerShadow = "INNER_SHADOW" - case dropShadow = "DROP_SHADOW" - case layerBlur = "LAYER_BLUR" - case backgroundBlur = "BACKGROUND_BLUR" -} diff --git a/Sources/Models/Figma/FigmaExportSetting.swift b/Sources/Models/Figma/FigmaExportSetting.swift deleted file mode 100644 index 0e903f8..0000000 --- a/Sources/Models/Figma/FigmaExportSetting.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Format and size to export an asset at. -/// Get more info: https://www.figma.com/developers/api#exportsetting-type -struct FigmaExportSetting: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case suffix - case rawFormat = "format" - case constraint - } - - // MARK: - Instance Properties - - /// File suffix to append to all filenames. - let suffix: String - - /// Raw value of the image format. - let rawFormat: String - - /// Constraint that determines sizing of exported asset. - let constraint: FigmaConstraint - - /// Image format. - var format: FigmaImageFormat? { - FigmaImageFormat(rawValue: rawFormat) - } -} diff --git a/Sources/Models/Figma/FigmaImageFormat.swift b/Sources/Models/Figma/FigmaImageFormat.swift deleted file mode 100644 index c9cf23c..0000000 --- a/Sources/Models/Figma/FigmaImageFormat.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known image formats. -/// Get more info: https://www.figma.com/developers/api#exportsetting-type -enum FigmaImageFormat: String, Hashable { - - // MARK: - Enumeration Cases - - case jpg = "JPG" - case png = "PNG" - case svg = "SVG" - case pdf = "PDF" -} diff --git a/Sources/Models/Figma/FigmaLayoutConstraint.swift b/Sources/Models/Figma/FigmaLayoutConstraint.swift deleted file mode 100644 index 3f5a4b4..0000000 --- a/Sources/Models/Figma/FigmaLayoutConstraint.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Layout constraint relative to containing Frame. -/// Get more info: https://www.figma.com/developers/api#layoutconstraint-type -struct FigmaLayoutConstraint: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case rawVertical = "vertical" - case rawHorizontal = "horizontal" - } - - // MARK: - Instance Properties - - /// Raw value of vertical constraint. - let rawVertical: String - - /// Raw value of horizontal constraint. - let rawHorizontal: String - - /// Vertical constraint. - var vertical: FigmaLayoutVerticalConstraint? { - FigmaLayoutVerticalConstraint(rawValue: rawVertical) - } - - /// Horizontal constraint. - var horizontal: FigmaLayoutHorizontalConstraint? { - FigmaLayoutHorizontalConstraint(rawValue: rawHorizontal) - } -} diff --git a/Sources/Models/Figma/FigmaLayoutGrid.swift b/Sources/Models/Figma/FigmaLayoutGrid.swift deleted file mode 100644 index 40a42a3..0000000 --- a/Sources/Models/Figma/FigmaLayoutGrid.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Guides to align and place objects within a frame. -/// Get more info: https://www.figma.com/developers/api#layoutgrid-type -struct FigmaLayoutGrid: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case rawPattern = "pattern" - case rawAlignment = "alignment" - case sectionSize - case isVisible = "visible" - case color - case gutterSize - case offset - case count - } - - // MARK: - Instance Properties - - /// Raw value of orientation of the grid. - let rawPattern: String - - /// Raw value of alignment of the grid. - /// Only meaningful for directional grids (columns or rows). - let rawAlignment: String? - - /// Width of column grid or height of row grid or square grid spacing. - let sectionSize: Double - - /// Is the grid currently visible? - /// Defaults to `true`. - let isVisible: Bool? - - /// Color of the grid. - let color: FigmaColor - - /// Spacing in between columns and rows. - /// Relevant only for directional grids (columns or rows). - let gutterSize: Double? - - /// Spacing before the first column or row. - /// Relevant only for directional grids (columns or rows). - let offset: Double? - - /// Number of columns or rows. - /// Relevant only for directional grids (columns or rows). - let count: Double? - - /// Orientation of the grid. - var pattern: FigmaLayoutGridPattern? { - FigmaLayoutGridPattern(rawValue: rawPattern) - } - - /// Alignment of the grid. - /// Relevant only for directional grids (columns or rows). - var alignment: FigmaLayoutGridAlignment? { - rawAlignment.flatMap(FigmaLayoutGridAlignment.init) - } -} diff --git a/Sources/Models/Figma/FigmaLayoutGridAlignment.swift b/Sources/Models/Figma/FigmaLayoutGridAlignment.swift deleted file mode 100644 index 65162a2..0000000 --- a/Sources/Models/Figma/FigmaLayoutGridAlignment.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known alignment types of grid. -/// Get more info: https://www.figma.com/developers/api#layoutgrid-type -enum FigmaLayoutGridAlignment: String, Hashable { - - // MARK: - Enumeration Cases - - /// Grid starts at the left or top of the frame. - case min = "MIN" - - /// Grid is stretched to fit the frame. - case stretch = "STRETCH" - - /// Grid is center aligned. - case center = "CENTER" -} diff --git a/Sources/Models/Figma/FigmaLayoutGridPattern.swift b/Sources/Models/Figma/FigmaLayoutGridPattern.swift deleted file mode 100644 index 083385c..0000000 --- a/Sources/Models/Figma/FigmaLayoutGridPattern.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known orientations of grid. -/// Get more info: https://www.figma.com/developers/api#layoutgrid-type -enum FigmaLayoutGridPattern: String, Hashable { - - // MARK: - Enumeration Cases - - /// Vertical grid. - case columns = "COLUMNS" - - /// Horizontal grid. - case rows = "ROWS" - - /// Square grid. - case grid = "GRID" -} diff --git a/Sources/Models/Figma/FigmaLayoutHorizontalConstraint.swift b/Sources/Models/Figma/FigmaLayoutHorizontalConstraint.swift deleted file mode 100644 index e9ceff1..0000000 --- a/Sources/Models/Figma/FigmaLayoutHorizontalConstraint.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known horizontal constraints. -/// Get more info: https://www.figma.com/developers/api#layoutconstraint-type -enum FigmaLayoutHorizontalConstraint: String, Hashable { - - // MARK: - Enumeration Cases - - /// Node is laid out relative to left of the containing frame. - case left = "LEFT" - - /// Node is laid out relative to right of the containing frame. - case right = "RIGHT" - - /// Node is horizontally centered relative to containing frame. - case center = "CENTER" - - /// Both left and right of node are constrained relative to containing frame (node stretches with frame). - case leftRight = "LEFT_RIGHT" - - /// Node scales horizontally with containing frame. - case scale = "SCALE" -} diff --git a/Sources/Models/Figma/FigmaLayoutVerticalConstraint.swift b/Sources/Models/Figma/FigmaLayoutVerticalConstraint.swift deleted file mode 100644 index a1da832..0000000 --- a/Sources/Models/Figma/FigmaLayoutVerticalConstraint.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known vertical constraints. -/// Get more info: https://www.figma.com/developers/api#layoutconstraint-type -enum FigmaLayoutVerticalConstraint: String, Hashable { - - // MARK: - Enumeration Cases - - /// Node is laid out relative to top of the containing frame. - case top = "TOP" - - /// Node is laid out relative to bottom of the containing frame. - case bottom = "BOTTOM" - - /// Node is vertically centered relative to containing frame. - case center = "CENTER" - - /// Both top and bottom of node are constrained relative to containing frame (node stretches with frame). - case topBottom = "TOP_BOTTOM" - - /// Node scales vertically with containing frame. - case scale = "SCALE" -} diff --git a/Sources/Models/Figma/FigmaLineHeightUnit.swift b/Sources/Models/Figma/FigmaLineHeightUnit.swift deleted file mode 100644 index 6ce58a9..0000000 --- a/Sources/Models/Figma/FigmaLineHeightUnit.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known line height units. -/// Get more info: https://www.figma.com/developers/api#typestyle-type -enum FigmaLineHeightUnit: String, Hashable { - - // MARK: - Enumeration Cases - - case pixels = "PIXELS" - case fontSizePercent = "FONT_SIZE_%" - case intrinsicPercent = "INTRINSIC_%" -} diff --git a/Sources/Models/Figma/FigmaPaint.swift b/Sources/Models/Figma/FigmaPaint.swift deleted file mode 100644 index 65771b3..0000000 --- a/Sources/Models/Figma/FigmaPaint.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A solid color, gradient, or image texture that can be applied as fills or strokes. -/// Get more info: https://www.figma.com/developers/api#paint-type -struct FigmaPaint: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case rawType = "type" - case isVisible = "visible" - case opacity - case color - case rawBlendMode = "blendMode" - case gradientHandlePositions - case gradientStops - case rawScaleMode = "scaleMode" - case imageTransform - case imageRef - case gifRef - } - - // MARK: - Instance Properties - - /// Raw type of paint. - let rawType: String - - /// Is the paint enabled? - /// Defaults to `true`. - let isVisible: Bool? - - /// Overall opacity of paint (colors within the paint can also have opacity values which would blend with this). - /// Defaults to `1`. - let opacity: Double? - - /// Solid color of the paint. - /// Relevant only for solid paints. - let color: FigmaColor? - - /// Raw value of blend mode of the gradient. - /// Relevant only for gradient paints. - let rawBlendMode: String? - - /// This field contains three vectors, each of which are a position in normalized object space - /// (normalized object space is if the top left corner of the bounding box of the object is (0; 0) - /// and the bottom right is (1; 1)). - /// The first position corresponds to the start of the gradient - /// (value 0 for the purposes of calculating gradient stops), - /// the second position is the end of the gradient (value 1), - /// and the third handle position determines the width of the gradient. - /// Relevant only for gradient paints. - let gradientHandlePositions: [FigmaVector]? - - /// Positions of key points along the gradient axis with the colors anchored there. - /// Colors along the gradient are interpolated smoothly between neighboring gradient stops. - /// Relevant only for gradient paints. - let gradientStops: [FigmaColorStop]? - - /// Raw value of image scaling mode. - /// Relevant only for image paints. - let rawScaleMode: String? - - /// Affine transform applied to the image, only present if `scaleMode` is `stretch`. - /// A 2D affine transformation matrix that can be used to calculate the affine transforms applied to a layer, - /// including scaling, rotation, shearing, and translation. - /// The form of the matrix is given as an array of 2 arrays of 3 numbers each. - /// E.g. the identity matrix would be [[1, 0, 0], [0, 1, 0]]. - /// Relevant only for image paints. - let imageTransform: [[Double]]? - - /// A reference to an image embedded in this node. - /// To download the image using this reference, use images route (`FigmaAPIFileImagesRoute`) - /// to retrieve the mapping from image references to image URLs. - /// Relevant only for image paints. - let imageRef: String? - - /// A reference to the GIF embedded in this node, if the image is a GIF. - /// To download the image using this reference, use images route (`FigmaAPIFileImagesRoute`) - /// to retrieve the mapping from image references to image URLs. - /// Relevant only for image paints. - let gifRef: String? - - /// Type of paint. - var type: FigmaPaintType? { - FigmaPaintType(rawValue: rawType) - } - - /// Blend mode of the gradient. - /// Relevant only for gradient paints. - var blendMode: FigmaBlendMode? { - rawBlendMode.flatMap(FigmaBlendMode.init) - } - - /// Image scaling mode. - /// Relevant only for image paints. - var scaleMode: FigmaScaleMode? { - rawScaleMode.flatMap(FigmaScaleMode.init) - } -} diff --git a/Sources/Models/Figma/FigmaRectangle.swift b/Sources/Models/Figma/FigmaRectangle.swift deleted file mode 100644 index c50fe4b..0000000 --- a/Sources/Models/Figma/FigmaRectangle.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A rectangle that expresses a bounding box in absolute coordinates. -/// Get more info: https://www.figma.com/developers/api#rectangle-type -struct FigmaRectangle: Decodable, Hashable { - - // MARK: - Instance Properties - - /// X coordinate of top left corner of the rectangle. - let x: Double? - - /// Y coordinate of top left corner of the rectangle. - let y: Double? - - /// Width of the rectangle. - let width: Double? - - /// Height of the rectangle. - let height: Double? -} diff --git a/Sources/Models/Figma/FigmaScaleMode.swift b/Sources/Models/Figma/FigmaScaleMode.swift deleted file mode 100644 index 3f253db..0000000 --- a/Sources/Models/Figma/FigmaScaleMode.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known scale modes. -/// Get more info: https://www.figma.com/developers/api#paint-type -enum FigmaScaleMode: String, Hashable { - - // MARK: - Enumeration Cases - - case fill = "FILL" - case fit = "FIT" - case tile = "TILE" - case stretch = "STRETCH" -} diff --git a/Sources/Models/Figma/FigmaStrokeAlignment.swift b/Sources/Models/Figma/FigmaStrokeAlignment.swift deleted file mode 100644 index d7ae6e0..0000000 --- a/Sources/Models/Figma/FigmaStrokeAlignment.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known types of stroke alignment. -/// Get more info: https://www.figma.com/developers/api#vector-props -enum FigmaStrokeAlignment: String, Hashable { - - // MARK: - Enumeration Cases - - /// Draw stroke inside the shape boundary. - case inside = "INSIDE" - - /// Draw stroke outside the shape boundary. - case outside = "OUTSIDE" - - /// Draw stroke centered along the shape boundary. - case center = "CENTER" -} diff --git a/Sources/Models/Figma/FigmaStrokeCap.swift b/Sources/Models/Figma/FigmaStrokeCap.swift deleted file mode 100644 index 5a80ae4..0000000 --- a/Sources/Models/Figma/FigmaStrokeCap.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known styles describing the end caps of vector paths. -/// Get more info: https://www.figma.com/developers/api#vector-props -enum FigmaStrokeCap: String, Hashable { - - // MARK: - Enumeration Cases - - case none = "NONE" - case round = "ROUND" - case square = "SQUARE" - case lineArrow = "LINE_ARROW" - case triangleArrow = "TRIANGLE_ARROW" -} diff --git a/Sources/Models/Figma/FigmaStrokeJoin.swift b/Sources/Models/Figma/FigmaStrokeJoin.swift deleted file mode 100644 index e973f0b..0000000 --- a/Sources/Models/Figma/FigmaStrokeJoin.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known styles describing how corners in vector paths are rendered. -/// Get more info: https://www.figma.com/developers/api#vector-props -enum FigmaStrokeJoin: String, Hashable { - - // MARK: - Enumeration Cases - - case miter = "MITER" - case bevel = "BEVEL" - case round = "ROUND" -} diff --git a/Sources/Models/Figma/FigmaStyle.swift b/Sources/Models/Figma/FigmaStyle.swift deleted file mode 100644 index 460aad5..0000000 --- a/Sources/Models/Figma/FigmaStyle.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A set of properties that can be applied to nodes and published. -/// Styles for a property can be created in the corresponding property's panel while editing a file. -/// Get more info: https://www.figma.com/developers/api#style-type -struct FigmaStyle: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case key - case rawType = "styleType" - case name - case description - } - - // MARK: - Instance Properties - - /// The key of the style. - let key: String? - - /// Raw type of the style - let rawType: String - - /// The name of the style. - let name: String? - - /// The description of the style. - let description: String? - - /// Type of the style. - var type: FigmaStyleType? { - FigmaStyleType(rawValue: rawType) - } -} diff --git a/Sources/Models/Figma/FigmaStyleType.swift b/Sources/Models/Figma/FigmaStyleType.swift deleted file mode 100644 index 877dff4..0000000 --- a/Sources/Models/Figma/FigmaStyleType.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known types of style. -/// Get more info: https://www.figma.com/developers/api#style-type -enum FigmaStyleType: String, Hashable { - - // MARK: - Enumeration Cases - - case fill = "FILL" - case text = "TEXT" - case effect = "EFFECT" - case grid = "GRID" -} diff --git a/Sources/Models/Figma/FigmaTextCase.swift b/Sources/Models/Figma/FigmaTextCase.swift deleted file mode 100644 index bb56456..0000000 --- a/Sources/Models/Figma/FigmaTextCase.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known text cases. -/// Get more info: https://www.figma.com/developers/api#typestyle-type -enum FigmaTextCase: String, Hashable { - - // MARK: - Enumeration Cases - - case original = "ORIGINAL" - case upper = "UPPER" - case lower = "LOWER" - case title = "TITLE" -} diff --git a/Sources/Models/Figma/FigmaTextDecoration.swift b/Sources/Models/Figma/FigmaTextDecoration.swift deleted file mode 100644 index f14249e..0000000 --- a/Sources/Models/Figma/FigmaTextDecoration.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known type of text decoration. -/// Get more info: https://www.figma.com/developers/api#typestyle-type -enum FigmaTextDecoration: String, Hashable { - - // MARK: - Enumeration Cases - - case none = "NONE" - case strikethrough = "STRIKETHROUGH" - case underline = "UNDERLINE" -} diff --git a/Sources/Models/Figma/FigmaTextHorizontalAlignment.swift b/Sources/Models/Figma/FigmaTextHorizontalAlignment.swift deleted file mode 100644 index a165149..0000000 --- a/Sources/Models/Figma/FigmaTextHorizontalAlignment.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known types of horizontal text alignment. -/// Get more info: https://www.figma.com/developers/api#typestyle-type -enum FigmaTextHorizontalAlignment: String, Hashable { - - // MARK: - Enumeration Cases - - case left = "LEFT" - case right = "RIGHT" - case center = "CENTER" - case justified = "JUSTIFIED" -} diff --git a/Sources/Models/Figma/FigmaTextVerticalAlignment.swift b/Sources/Models/Figma/FigmaTextVerticalAlignment.swift deleted file mode 100644 index 2b954a2..0000000 --- a/Sources/Models/Figma/FigmaTextVerticalAlignment.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Enumeration of known types of vertical text alignment. -/// Get more info: https://www.figma.com/developers/api#typestyle-type -enum FigmaTextVerticalAlignment: String, Hashable { - - // MARK: - Enumeration Cases - - case top = "TOP" - case center = "CENTER" - case bottom = "BOTTOM" -} diff --git a/Sources/Models/Figma/FigmaVector.swift b/Sources/Models/Figma/FigmaVector.swift deleted file mode 100644 index 6a2ba93..0000000 --- a/Sources/Models/Figma/FigmaVector.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// A 2D vector. -/// Get more info: https://www.figma.com/developers/api#vector-type -struct FigmaVector: Decodable, Hashable { - - // MARK: - Instance Properties - - /// X coordinate of the vector. - let x: Double - - /// Y coordinate of the vector. - let y: Double -} diff --git a/Sources/Models/Figma/Nodes/FigmaBooleanOperationNodePayload.swift b/Sources/Models/Figma/Nodes/FigmaBooleanOperationNodePayload.swift deleted file mode 100644 index 33c9b14..0000000 --- a/Sources/Models/Figma/Nodes/FigmaBooleanOperationNodePayload.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Boolean operation node specific proprties. -/// Get more info: https://www.figma.com/developers/api#boolean_operation-props -struct FigmaBooleanOperationNodePayload: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case children - case rawOperationType = "booleanOperation" - } - - // MARK: - Instance Properties - - /// An array of nodes that are being boolean operated on. - let children: [FigmaNode]? - - /// Raw type of Boolean operation. - let rawOperationType: String - - /// Type of Boolean operation. - var operationType: FigmaBooleanOperationType? { - FigmaBooleanOperationType(rawValue: rawOperationType) - } -} diff --git a/Sources/Models/Figma/Nodes/FigmaCanvasNodeInfo.swift b/Sources/Models/Figma/Nodes/FigmaCanvasNodeInfo.swift deleted file mode 100644 index def0b95..0000000 --- a/Sources/Models/Figma/Nodes/FigmaCanvasNodeInfo.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Canvas node specific proprties. -/// Get more info: https://www.figma.com/developers/api#canvas-props -struct FigmaCanvasNodeInfo: Decodable, Hashable { - - // MARK: - Instance Properties - - /// An array of top level layers on the canvas. - let children: [FigmaNode] - - /// Background color of the canvas. - let backgroundColor: FigmaColor - - /// Node ID that corresponds to the start frame for prototypes. - let prototypeStartNodeID: String? - - /// An array of export settings representing images to export from the canvas. - let exportSettings: [FigmaExportSetting]? -} diff --git a/Sources/Models/Figma/Nodes/FigmaDocumentNodeInfo.swift b/Sources/Models/Figma/Nodes/FigmaDocumentNodeInfo.swift deleted file mode 100644 index a0ea248..0000000 --- a/Sources/Models/Figma/Nodes/FigmaDocumentNodeInfo.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Document node specific proprties. -/// Get more info: https://www.figma.com/developers/api#document-props -struct FigmaDocumentNodeInfo: Decodable, Hashable { - - // MARK: - Instance Properties - - /// An array of canvases attached to the document. - let children: [FigmaNode]? -} diff --git a/Sources/Models/Figma/Nodes/FigmaFrameNodeInfo.swift b/Sources/Models/Figma/Nodes/FigmaFrameNodeInfo.swift deleted file mode 100644 index 73fc06b..0000000 --- a/Sources/Models/Figma/Nodes/FigmaFrameNodeInfo.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Frame node specific proprties. -/// Get more info: https://www.figma.com/developers/api#frame-props -struct FigmaFrameNodeInfo: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case children - case isLocked = "locked" - case background - case exportSettings - case rawBlendMode = "blendMode" - case preserveRatio - case constraints - case transitionNodeID - case transitionDuration - case rawTransitionEasing - case opacity - case absoluteBoundingBox - case clipsContent - case layoutGrids - case effects - case isMask - case isMaskOutline - } - - // MARK: - Instance Properties - - /// An array of nodes that are direct children of this node. - let children: [FigmaNode]? - - /// If true, layer is locked and cannot be edited. - /// Defaults to `false`. - let isLocked: Bool? - - /// Background of the node. - let background: [FigmaPaint]? - - /// An array of export settings representing images to export from node. - let exportSettings: [FigmaExportSetting]? - - /// Raw value of blend mode. - /// Describes how this node blends with nodes behind it in the scene. - let rawBlendMode: String - - /// Keep height and width constrained to same ratio. - /// Defaults to `false`. - let preserveRatio: Bool? - - /// Horizontal and vertical layout constraints for node. - let constraints: FigmaLayoutConstraint - - /// Node ID of node to transition to in prototyping. - let transitionNodeID: String? - - /// The duration of the prototyping transition on this node in milliseconds. - let transitionDuration: Double? - - /// Raw value of easing curve type used in the prototyping transition on this node. - let rawTransitionEasing: String? - - /// Opacity of the node. - /// Defaults to `1`. - let opacity: Double? - - /// Bounding box of the node in absolute space coordinates. - let absoluteBoundingBox: FigmaRectangle - - /// Whether or not this node clip content outside of its bounds. - let clipsContent: Bool - - /// An array of layout grids attached to this node. - /// Group nodes do not have this attribute. - let layoutGrids: [FigmaLayoutGrid]? - - /// An array of effects attached to this node. - let effects: [FigmaEffect]? - - /// Does this node mask sibling nodes in front of it? - /// Defaults to `false`. - let isMask: Bool? - - /// Does this mask ignore fill style (like gradients) and effects? - /// Defaults to `false`. - let isMaskOutline: Bool? - - /// Blend mode. - /// Describes how this node blends with nodes behind it in the scene. - var blendMode: FigmaBlendMode? { - FigmaBlendMode(rawValue: rawBlendMode) - } - - /// The easing curve used in the prototyping transition on this node. - var transitionEasing: FigmaEasingType? { - rawTransitionEasing.flatMap(FigmaEasingType.init) - } -} diff --git a/Sources/Models/Figma/Nodes/FigmaInstanceNodePayload.swift b/Sources/Models/Figma/Nodes/FigmaInstanceNodePayload.swift deleted file mode 100644 index df5ee58..0000000 --- a/Sources/Models/Figma/Nodes/FigmaInstanceNodePayload.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Instance node specific proprties. -/// Get more info: https://www.figma.com/developers/api#instance-props -struct FigmaInstanceNodePayload: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case componentID = "componentId" - } - - // MARK: - Instance Properties - - /// Identifier of component that this instance came from, refers to `components` table. - let componentID: String -} diff --git a/Sources/Models/Figma/Nodes/FigmaRectangleNodePayload.swift b/Sources/Models/Figma/Nodes/FigmaRectangleNodePayload.swift deleted file mode 100644 index c2e841e..0000000 --- a/Sources/Models/Figma/Nodes/FigmaRectangleNodePayload.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Rectangle node specific proprties. -/// Get more info: https://www.figma.com/developers/api#rectangle-props -struct FigmaRectangleNodePayload: Decodable, Hashable { - - // MARK: - Instance Properties - - /// Radius of each corner of the rectangle if a single radius is set for all corners. - let cornerRadius: Double? - - /// Array of length 4 of the radius of each corner of the rectangle, - /// starting in the top left and proceeding clockwise. - let rectangleCornerRadii: [Double]? -} diff --git a/Sources/Models/Figma/Nodes/FigmaSliceNodeInfo.swift b/Sources/Models/Figma/Nodes/FigmaSliceNodeInfo.swift deleted file mode 100644 index 3077b41..0000000 --- a/Sources/Models/Figma/Nodes/FigmaSliceNodeInfo.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Slice node specific proprties. -/// Get more info: https://www.figma.com/developers/api#slice-props -struct FigmaSliceNodeInfo: Decodable, Hashable { - - // MARK: - Instance Properties - - /// An array of export settings representing images to export from this node. - let exportSettings: [FigmaExportSetting]? - - /// Bounding box of the node in absolute space coordinates. - let absoluteBoundingBox: FigmaRectangle -} diff --git a/Sources/Models/Figma/Nodes/FigmaTextNodePayload.swift b/Sources/Models/Figma/Nodes/FigmaTextNodePayload.swift deleted file mode 100644 index f0d131f..0000000 --- a/Sources/Models/Figma/Nodes/FigmaTextNodePayload.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Text node specific proprties. -/// Get more info: https://www.figma.com/developers/api#rectangle-props -struct FigmaTextNodePayload: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case text = "characters" - case style - case characterStyleOverrides - case styleOverrideTable - } - - // MARK: - Instance Properties - - /// Text contained within text box. - let text: String - - /// Style of text including font family and weight. - let style: FigmaTypeStyle? - - /// Array with same number of elements as characeters in text box, - /// each element is a reference to the `styleOverrideTable` - /// and maps to the corresponding character in the `text` field. - /// Elements with value 0 have the default type style. - let characterStyleOverrides: [Int]? - - /// Map from ID to `FigmaTypeStyle` for looking up style overrides. - let styleOverrideTable: [Int: FigmaTypeStyle]? -} diff --git a/Sources/Models/Figma/Nodes/FigmaVectorNodeInfo.swift b/Sources/Models/Figma/Nodes/FigmaVectorNodeInfo.swift deleted file mode 100644 index 73e8787..0000000 --- a/Sources/Models/Figma/Nodes/FigmaVectorNodeInfo.swift +++ /dev/null @@ -1,156 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Vector node specific proprties. -/// Get more info: https://www.figma.com/developers/api#vector-props -struct FigmaVectorNodeInfo: Decodable, Hashable { - - // MARK: - Nested Types - - private enum CodingKeys: String, CodingKey { - case isLocked = "locked" - case exportSettings - case rawBlendMode = "blendMode" - case preserveRatio - case constraints - case transitionNodeID - case transitionDuration - case rawTransitionEasing - case opacity - case absoluteBoundingBox - case effects - case isMask - case fills - case strokes - case strokeWeight - case rawStrokeCap = "strokeCap" - case rawStrokeJoin = "strokeJoin" - case strokeDashes - case strokeMiterAngle - case rawStrokeAlignment = "strokeAlign" - case styles - } - - // MARK: - Instance Properties - - /// If true, layer is locked and cannot be edited. - /// Defaults to `false`. - let isLocked: Bool? - - /// An array of export settings representing images to export from node. - let exportSettings: [FigmaExportSetting]? - - /// Raw value of blend mode. - /// Describes how this node blends with nodes behind it in the scene. - let rawBlendMode: String - - /// Keep height and width constrained to same ratio. - /// Defaults to `false`. - let preserveRatio: Bool? - - /// Horizontal and vertical layout constraints for node. - let constraints: FigmaLayoutConstraint - - /// Node ID of node to transition to in prototyping. - let transitionNodeID: String? - - /// The duration of the prototyping transition on this node in milliseconds. - let transitionDuration: Double? - - /// Raw value of easing curve type used in the prototyping transition on this node. - let rawTransitionEasing: String? - - /// Opacity of the node. - /// Defaults to `1`. - let opacity: Double? - - /// Bounding box of the node in absolute space coordinates. - let absoluteBoundingBox: FigmaRectangle - - /// An array of effects attached to this node. - let effects: [FigmaEffect]? - - /// Does this node mask sibling nodes in front of it? - /// Defaults to `false`. - let isMask: Bool? - - /// An array of fill paints applied to the node. - let fills: [FigmaPaint]? - - /// An array of stroke paints applied to the node. - let strokes: [FigmaPaint]? - - /// The weight of strokes on the node. - let strokeWeight: Double - - /// Raw value of style that describes the end caps of vector paths. - /// Defaults to `NONE`. - let rawStrokeCap: String? - - /// Raw value of style that describes how corners in vector paths are rendered. - /// Defaults to `MITER`. - let rawStrokeJoin: String? - - /// An array of floating point numbers describing the pattern of dash length - /// and gap lengths that the vector path follows. - /// For example a value of [1, 2] indicates - /// that the path has a dash of length 1 followed by a gap of length 2, repeated. - let strokeDashes: [Double]? - - /// The corner angle, in degrees, below which `strokeJoin` will be set to `bevel` to avoid super sharp corners. - /// Only valid if `strokeJoin` is `miter`. - /// Defaults to `28.96`. - let strokeMiterAngle: Double? - - /// Raw value of stroke alignment that describes where stroke is drawn relative to the vector outline. - let rawStrokeAlignment: String - - /// A mapping of a raw style type to style ID (see `FigmaStyle`) of styles present on this node. - /// The style ID can be used to look up more information about the style in the top-level styles field. - let styles: [String: String]? - - /// Blend mode. - /// Describes how this node blends with nodes behind it in the scene. - var blendMode: FigmaBlendMode? { - FigmaBlendMode(rawValue: rawBlendMode) - } - - /// The easing curve used in the prototyping transition on this node. - var transitionEasing: FigmaEasingType? { - rawTransitionEasing.flatMap(FigmaEasingType.init) - } - - /// Style that describes the end caps of vector paths. - var strokeCap: FigmaStrokeCap? { - guard let rawStrokeCap = rawStrokeCap else { - return FigmaStrokeCap.none - } - - return FigmaStrokeCap(rawValue: rawStrokeCap) - } - - /// Style that describes how corners in vector paths are rendered. - var strokeJoin: FigmaStrokeJoin? { - guard let rawStrokeJoin = rawStrokeJoin else { - return FigmaStrokeJoin.miter - } - - return FigmaStrokeJoin(rawValue: rawStrokeJoin) - } - - /// Stroke alignment that describes where stroke is drawn relative to the vector outline. - var strokeAlignment: FigmaStrokeAlignment? { - FigmaStrokeAlignment(rawValue: rawStrokeAlignment) - } - - // MARK: - Instance Methods - - func styleID(of styleType: FigmaStyleType) -> String? { - return styles?[styleType.rawValue.lowercased()] - } -} diff --git a/Sources/Models/Spacing.swift b/Sources/Models/Spacing.swift deleted file mode 100644 index d21238c..0000000 --- a/Sources/Models/Spacing.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation - -struct Spacing: Equatable { - - // MARK: - Instance Properties - - let name: String - let value: Double -} diff --git a/Sources/Models/TemplateType.swift b/Sources/Models/TemplateType.swift deleted file mode 100644 index 1062558..0000000 --- a/Sources/Models/TemplateType.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PathKit - -enum TemplateType { - - // MARK: - Nested Types - - private enum Constants { - static let bundleTemplates = "Templates" - static let podsTemplates = "../Templates" - static let shareTemplates = "../../share/figmagen" - } - - // MARK: - Enumeration Cases - - case native(name: String) - case custom(path: String) - - // MARK: - Instance Methods - - func resolvePath() throws -> String { - switch self { - case let .native(name: templateName): - let bundle = Bundle(for: BundleToken.self) - - if let bundleTemplatesPath = bundle.path(forResource: Constants.bundleTemplates, ofType: nil) { - return Path(bundleTemplatesPath).appending(templateName).string - } - - var executablePath = Path(ProcessInfo.processInfo.executablePath) - - while executablePath.isSymlink { - executablePath = try executablePath.symlinkDestination() - } - - let podsTemplatesPath = executablePath.appending(Constants.podsTemplates) - - if podsTemplatesPath.exists { - return podsTemplatesPath.appending(templateName).string - } - - return executablePath - .appending(Constants.shareTemplates) - .appending(templateName) - .string - - case let .custom(path: templatePath): - return templatePath - } - } -} - -private final class BundleToken {} diff --git a/Sources/Models/TextStyle.swift b/Sources/Models/TextStyle.swift deleted file mode 100644 index 1fa17ee..0000000 --- a/Sources/Models/TextStyle.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -struct TextStyle: Hashable { - - // MARK: - Instance Properties - - let name: String - - let fontFamily: String - let fontPostScriptName: String - let fontWeight: Double - let fontSize: Double - - let textColor: Color - - let paragraphSpacing: Double? - let paragraphIndent: Double? - let lineHeight: Double? - let letterSpacing: Double? -} diff --git a/Sources/Services.swift b/Sources/Services.swift deleted file mode 100644 index 9bc3790..0000000 --- a/Sources/Services.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -final class Services { - - // MARK: - Instance Methods - - private func makeAPIProvider(accessToken: String) -> FigmaAPIProvider { - return DefaultFigmaAPIProvider(accessToken: accessToken) - } - - private func makeNodesExtractor() -> NodesExtractor { - return DefaultNodesExtractor() - } -} - -extension Services: ColorsServices { - - // MARK: - Instance Methods - - func makeColorsProvider(accessToken: String) -> ColorsProvider { - return DefaultColorsProvider( - apiProvider: makeAPIProvider(accessToken: accessToken), - nodesExtractor: makeNodesExtractor() - ) - } - - func makeColorsRenderer() -> ColorsRenderer { - return DefaultColorsRenderer() - } -} - -extension Services: TextStylesServices { - - // MARK: - Instance Methods - - func makeTextStylesProvider(accessToken: String) -> TextStylesProvider { - return DefaultTextStylesProvider( - apiProvider: makeAPIProvider(accessToken: accessToken), - nodesExtractor: makeNodesExtractor() - ) - } - - func makeTextStylesRenderer() -> TextStylesRenderer { - return DefaultTextStylesRenderer() - } -} - -extension Services: SpacingsServices { - - // MARK: - Instance Methods - - func makeSpacingsProvider(accessToken: String) -> SpacingsProvider { - DefaultSpacingsProvider( - apiProvider: makeAPIProvider(accessToken: accessToken), - nodesExtractor: makeNodesExtractor() - ) - } - - func makeSpacingsRenderer() -> SpacingsRenderer { - DefaultSpacingsRenderer() - } -} diff --git a/Sources/Services/API/DefaultFigmaAPIProvider.swift b/Sources/Services/API/DefaultFigmaAPIProvider.swift deleted file mode 100644 index afaff87..0000000 --- a/Sources/Services/API/DefaultFigmaAPIProvider.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit -import Alamofire - -final class DefaultFigmaAPIProvider { - - // MARK: - Nested Types - - private enum Constants { - static let serverBaseURL = URL(string: "https://api.figma.com")! - static let accessTokenHeaderName = "X-Figma-Token" - } - - // MARK: - Instance Properties - - private let alamofireSession: Alamofire.Session - private let responseDecoder: JSONDecoder - - // MARK: - - - private let accessToken: String - - // MARK: - Initializers - - init(accessToken: String) { - self.accessToken = accessToken - - alamofireSession = Alamofire.Session() - responseDecoder = JSONDecoder() - - responseDecoder.dateDecodingStrategy = .custom { decoder in - let container = try decoder.singleValueContainer() - let dateString = try container.decode(String.self) - - if let date = DateFormatter.figmaAPI(withMilliseconds: true).date(from: dateString) { - return date - } - - if let date = DateFormatter.figmaAPI(withMilliseconds: false).date(from: dateString) { - return date - } - - throw DecodingError.dataCorruptedError( - in: container, - debugDescription: "Date string does not match format expected by formatter" - ) - } - } -} - -extension DefaultFigmaAPIProvider: FigmaAPIProvider { - - // MARK: - Instance Methods - - func request(route: Route) -> Promise { - let url = Constants.serverBaseURL - .appendingPathComponent(route.apiVersion.urlPath) - .appendingPathComponent(route.urlPath) - - let accessTokenHTTPHeader = HTTPHeader(name: Constants.accessTokenHeaderName, value: accessToken) - - let httpMethod = HTTPMethod(rawValue: route.httpMethod.rawValue) - let httpHeaders = HTTPHeaders([accessTokenHTTPHeader]) - - return Promise { seal in - alamofireSession.request( - url, - method: httpMethod, - parameters: route.parameters, - headers: httpHeaders - ) - .validate() - .responseDecodable(of: Route.Response.self, decoder: responseDecoder) { response in - switch response.result { - case let .failure(error): - seal.reject(error) - - case let .success(value): - seal.fulfill(value) - } - } - } - } -} - -private extension DateFormatter { - - // MARK: - Type Properties - static func figmaAPI(withMilliseconds: Bool) -> DateFormatter { - let dateFormatter = DateFormatter() - - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - - if withMilliseconds { - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" - } else { - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" - } - - return dateFormatter - } -} diff --git a/Sources/Services/API/FigmaAPIRoute.swift b/Sources/Services/API/FigmaAPIRoute.swift deleted file mode 100644 index 8001387..0000000 --- a/Sources/Services/API/FigmaAPIRoute.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -protocol FigmaAPIRoute { - - // MARK: - Nested Types - - associatedtype Response: Decodable - associatedtype Parameters: Encodable - - // MARK: - Instance Properties - - var apiVersion: FigmaAPIVersion { get } - var httpMethod: FigmaAPIHTTPMethod { get } - var urlPath: String { get } - var parameters: Parameters { get } -} - -extension FigmaAPIRoute { - - // MARK: - Instance Properties - - var apiVersion: FigmaAPIVersion { - .v1 - } - - var httpMethod: FigmaAPIHTTPMethod { - .get - } -} diff --git a/Sources/Services/API/Routes/FigmaAPIFileRoute.swift b/Sources/Services/API/Routes/FigmaAPIFileRoute.swift deleted file mode 100644 index e876fd1..0000000 --- a/Sources/Services/API/Routes/FigmaAPIFileRoute.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -/// Route to fetch the document refered to by key. -struct FigmaAPIFileRoute: FigmaAPIRoute { - - // MARK: - Nested Types - - typealias Response = FigmaFile - - struct Parameters: Encodable { - let version: String? - let ids: String? - let depth: Int? - } - - // MARK: - Instance Properties - - /// The file key can be parsed from any Figma file URL: https://www.figma.com/file/:key/:title - let fileKey: String - - /// Route parameters. - let parameters: Parameters - - /// Route URL path. - var urlPath: String { - "files/\(fileKey)" - } - - // MARK: - Initializers - - /// Creates a new instance with a file key and optional parameters. - /// - /// - Parameter fileKey: The file key. - /// - Parameter version: A specific version ID to get. Omitting this will get the current version of the file. - /// - Parameter ids: List of nodes that you care about in the document. - /// If specified, only a subset of the document will be returned corresponding to the nodes listed, - /// their children, and everything between the root node and the listed nodes. - /// - Parameter depth: Positive integer representing how deep into the document tree to traverse. - /// For example, setting this to 1 returns only Pages, - /// setting it to 2 returns Pages and all top level objects on each page. - /// Not setting this parameter returns all nodes. - init(fileKey: String, version: String? = nil, ids: [String]? = nil, depth: Int? = nil) { - self.fileKey = fileKey - - self.parameters = Parameters( - version: version, - ids: ids?.joined(separator: ", "), - depth: depth - ) - } -} diff --git a/Sources/Services/Colors/ColorsError.swift b/Sources/Services/Colors/ColorsError.swift deleted file mode 100644 index 7cdffb7..0000000 --- a/Sources/Services/Colors/ColorsError.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -enum ColorsError: Error, CustomStringConvertible { - - // MARK: - Enumeration Cases - - case styleNotFound(nodeName: String, nodeID: String) - case invalidStyleName(nodeName: String, nodeID: String) - case colorNotFound(nodeName: String, nodeID: String) - - // MARK: - Instance Properties - - var description: String { - switch self { - case let .styleNotFound(nodeName, nodeID): - return #"Figma file does not contain a style for node "\#(nodeName)" (\#(nodeID))"# - - case let .invalidStyleName(nodeName, nodeID): - return #"Style name is either empty or nil in node "\#(nodeName)" (\#(nodeID))"# - - case let .colorNotFound(nodeName, nodeID): - return #"Failed to extract a color of style in node "\#(nodeName)" (\#(nodeID))"# - } - } -} diff --git a/Sources/Services/Colors/ColorsProvider.swift b/Sources/Services/Colors/ColorsProvider.swift deleted file mode 100644 index 1e94bbe..0000000 --- a/Sources/Services/Colors/ColorsProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -protocol ColorsProvider { - - // MARK: - Instance Methods - - func fetchColors( - fileKey: String, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) -> Promise<[Color]> -} diff --git a/Sources/Services/Colors/ColorsRenderer.swift b/Sources/Services/Colors/ColorsRenderer.swift deleted file mode 100644 index d8ce7b3..0000000 --- a/Sources/Services/Colors/ColorsRenderer.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -protocol ColorsRenderer { - - // MARK: - Instance Methods - - func renderTemplate(_ templateType: TemplateType, to destinationPath: String, colors: [Color]) throws -} diff --git a/Sources/Services/Colors/DefaultColorsProvider.swift b/Sources/Services/Colors/DefaultColorsProvider.swift deleted file mode 100644 index b3f63ca..0000000 --- a/Sources/Services/Colors/DefaultColorsProvider.swift +++ /dev/null @@ -1,104 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -final class DefaultColorsProvider { - - // MARK: - Instance Properties - - let apiProvider: FigmaAPIProvider - let nodesExtractor: NodesExtractor - - // MARK: - Initializers - - init( - apiProvider: FigmaAPIProvider, - nodesExtractor: NodesExtractor - ) { - self.apiProvider = apiProvider - self.nodesExtractor = nodesExtractor - } - - // MARK: - Instance Methods - - private func extractColor(from node: FigmaNode, styles: [String: FigmaStyle]) throws -> Color? { - guard let vectorNodeInfo = node.vectorInfo, let nodeStyleID = vectorNodeInfo.styleID(of: .fill) else { - return nil - } - - guard let nodeStyle = styles[nodeStyleID] else { - throw ColorsError.styleNotFound(nodeName: node.name, nodeID: node.id) - } - - guard let nodeStyleName = nodeStyle.name, !nodeStyleName.isEmpty else { - throw ColorsError.invalidStyleName(nodeName: node.name, nodeID: node.id) - } - - let nodeSingleSolidFill = vectorNodeInfo - .fills - .flatMap { $0.count == 1 ? $0.first : nil } - .flatMap { $0.type == .solid ? $0 : nil } - - guard let nodeFill = nodeSingleSolidFill else { - return nil - } - - guard let nodeFillColor = nodeFill.color else { - throw ColorsError.colorNotFound(nodeName: node.name, nodeID: node.id) - } - - return Color( - name: nodeStyleName, - red: nodeFillColor.r, - green: nodeFillColor.g, - blue: nodeFillColor.b, - alpha: nodeFillColor.a - ) - } - - private func extractColors( - from file: FigmaFile, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) throws -> [Color] { - let styles = file - .styles? - .filter { $0.value.type == .fill } ?? [:] - - return try nodesExtractor - .extractNodes(from: file, including: includingNodeIDs, excluding: excludingNodeIDs) - .lazy - .compactMap { try extractColor(from: $0, styles: styles) } - .reduce(into: []) { result, color in - if !result.contains(color) { - result.append(color) - } - } - } -} - -extension DefaultColorsProvider: ColorsProvider { - - // MARK: - Instance Methods - - func fetchColors( - fileKey: String, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) -> Promise<[Color]> { - return firstly { - self.apiProvider.request(route: FigmaAPIFileRoute(fileKey: fileKey)) - }.map(on: DispatchQueue.global(qos: .userInitiated)) { file in - try self.extractColors( - from: file, - includingNodes: includingNodeIDs, - excludingNodes: excludingNodeIDs - ) - } - } -} diff --git a/Sources/Services/Colors/DefaultColorsRenderer.swift b/Sources/Services/Colors/DefaultColorsRenderer.swift deleted file mode 100644 index 5cccd63..0000000 --- a/Sources/Services/Colors/DefaultColorsRenderer.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import StencilSwiftKit -import PathKit - -final class DefaultColorsRenderer { - - // MARK: - Instance Methods - - private func hexComponent(from number: Double) -> String { - return String(format: "%02lX", Int(number * 255.0)) - } - - private func makeContext(with colors: [Color]) -> [String: Any] { - let colors = colors.map { color in - return [ - "name": color.name, - "red": hexComponent(from: color.red), - "green": hexComponent(from: color.green), - "blue": hexComponent(from: color.blue), - "alpha": hexComponent(from: color.alpha) - ] - } - - return ["colors": colors] - } -} - -extension DefaultColorsRenderer: ColorsRenderer { - - // MARK: - Instance Methods - - func renderTemplate(_ templateType: TemplateType, to destinationPath: String, colors: [Color]) throws { - let templatePath = Path(try templateType.resolvePath()) - let destinationPath = Path(destinationPath) - - let template = try StencilSwiftTemplate( - templateString: templatePath.read(), - environment: stencilSwiftEnvironment() - ) - - let rendered = try template.render(makeContext(with: colors)) - - try destinationPath.parent().mkpath() - try destinationPath.write(rendered, encoding: .utf8) - } -} diff --git a/Sources/Services/Nodes/DefaultNodesExtractor.swift b/Sources/Services/Nodes/DefaultNodesExtractor.swift deleted file mode 100644 index e45609a..0000000 --- a/Sources/Services/Nodes/DefaultNodesExtractor.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -final class DefaultNodesExtractor { - - // MARK: - Instance Methods - - private func extractNodes( - from node: FigmaNode, - including includingNodeIDs: inout Set, - excluding excludingNodeIDs: inout Set, - forceInclude: Bool - ) -> [FigmaNode] { - let isIncludingNode = includingNodeIDs.remove(node.id) != nil - let isExcludingNode = excludingNodeIDs.remove(node.id) != nil - - var nodes: [FigmaNode] = [] - - guard !isExcludingNode else { - return [] - } - - if isIncludingNode || forceInclude { - nodes.append(node) - } - - guard let children = node.children, !children.isEmpty else { - return nodes - } - - return nodes + children.flatMap { child in - return extractNodes( - from: child, - including: &includingNodeIDs, - excluding: &excludingNodeIDs, - forceInclude: forceInclude || isIncludingNode - ) - } - } - - private func resolveNodeID(_ nodeID: String) throws -> String { - guard let unescapedNodeID = nodeID.removingPercentEncoding else { - throw NodesError.invalidNodeID(nodeID) - } - - return unescapedNodeID - } - - private func resolveNodeIDs(_ nodeIDs: [String]?, defaultNodeIDs: Set) throws -> Set { - guard let nodeIDs = nodeIDs, !nodeIDs.isEmpty else { - return defaultNodeIDs - } - - return try Set(nodeIDs.map { try resolveNodeID($0) }) - } -} - -extension DefaultNodesExtractor: NodesExtractor { - - // MARK: - Instance Methods - - func extractNodes( - from file: FigmaFile, - including includingNodeIDs: [String]?, - excluding excludingNodeIDs: [String]? - ) throws -> [FigmaNode] { - var includingNodeIDs = try self.resolveNodeIDs(includingNodeIDs, defaultNodeIDs: [file.document.id]) - var excludingNodeIDs = try self.resolveNodeIDs(excludingNodeIDs, defaultNodeIDs: []) - - return extractNodes( - from: file.document, - including: &includingNodeIDs, - excluding: &excludingNodeIDs, - forceInclude: false - ) - } -} diff --git a/Sources/Services/Nodes/NodesExtractor.swift b/Sources/Services/Nodes/NodesExtractor.swift deleted file mode 100644 index fc53f0f..0000000 --- a/Sources/Services/Nodes/NodesExtractor.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -protocol NodesExtractor { - - // MARK: - Instance Methods - - func extractNodes( - from file: FigmaFile, - including includingNodeIDs: [String]?, - excluding excludingNodeIDs: [String]? - ) throws -> [FigmaNode] -} diff --git a/Sources/Services/Spacings/DefaultSpacingsProvider.swift b/Sources/Services/Spacings/DefaultSpacingsProvider.swift deleted file mode 100644 index 06c2e45..0000000 --- a/Sources/Services/Spacings/DefaultSpacingsProvider.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -final class DefaultSpacingsProvider { - - // MARK: - Instance Properties - - let apiProvider: FigmaAPIProvider - let nodesExtractor: NodesExtractor - - // MARK: - Initializers - - init( - apiProvider: FigmaAPIProvider, - nodesExtractor: NodesExtractor - ) { - self.apiProvider = apiProvider - self.nodesExtractor = nodesExtractor - } - - // MARK: - Instance Methods - - private func extractSpacing(from node: FigmaNode) throws -> Spacing? { - guard case .component(let nodeInfo) = node.type else { - return nil - } - guard !node.name.isEmpty else { - throw SpacingsError.invalidSpacingName(nodeName: node.name, nodeID: node.id) - } - guard let value = nodeInfo.absoluteBoundingBox.height else { - throw SpacingsError.spacingNotFound(nodeName: node.name, nodeID: node.id) - } - return Spacing(name: node.name, value: value) - } - - private func extractSpacings( - from file: FigmaFile, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) throws -> [Spacing] { - return try nodesExtractor - .extractNodes(from: file, including: includingNodeIDs, excluding: excludingNodeIDs) - .lazy - .compactMap { try extractSpacing(from: $0) } - .sorted { $0.name < $1.name } - } -} - -extension DefaultSpacingsProvider: SpacingsProvider { - - // MARK: - Instance Methods - - func fetchSpacings( - fileKey: String, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) -> Promise<[Spacing]> { - return firstly { - self.apiProvider.request(route: FigmaAPIFileRoute(fileKey: fileKey)) - }.map(on: DispatchQueue.global(qos: .userInitiated)) { file in - try self.extractSpacings( - from: file, - includingNodes: includingNodeIDs, - excludingNodes: excludingNodeIDs - ) - } - } -} diff --git a/Sources/Services/Spacings/DefaultSpacingsRenderer.swift b/Sources/Services/Spacings/DefaultSpacingsRenderer.swift deleted file mode 100644 index 78bfdef..0000000 --- a/Sources/Services/Spacings/DefaultSpacingsRenderer.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation -import StencilSwiftKit -import PathKit - -final class DefaultSpacingsRenderer { - - // MARK: - Instance Methods - - private func makeContext(with spacings: [Spacing]) -> [String: Any] { - let spacings = spacings.map { spacing in - return [ - "name": spacing.name, - "value": spacing.value - ] - } - return ["spacings": spacings] - } -} - -extension DefaultSpacingsRenderer: SpacingsRenderer { - - // MARK: - Instance Methods - - func renderTemplate(_ templateType: TemplateType, to destinationPath: String, spacings: [Spacing]) throws { - let templatePath = Path(try templateType.resolvePath()) - let destinationPath = Path(destinationPath) - - let template = try StencilSwiftTemplate( - templateString: templatePath.read(), - environment: stencilSwiftEnvironment() - ) - - let rendered = try template.render(makeContext(with: spacings)) - - try destinationPath.parent().mkpath() - try destinationPath.write(rendered, encoding: .utf8) - } -} diff --git a/Sources/Services/Spacings/SpacingsError.swift b/Sources/Services/Spacings/SpacingsError.swift deleted file mode 100644 index 8fbd914..0000000 --- a/Sources/Services/Spacings/SpacingsError.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation - -enum SpacingsError: Error, CustomStringConvertible { - - // MARK: - Enumeration Cases - - case invalidSpacingName(nodeName: String, nodeID: String) - case spacingNotFound(nodeName: String, nodeID: String) - - // MARK: - Instance Properties - - var description: String { - switch self { - case let .invalidSpacingName(nodeName, nodeID): - return #"Spacing name is empty in node "\#(nodeName)" (\#(nodeID))"# - - case let .spacingNotFound(nodeName, nodeID): - return #"Failed to extract a spacing from in node "\#(nodeName)" (\#(nodeID))"# - } - } -} diff --git a/Sources/Services/Spacings/SpacingsProvider.swift b/Sources/Services/Spacings/SpacingsProvider.swift deleted file mode 100644 index ee340b6..0000000 --- a/Sources/Services/Spacings/SpacingsProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -protocol SpacingsProvider { - - // MARK: - Instance Methods - - func fetchSpacings( - fileKey: String, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) -> Promise<[Spacing]> -} diff --git a/Sources/Services/Spacings/SpacingsRenderer.swift b/Sources/Services/Spacings/SpacingsRenderer.swift deleted file mode 100644 index 1dc9d10..0000000 --- a/Sources/Services/Spacings/SpacingsRenderer.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FigmaGen -// Copyright © 2020 HeadHunter -// MIT Licence -// - -import Foundation - -protocol SpacingsRenderer { - - // MARK: - Instance Methods - - func renderTemplate(_ templateType: TemplateType, to destinationPath: String, spacings: [Spacing]) throws -} diff --git a/Sources/Services/TextStyles/DefaultTextStylesProvider.swift b/Sources/Services/TextStyles/DefaultTextStylesProvider.swift deleted file mode 100644 index 8375519..0000000 --- a/Sources/Services/TextStyles/DefaultTextStylesProvider.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -final class DefaultTextStylesProvider { - - // MARK: - Instance Properties - - let apiProvider: FigmaAPIProvider - let nodesExtractor: NodesExtractor - - // MARK: - Initializers - - init( - apiProvider: FigmaAPIProvider, - nodesExtractor: NodesExtractor - ) { - self.apiProvider = apiProvider - self.nodesExtractor = nodesExtractor - } - - // MARK: - Instance Methods - - private func extractTextColor(from nodeInfo: FigmaVectorNodeInfo, styles: [String: FigmaStyle]) -> Color? { - let nodeStyleName = nodeInfo - .styleID(of: .fill) - .flatMap { styles[$0] }? - .name - - let nodeSingleSolidFill = nodeInfo - .fills - .flatMap { $0.count == 1 ? $0.first : nil } - .flatMap { $0.type == .solid ? $0 : nil } - - guard let nodeFillColor = nodeSingleSolidFill?.color else { - return nil - } - - return Color( - name: nodeStyleName, - red: nodeFillColor.r, - green: nodeFillColor.g, - blue: nodeFillColor.b, - alpha: nodeFillColor.a - ) - } - - private func extractTextStyle(from node: FigmaNode, styles: [String: FigmaStyle]) throws -> TextStyle? { - guard case let .text(info: nodeInfo, payload: textNodePayload) = node.type else { - return nil - } - - guard let nodeStyleID = nodeInfo.styleID(of: .text) else { - return nil - } - - guard let nodeStyle = styles[nodeStyleID], nodeStyle.type == .text else { - throw TextStylesError.styleNotFound(nodeName: node.name, nodeID: node.id) - } - - guard let nodeStyleName = nodeStyle.name, !nodeStyleName.isEmpty else { - throw TextStylesError.invalidStyleName(nodeName: node.name, nodeID: node.id) - } - - guard let nodeTextStyle = textNodePayload.style else { - throw TextStylesError.textStyleNotFound(nodeName: node.name, nodeID: node.id) - } - - guard let fontFamily = nodeTextStyle.fontFamily, !fontFamily.isEmpty else { - throw TextStylesError.invalidFontFamily(nodeName: node.name, nodeID: node.id) - } - - guard let fontPostScriptName = nodeTextStyle.fontPostScriptName, !fontPostScriptName.isEmpty else { - throw TextStylesError.invalidFontName(nodeName: node.name, nodeID: node.id) - } - - guard let fontWeight = nodeTextStyle.fontWeight else { - throw TextStylesError.invalidFontWeight(nodeName: node.name, nodeID: node.id) - } - - guard let fontSize = nodeTextStyle.fontSize else { - throw TextStylesError.invalidFontSize(nodeName: node.name, nodeID: node.id) - } - - guard let textColor = extractTextColor(from: nodeInfo, styles: styles) else { - throw TextStylesError.invalidTextColor(nodeName: node.name, nodeID: node.id) - } - - return TextStyle( - name: nodeStyleName, - fontFamily: fontFamily, - fontPostScriptName: fontPostScriptName, - fontWeight: fontWeight, - fontSize: fontSize, - textColor: textColor, - paragraphSpacing: nodeTextStyle.paragraphSpacing, - paragraphIndent: nodeTextStyle.paragraphIndent, - lineHeight: nodeTextStyle.lineHeight, - letterSpacing: nodeTextStyle.letterSpacing - ) - } - - private func extractTextStyles( - from file: FigmaFile, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) throws -> [TextStyle] { - return try nodesExtractor - .extractNodes(from: file, including: includingNodeIDs, excluding: excludingNodeIDs) - .lazy - .compactMap { try extractTextStyle(from: $0, styles: file.styles ?? [:]) } - .reduce(into: []) { result, textStyle in - if !result.contains(textStyle) { - result.append(textStyle) - } - } - } -} - -extension DefaultTextStylesProvider: TextStylesProvider { - - // MARK: - Instance Methods - - func fetchTextStyles( - fileKey: String, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) -> Promise<[TextStyle]> { - return firstly { - self.apiProvider.request(route: FigmaAPIFileRoute(fileKey: fileKey)) - }.map(on: DispatchQueue.global(qos: .userInitiated)) { file in - try self.extractTextStyles( - from: file, - includingNodes: includingNodeIDs, - excludingNodes: excludingNodeIDs - ) - } - } -} diff --git a/Sources/Services/TextStyles/DefaultTextStylesRenderer.swift b/Sources/Services/TextStyles/DefaultTextStylesRenderer.swift deleted file mode 100644 index f79c11d..0000000 --- a/Sources/Services/TextStyles/DefaultTextStylesRenderer.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import StencilSwiftKit -import PathKit - -final class DefaultTextStylesRenderer { - - // MARK: - Instance Methods - - private func mapColorComponent(_ colorComponent: Double) -> String { - return String(format: "%02lX", Int(colorComponent * 255.0)) - } - - private func mapColor(_ color: Color) -> [String: Any] { - var dictionary: [String: Any] = [ - "red": mapColorComponent(color.red), - "green": mapColorComponent(color.green), - "blue": mapColorComponent(color.blue), - "alpha": mapColorComponent(color.alpha) - ] - - if let colorName = color.name { - dictionary["name"] = colorName - } - - return dictionary - } - - private func mapTextStyle(_ textStyle: TextStyle) -> [String: Any] { - var dictionary: [String: Any] = [ - "name": textStyle.name, - "fontFamily": textStyle.fontFamily, - "fontPostScriptName": textStyle.fontPostScriptName, - "fontWeight": "\(textStyle.fontWeight)", - "fontSize": "\(textStyle.fontSize)", - "textColor": mapColor(textStyle.textColor) - ] - - if let paragraphSpacing = textStyle.paragraphSpacing { - dictionary["paragraphSpacing"] = "\(paragraphSpacing.rounded(precision: 2))" - } - - if let paragraphIndent = textStyle.paragraphIndent { - dictionary["paragraphIndent"] = "\(paragraphIndent.rounded(precision: 2))" - } - - if let lineHeight = textStyle.lineHeight { - dictionary["lineHeight"] = "\(lineHeight.rounded(precision: 2))" - } - - if let letterSpacing = textStyle.letterSpacing { - dictionary["letterSpacing"] = "\(letterSpacing.rounded(precision: 2))" - } - - return dictionary - } - - private func makeContext(with textStyles: [TextStyle]) -> [String: Any] { - return ["textStyles": textStyles.map(mapTextStyle)] - } -} - -extension DefaultTextStylesRenderer: TextStylesRenderer { - - // MARK: - Instance Methods - - func renderTemplate(_ templateType: TemplateType, to destinationPath: String, textStyles: [TextStyle]) throws { - let templatePath = Path(try templateType.resolvePath()) - let destinationPath = Path(destinationPath) - - let template = try StencilSwiftTemplate( - templateString: templatePath.read(), - environment: stencilSwiftEnvironment() - ) - - let output = try template.render(makeContext(with: textStyles)) - - try destinationPath.parent().mkpath() - try destinationPath.write(output) - } -} diff --git a/Sources/Services/TextStyles/TextStylesError.swift b/Sources/Services/TextStyles/TextStylesError.swift deleted file mode 100644 index 445c38a..0000000 --- a/Sources/Services/TextStyles/TextStylesError.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -enum TextStylesError: Error { - - // MARK: - Enumeration Cases - - case styleNotFound(nodeName: String, nodeID: String) - case invalidStyleName(nodeName: String, nodeID: String) - - case textStyleNotFound(nodeName: String, nodeID: String) - - case invalidFontFamily(nodeName: String, nodeID: String) - case invalidFontName(nodeName: String, nodeID: String) - case invalidFontWeight(nodeName: String, nodeID: String) - case invalidFontSize(nodeName: String, nodeID: String) - case invalidTextColor(nodeName: String, nodeID: String) - - // MARK: - Instance Properties - - var description: String { - switch self { - case let .styleNotFound(nodeName, nodeID): - return #"Figma file does not contain a style for node "\#(nodeName)" (\#(nodeID))"# - - case let .invalidStyleName(nodeName, nodeID): - return #"Style name of node "\#(nodeName)" (\#(nodeID)) is either empty or nil"# - - case let .textStyleNotFound(nodeName, nodeID): - return #"Failed to extract a text style of node "\#(nodeName)" (\#(nodeID))"# - - case let .invalidFontFamily(nodeName, nodeID): - return #"Style font family of node "\#(nodeName)" (\#(nodeID)) is either empty or nil"# - - case let .invalidFontName(nodeName, nodeID): - return #"Style font name of node "\#(nodeName)" (\#(nodeID)) is either empty or nil"# - - case let .invalidFontWeight(nodeName, nodeID): - return #"Style font weight of node "\#(nodeName)" (\#(nodeID)) is nil"# - - case let .invalidFontSize(nodeName, nodeID): - return #"Style font size of node "\#(nodeName)" (\#(nodeID)) is nil"# - - case let .invalidTextColor(nodeName, nodeID): - return "Text color of node \(nodeName) ('\(nodeID)') cannot be resolved" - } - } -} diff --git a/Sources/Services/TextStyles/TextStylesProvider.swift b/Sources/Services/TextStyles/TextStylesProvider.swift deleted file mode 100644 index a96326f..0000000 --- a/Sources/Services/TextStyles/TextStylesProvider.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PromiseKit - -protocol TextStylesProvider { - - // MARK: - Instance Methods - - func fetchTextStyles( - fileKey: String, - includingNodes includingNodeIDs: [String]?, - excludingNodes excludingNodeIDs: [String]? - ) -> Promise<[TextStyle]> -} diff --git a/Sources/Services/TextStyles/TextStylesRenderer.swift b/Sources/Services/TextStyles/TextStylesRenderer.swift deleted file mode 100644 index 3c025dc..0000000 --- a/Sources/Services/TextStyles/TextStylesRenderer.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -protocol TextStylesRenderer { - - // MARK: - Instance Methods - - func renderTemplate(_ templateType: TemplateType, to destinationPath: String, textStyles: [TextStyle]) throws -} diff --git a/Sources/Tools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift b/Sources/Tools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift deleted file mode 100644 index 249259d..0000000 --- a/Sources/Tools/Extensions/KeyedDecodingContainerProtocol+Extensions.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -extension KeyedDecodingContainerProtocol { - - // MARK: - Instance Methods - - func decode(forKey key: Key) throws -> T { - return try decode(T.self, forKey: key) - } - - func decodeIfPresent(forKey key: Self.Key) throws -> T? { - return try decodeIfPresent(T.self, forKey: key) - } -} diff --git a/Sources/Tools/Extensions/Path+Extensions.swift b/Sources/Tools/Extensions/Path+Extensions.swift deleted file mode 100644 index 8a662df..0000000 --- a/Sources/Tools/Extensions/Path+Extensions.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import PathKit - -extension Path { - - // MARK: - Instance Methods - - func appending(_ path: Path) -> Path { - return self + path - } - - func appending(_ path: String) -> Path { - return self + path - } -} diff --git a/Sources/Tools/Extensions/ProcessInfo+Extensions.swift b/Sources/Tools/Extensions/ProcessInfo+Extensions.swift deleted file mode 100644 index ac796e9..0000000 --- a/Sources/Tools/Extensions/ProcessInfo+Extensions.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -extension ProcessInfo { - - // MARK: - Instance Properties - - var executablePath: String { - return arguments[0] - } -} diff --git a/Sources/Tools/Extensions/Routable+Extensions.swift b/Sources/Tools/Extensions/Routable+Extensions.swift deleted file mode 100644 index 2a75fa6..0000000 --- a/Sources/Tools/Extensions/Routable+Extensions.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import SwiftCLI - -#if canImport(RainbowSwift) -import RainbowSwift -#else -import Rainbow -#endif - -extension Routable { - - // MARK: - Instance Methods - - func fail(message: String) -> Never { - stderr <<< message.red - - exit(EXIT_FAILURE) - } - - func fail(error: Error) -> Never { - fail(message: "Failed with error: \(error)") - } - - func success(message: String? = nil) -> Never { - if let message = message { - stdout <<< message.green - } - - exit(EXIT_SUCCESS) - } -} diff --git a/Sources/Tools/Extensions/Sequence+Extensions.swift b/Sources/Tools/Extensions/Sequence+Extensions.swift deleted file mode 100644 index 2084e98..0000000 --- a/Sources/Tools/Extensions/Sequence+Extensions.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -extension Sequence { - - // MARK: - Instance Properties - - var lazyFirst: Element? { - return first { _ in true } - } -} diff --git a/Sources/Tools/Extensions/SingleValueDecodingContainer+Extensions.swift b/Sources/Tools/Extensions/SingleValueDecodingContainer+Extensions.swift deleted file mode 100644 index 8d93bca..0000000 --- a/Sources/Tools/Extensions/SingleValueDecodingContainer+Extensions.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -extension SingleValueDecodingContainer { - - // MARK: - Instance Methods - - func decode() throws -> T { - return try decode(T.self) - } -} diff --git a/Sources/Tools/Extensions/UnkeyedDecodingContainer+Extensions.swift b/Sources/Tools/Extensions/UnkeyedDecodingContainer+Extensions.swift deleted file mode 100644 index aa171a1..0000000 --- a/Sources/Tools/Extensions/UnkeyedDecodingContainer+Extensions.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation - -extension UnkeyedDecodingContainer { - - // MARK: - Instance Methods - - mutating func decode() throws -> T { - return try decode(T.self) - } - - mutating func decodeIfPresent() throws -> T? { - return try decodeIfPresent(T.self) - } -} diff --git a/Sources/main.swift b/Sources/main.swift deleted file mode 100755 index 9814107..0000000 --- a/Sources/main.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - -import Foundation -import SwiftCLI - -let services = Services() - -let figmaGen = CLI( - name: "figmagen", - version: "1.0.0", - description: "A tool to automate resources using the Figma API." -) - -figmaGen.commands = [ - ColorsCommand(services: services), - TextStylesCommand(services: services), - SpacingsCommand(services: services), - GenerateCommand(services: services) -] - -figmaGen.goAndExitOnError() diff --git a/Templates/AnimationEaseTokens.stencil b/Templates/AnimationEaseTokens.stencil new file mode 100644 index 0000000..afae398 --- /dev/null +++ b/Templates/AnimationEaseTokens.stencil @@ -0,0 +1,70 @@ +{% include "FileHeader.stencil" %} +{% if semanticAnimationEaseBase or semanticAnimationEaseSpring or coreAnimationEaseBase or coreAnimationEaseSpring %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro makeBase baseAnimations %} +{% for animation in baseAnimations %} + public let {% call propertyName animation.path.last %} = AnimationEaseBase( + x1: {{ animation.x1 }}, + y1: {{ animation.y1 }}, + x2: {{ animation.x2 }}, + y2: {{ animation.y2 }} + ) +{% endfor %} +{% endmacro %} +{% macro makeSpring springAnimations %} +{% for animation in springAnimations %} + public let {% call propertyName animation.path.last %} = AnimationEaseSpring( + stiffness: {{ animation.stiffness }}, + damping: {{ animation.damping }}, + mass: {{ animation.mass }} + ) +{% endfor %} +{% endmacro %} + +import Foundation + +public struct AnimationEase: Sendable { + {% if semanticAnimationEaseBase or semanticAnimationEaseSpring %} + + // MARK: - Semantic + {% if semanticAnimationEaseBase %} + + {% call makeBase semanticAnimationEaseBase %} + {% endif %} + {% if semanticAnimationEaseSpring %} + + {% call makeSpring semanticAnimationEaseSpring %} + {% endif %} + {% endif %} + + {% if coreAnimationEaseBase or coreAnimationEaseSpring %} + + // MARK: - Core + {% if coreAnimationEaseBase %} + + {% call makeBase coreAnimationEaseBase %} + {% endif %} + {% if coreAnimationEaseSpring %} + + {% call makeSpring coreAnimationEaseSpring %} + {% endif %} + {% endif %} +} + +struct AnimationEaseBase { + let path: [String] + let x1: String + let y1: String + let x2: String + let y2: String +} + +struct AnimationEaseSpring { + let path: [String] + let stiffness: String + let damping: String + let mass: String +} +{% else %} +// No animation ease tokens found +{% endif %} \ No newline at end of file diff --git a/Templates/AnimationTimeTokens.stencil b/Templates/AnimationTimeTokens.stencil new file mode 100644 index 0000000..6e26be8 --- /dev/null +++ b/Templates/AnimationTimeTokens.stencil @@ -0,0 +1,28 @@ +{% include "FileHeader.stencil" %} +{% if semanticAnimationTimes or coreAnimationTimes %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} + +import Foundation + +public struct MagritteAnimationTimes: Sendable { + + {% if semanticAnimationTimes %} + // MARK: - semantic + {% for time in semanticAnimationTimes %} + + public let {% call propertyName time.path.last %}: Double = {{ time.duration}} + {% endfor %} + {% endif %} + + {% if coreAnimationTimes %} + + // MARK: - core + {% for time in coreAnimationTimes %} + + public let {% call propertyName time.path.last %}: Double = {{ time.duration}} + {% endfor %} + {% endif %} +} +{% else %} +// No animation time tokens found +{% endif %} \ No newline at end of file diff --git a/Templates/BaseColorTokens.stencil b/Templates/BaseColorTokens.stencil new file mode 100644 index 0000000..58720db --- /dev/null +++ b/Templates/BaseColorTokens.stencil @@ -0,0 +1,39 @@ +{% include "FileHeader.stencil" %} +{% if colors %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"BaseColorTokens" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ tokenTypeName }} { + {% for color in colors %} + /// {{ color.value }} + {{ accessModifier }} let {% call propertyName color.path|removingFirst:"color"|removingFirst:"base"|join:"." %} = {{ colorTypeName }}(hex: 0x{{ color.value|fullHex|replace:"#","" }}) + {% endfor %} +} + +private extension {{ colorTypeName }} { + + convenience init(hex: UInt32) { + let red = UInt8((hex >> 24) & 0xFF) + let green = UInt8((hex >> 16) & 0xFF) + let blue = UInt8((hex >> 8) & 0xFF) + let alpha = UInt8(hex & 0xFF) + + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: CGFloat(alpha) / 255.0 + ) + } +} +{% else %} +// No base color tokens found +{% endif %} \ No newline at end of file diff --git a/Templates/BorderRadiusTokens.stencil b/Templates/BorderRadiusTokens.stencil new file mode 100644 index 0000000..a6d3995 --- /dev/null +++ b/Templates/BorderRadiusTokens.stencil @@ -0,0 +1,46 @@ +{% include "FileHeader.stencil" %} +{% if semanticBorderRadius or coreBorderRadius or staticBorderRadius -%} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"BorderRadiusTokens" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro borderRadiusProperty borderRadius -%} + {{ accessModifier }} let {% call propertyName borderRadius.path|removingFirst:"static"|removingFirst:"semantic"|removingFirst:"core"|join:"_" %}: Double = {{ borderRadius.value }} +{%- endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ tokenTypeName }} { + + // MARK: - Instance Properties + {% if semanticBorderRadius %} + + // Semantic + + {% for borderRadius in semanticBorderRadius %} + {% call borderRadiusProperty borderRadius %} + {% endfor %} + {% endif %} + {% if staticBorderRadius %} + + // Static + + {% for borderRadius in staticBorderRadius %} + {% call borderRadiusProperty borderRadius %} + {% endfor %} + {% endif %} + {% if coreBorderRadius %} + + // Core + + {% for borderRadius in coreBorderRadius %} + {% call borderRadiusProperty borderRadius %} + {% endfor %} + {% endif %} +} +{%- else -%} +// No border tokens found +{%- endif %} diff --git a/Templates/BorderTokens.stencil b/Templates/BorderTokens.stencil new file mode 100644 index 0000000..40c2203 --- /dev/null +++ b/Templates/BorderTokens.stencil @@ -0,0 +1,49 @@ +{% include "FileHeader.stencil" %} +{% if semanticBorders or coreBorders -%} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"BorderTokens" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro borderProperty border -%} + /// {{ border.path|join:"." }} + /// + /// Width: {{ border.width }} + /// Style: {{ border.style|default:"solid" }} + {{ accessModifier }} var {% call propertyName border.path|join:"_" %}: BorderToken { + BorderToken( + width: {{ border.width }}, + style: "{{ border.style|default:"solid" }}" + ) + } +{%- endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +public struct BorderToken: Hashable { + public let width: CGFloat + public let style: String + + public init(width: CGFloat, style: String) { + self.width = width + self.style = style + } +} + +{{ accessModifier }} struct {{ tokenTypeName }} { + + // MARK: - Instance Properties + {% for border in semanticBorders %} + + {% call borderProperty border %} + {% endfor %} + {% for border in coreBorders %} + + {% call borderProperty border %} + {% endfor %} +} +{%- else -%} +// No border tokens found +{%- endif %} diff --git a/Templates/BoxShadowTokens.stencil b/Templates/BoxShadowTokens.stencil new file mode 100644 index 0000000..4f02db5 --- /dev/null +++ b/Templates/BoxShadowTokens.stencil @@ -0,0 +1,99 @@ +{% include "FileHeader.stencil" %} +{% if dictThemedBoxShadows[fallbackTheme] %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"BoxShadowTokens" }}{% endset %} +{% set shadowTypeName %}{{ options.shadowTypeName|default:"Shadow" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set viewTypeName %}{{ options.viewTypeName|default:"UIView" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% set shadowPropertyName %}{% call propertyName shadowTypeName %}{% endset %} +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ tokenTypeName }} { + {% outer: for boxShadow in dictThemedBoxShadows[fallbackTheme] %} + + {% for themedValue in themedBoxShadows %} + {% for shadowToken in themedValue.value %} + {% if forloop.outer.counter == forloop.counter %} + /// - {{ themedValue.themeName }}: + /// Offset: x {{ shadowToken.x }}; y {{ shadowToken.y }} + /// Radius: {{ shadowToken.blur }} + /// Color: {{ shadowToken.color }} + {% endif %} + {% endfor %} + {% endfor %} + {{ accessModifier }} let {% call propertyName boxShadow.path.last %}: {{ shadowTypeName }} + {% endfor %} +} + +{{ accessModifier }} struct {{ shadowTypeName }}: Equatable { + + // MARK: - Instance Properties + + {{ accessModifier }} let offset: CGSize + {{ accessModifier }} let radius: CGFloat + {{ accessModifier }} let color: {{ colorTypeName }}? + {{ accessModifier }} let opacity: Float + + // MARK: - Initializers + + {{ accessModifier }} init( + offset: CGSize = CGSize(width: 0, height: -3), + radius: CGFloat = 3.0, + color: {{ colorTypeName }}? = .black, + opacity: Float = 0.0 + ) { + self.offset = offset + self.radius = radius + self.color = color + self.opacity = opacity + } +} + +{{ accessModifier }} extension CALayer { + + // MARK: - Instance Properties + + var {{ shadowPropertyName }}: {{ shadowTypeName }} { + get { + {{ shadowTypeName }}( + offset: shadowOffset, + radius: shadowRadius, + color: shadowColor.map({{ colorTypeName }}.init(cgColor:)), + opacity: shadowOpacity + ) + } + + set { + shadowOffset = newValue.offset + shadowRadius = newValue.radius + shadowColor = newValue.color?.cgColor + shadowOpacity = newValue.opacity + } + } + + // MARK: - Initializers + + convenience init({{ shadowPropertyName }}: {{ shadowTypeName }}) { + self.init() + + self.{{ shadowPropertyName }} = {{ shadowPropertyName }} + } +} + +{{ accessModifier }} extension {{ viewTypeName }} { + + // MARK: - Instance Properties + + var {{ shadowPropertyName }}: {{ shadowTypeName }} { + get { layer.{{ shadowPropertyName }} } + set { layer.{{ shadowPropertyName }} = newValue } + } +} +{% else %} +// No box shadow tokens found +{% endif %} diff --git a/Templates/ColorStyles.stencil b/Templates/ColorStyles.stencil new file mode 100644 index 0000000..a2c12b8 --- /dev/null +++ b/Templates/ColorStyles.stencil @@ -0,0 +1,82 @@ +{% include "FileHeader.stencil" %} +{% if colorStyles %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set styleTypeName %}{{ options.styleTypeName|default:"ColorStyle" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro styleMutator propertyName propertyTypeName %} + {% set methodName %}with{{ propertyName|upperFirstLetter }}{% endset %} + {{ accessModifier }} func {{ methodName }}(_ {{propertyName}}: {{ propertyTypeName }}) -> {{ styleTypeName }} { + return {{ styleTypeName }}( + red: red, + green: green, + blue: blue, + alpha: alpha + ) + } +{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ styleTypeName }}: Equatable { + + // MARK: - Type Properties +{% for style in colorStyles %} + + /// {{ style.name }} + /// + /// {{ style.color|colorInfo }} + {{ accessModifier }} static let {% call propertyName style.name %} = {{ styleTypeName }}( + red: {{ style.color.red }}, + green: {{ style.color.green }}, + blue: {{ style.color.blue }}, + alpha: {{ style.color.alpha }} + ) +{% endfor %} + + // MARK: - Instance Properties + + {{ accessModifier }} let red: CGFloat + {{ accessModifier }} let green: CGFloat + {{ accessModifier }} let blue: CGFloat + {{ accessModifier }} let alpha: CGFloat + + {{ accessModifier }} var color: {{ colorTypeName }} { + return {{ colorTypeName }}(style: self) + } + + // MARK: - Initializers + + {{ accessModifier }} init(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat = 1.0) { + self.red = red + self.green = green + self.blue = blue + self.alpha = alpha + } + + // MARK: - Instance Methods + + {% call styleMutator "red" "CGFloat" %} + + {% call styleMutator "green" "CGFloat" %} + + {% call styleMutator "blue" "CGFloat" %} + + {% call styleMutator "alpha" "CGFloat" %} +} + +{{ accessModifier }} extension {{ colorTypeName }} { + + // MARK: - Initializers + + convenience init(style: {{ styleTypeName }}) { + self.init(red: style.red, green: style.green, blue: style.blue, alpha: style.alpha) + } +} +{% else %} +// No color style found +{% endif %} diff --git a/Templates/ColorTokens.stencil b/Templates/ColorTokens.stencil new file mode 100644 index 0000000..f947781 --- /dev/null +++ b/Templates/ColorTokens.stencil @@ -0,0 +1,39 @@ +{% include "FileHeader.stencil" %} +{% if dictThemedColors[fallbackTheme] %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"ColorTokens" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro recursiveBlock item %} + {% for color in item.colors %} + /// {{ color.name }} + /// + {% for theme, token in dictThemedColors|findTokenInThemes:color.name,"colors" %} + /// {{ theme }}: {{ token.value }} + {% endfor %} + {{ accessModifier }} let {% call propertyName color.path.last %}: {{ colorTypeName }} + {% endfor %} + {% for child in item.children %} + {{ accessModifier }} struct {% call typeName child.name %} { + {% filter indent:4 %} + {% call recursiveBlock child %} + {% endfilter %} + } + + {{ accessModifier }} let {% call propertyName child.name %}: {% call typeName child.name %} + {% endfor %} +{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ tokenTypeName }} { + {% call recursiveBlock dictThemedColors[fallbackTheme] %} +} +{% else %} +// No color tokens found +{% endif %} diff --git a/Templates/Colors.stencil b/Templates/Colors.stencil deleted file mode 100644 index 8116df8..0000000 --- a/Templates/Colors.stencil +++ /dev/null @@ -1,35 +0,0 @@ -// swiftlint:disable all -{% if colors %} -{% macro rgbaHex color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %} -{% macro rgbHexString color %}#{{color.red}}{{color.green}}{{color.blue}}{% endmacro %} -{% macro rgbaHexString color %}{% call rgbHexString color %}{{color.alpha}}{% endmacro %} -{% macro rgbString color %}{{color.red|hexToInt}} {{color.green|hexToInt}} {{color.blue|hexToInt}}{% endmacro %} -{% macro rgbaString color %}{% call rgbString color %}, {{color.alpha|hexToInt|int255toFloat|percent}}{% endmacro %} -{% macro colorName color %}{{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endmacro %} -import UIKit.UIColor - -public enum Colors { -{% for color in colors %} - - /// {{color.name}} - /// - /// Hex: {% call rgbaHexString color %}; rgba: {% call rgbaString color %}. - public static let {% call colorName color %} = UIColor(rgbaHex: {% call rgbaHex color %}) -{% endfor %} -} - -private extension UIColor { - - convenience init(rgbaHex: UInt32) { - self.init( - red: CGFloat((rgbaHex >> 24) & 0xFF) / 255.0, - green: CGFloat((rgbaHex >> 16) & 0xFF) / 255.0, - blue: CGFloat((rgbaHex >> 8) & 0xFF) / 255.0, - alpha: CGFloat(rgbaHex & 0xFF) / 255.0 - ) - } -} -{% else %} -// No color found -{% endif %} -// swiftlint:enable all diff --git a/Templates/FileHeader.stencil b/Templates/FileHeader.stencil new file mode 100644 index 0000000..890c6c8 --- /dev/null +++ b/Templates/FileHeader.stencil @@ -0,0 +1,2 @@ +// swiftlint:disable all +// Generated using FigmaGen - https://github.com/hhru/FigmaGen diff --git a/Templates/FontFamilyTokens.stencil b/Templates/FontFamilyTokens.stencil new file mode 100644 index 0000000..de6a249 --- /dev/null +++ b/Templates/FontFamilyTokens.stencil @@ -0,0 +1,23 @@ +{% include "FileHeader.stencil" %} +{% if fontFamilies %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"FontFamilyTokens" }}{% endset %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{{ accessModifier }} struct {{ tokenTypeName }} { + + {% for fontFamily in fontFamilies %} + {{ accessModifier }} struct {% call typeName fontFamily.path.last %} { + {% for fontWeight in fontWeights %} + {{ accessModifier }} let {% call propertyName fontWeight.path.last %} = "{{ fontFamily.value }}-{{ fontWeight.value|replace:" ","" }}" + {% endfor %} + } + + {% endfor %} + {% for fontFamily in fontFamilies %} + public let {% call propertyName fontFamily.path.last %} = {% call typeName fontFamily.path.last %}() + {% endfor %} +} +{% else %} +// No font family tokens found +{% endif %} \ No newline at end of file diff --git a/Templates/GradientTokens.stencil b/Templates/GradientTokens.stencil new file mode 100644 index 0000000..7f25690 --- /dev/null +++ b/Templates/GradientTokens.stencil @@ -0,0 +1,41 @@ +{% include "FileHeader.stencil" %} +{% if dictThemedGradients[fallbackTheme] %} +{% set gradientTypeName %}{{ options.colorTypeName|default:"LinearGradient" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro recursiveBlock item %} + {% for gradient in item.gradients %} + + /// {{ gradient.name }} + /// + {% for theme, token in dictThemedGradients|findTokenInThemes:gradient.name,"gradients" %} + /// {{ theme }}: + {% for stop in token.stops %} + /// - {{ stop.color }} {{ stop.percentage }} + {% endfor %} + {% endfor %} + public let {% call propertyName gradient.path.last %}: {{ gradientTypeName }} + {% endfor %} + {% for child in item.children %} + {% if child.name == "gradient" %} + {% call recursiveBlock child %} + {% else %} + public struct {% call typeName child.name %} { + {% filter indent:4 %} + {% call recursiveBlock child %} + {% endfilter %} + } + + public let {% call propertyName child.name %}: {% call typeName child.name %} + {% endif %} + {% endfor %} +{% endmacro %} + +import SwiftUI + +public struct Gradients { + {% call recursiveBlock dictThemedGradients[fallbackTheme] %} +} +{% else %} +// No color tokens found +{% endif %} diff --git a/Templates/Images.stencil b/Templates/Images.stencil new file mode 100644 index 0000000..4062388 --- /dev/null +++ b/Templates/Images.stencil @@ -0,0 +1,94 @@ +{% include "FileHeader.stencil" %} +{% if imageSets %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set imagesEnumName %}{{ options.imagesEnumName|default:"Images" }}{% endset %} +{% set imageTypeName %}{{ options.imageTypeName|default:"UIImage" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro assetImage asset %}{{ imageTypeName }}(named: "{{ asset.name }}"){% endmacro %} +{% macro resourceFileName resource %}{{ resource.fileName }}.{{ resource.fileExtension }}{% endmacro %} +{% macro resourceImage resource %}{{ imageTypeName }}(named: "{% call resourceFileName resource %}"){% endmacro %} +{% macro imageProperties image %} +{% if image.asset %} + /// {{ image.name }} + /// + /// Asset: {{ image.asset.name }} + {{ accessModifier }} static var {% call propertyName image.name %}: {{ imageTypeName }} { + return {% call assetImage image.asset %}! + } +{% elif image.resource %} + /// {{ image.name }} + /// + /// Resource: {% call resourceFileName image.resource %} + {{ accessModifier }} static var {% call propertyName image.name %}: {{ imageTypeName }} { + return {% call resourceImage image.resource %}! + } +{% endif %} +{% endmacro %} +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} enum {{ imagesEnumName }} { + + // MARK: - Nested Types + + {{ accessModifier }} enum ValidationError: Error, CustomStringConvertible { + case assetNotFound(name: String) + case resourceNotFound(name: String) + + {{ accessModifier }} var description: String { + switch self { + case let .assetNotFound(name): + return "Image asset '\(name)' couldn't be loaded" + + case let .resourceNotFound(name): + return "Image resource file '\(name)' couldn't be loaded" + } + } + } +{% for set in imageSets where set.images.count > 1 %} + + {{ accessModifier }} enum {% call typeName set.name %} { + {% for image in set.images %} + + {% filter indent:4 %} + {% call imageProperties image %} + {% endfilter %} + {% endfor %} + } +{% endfor %} +{% for set in imageSets where set.images.count == 1 %} +{% if forloop.first %} + + // MARK: - Type Properties +{% endif%} + +{% call imageProperties set.images.0 %} +{% endfor %} + + // MARK: - Type Methods + + {{ accessModifier }} static func validate() throws { + {% for set in imageSets %} + {% for image in set.images %} + {% if image.asset %} + guard {% call assetImage image.asset %} != nil else { + throw ValidationError.assetNotFound(name: "{{ image.asset.name }}") + } + {% elif image.resource %} + guard {% call resourceImage image.resource %} != nil else { + throw ValidationError.resourceNotFound(name: "{% call resourceFileName image.resource %}") + } + {% endif %} + + {% endfor %} + {% endfor %} + print("All images are valid") + } +} +{% else %} +// No images found +{% endif %} diff --git a/Templates/ShadowStyles.stencil b/Templates/ShadowStyles.stencil new file mode 100644 index 0000000..aaee3a4 --- /dev/null +++ b/Templates/ShadowStyles.stencil @@ -0,0 +1,312 @@ +{% include "FileHeader.stencil" %} +{% if shadowStyles %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set classAccessModifier %}{% if options.publicAccess %}open{% else %}internal{% endif %}{% endset %} +{% set styleTypeName %}{{ options.styleTypeName|default:"ShadowStyle" }}{% endset %} +{% set shadowTypeName %}{{ options.shadowTypeName|default:"Shadow" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set viewTypeName %}{{ options.viewTypeName|default:"UIView" }}{% endset %} +{% set bezierPathTypeName %}{{ options.bezierPathTypeName|default:"UIBezierPath" }}{% endset %} +{% set shadowStyleLayerTypeName %}{{ options.shadowStyleLayerTypeName|default:"ShadowStyleLayer" }}{% endset %} +{% set shadowStyleViewTypeName %}{{ options.shadowStyleViewTypeName|default:"ShadowStyleView" }}{% endset %} +{% macro shadowName style index %}{{ style.name }} {% if style.shadows.count > 1 %}{{ index }}{% endif %}{% endmacro %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ shadowTypeName }}: Equatable { + + // MARK: - Type Properties + + {{ accessModifier }} static let clear = {{ shadowTypeName }}() +{% for style in shadowStyles %} +{% for shadow in style.shadows where shadow.type == "drop" %} + + {% set shadowName %}{% call shadowName style forloop.counter %}{% endset %} + /// {{shadowName}} + /// + /// Offset: {{ shadow.offset|vectorInfo }} + /// Radius: {{ shadow.radius }} + /// Color: {{ shadow.color|colorInfo|lowerFirstWord }} + /// Opacity: {{ shadow.color.alpha }} + {{ accessModifier }} static let {% call propertyName shadowName %} = {{ shadowTypeName }}( + offset: CGSize(width: {{shadow.offset.x}}, height: {{shadow.offset.y}}), + radius: {{shadow.radius}}, + color: {{ colorTypeName }}( + red: {{ shadow.color.red }}, + green: {{ shadow.color.green }}, + blue: {{ shadow.color.blue }}, + alpha: 1.0 + ), + opacity: {{ shadow.color.alpha }} + ) +{% endfor %} +{% endfor %} + + // MARK: - Instance Properties + + {{ accessModifier }} let offset: CGSize + {{ accessModifier }} let radius: CGFloat + {{ accessModifier }} let color: {{ colorTypeName }}? + {{ accessModifier }} let opacity: Float + + // MARK: - Initializers + + {{ accessModifier }} init( + offset: CGSize = CGSize(width: 0, height: -3), + radius: CGFloat = 3.0, + color: {{ colorTypeName }}? = .black, + opacity: Float = 0.0 + ) { + self.offset = offset + self.radius = radius + self.color = color + self.opacity = opacity + } +} + +{{ accessModifier }} struct {{ styleTypeName }} { + + // MARK: - Type Properties + + {{ accessModifier }} static let clear = {{ styleTypeName }}() + {% for style in shadowStyles %} + + /// {{ style.name }} + {{ accessModifier }} static let {% call propertyName style.name %} = {{ styleTypeName }}( + shadows: [ + {% for shadow in style.shadows where shadow.type == "drop" %} + {% set shadowName %}{% call shadowName style forloop.counter %}{% endset %} + {% if forloop.last %} + .{% call propertyName shadowName %} + {% else %} + .{% call propertyName shadowName %}, + {% endif %} + {% endfor %} + ] + ) + {% endfor %} + + // MARK: - Instance Properties + + {{ accessModifier }} let shadows: [{{ shadowTypeName }}] + + // MARK: - Initializers + + {{ accessModifier }} init(shadows: [{{ shadowTypeName }}] = []) { + self.shadows = shadows + } +} + +{{ accessModifier }} extension CALayer { + + // MARK: - Instance Properties + + var shadow: Shadow { + get { + Shadow( + offset: shadowOffset, + radius: shadowRadius, + color: shadowColor.map({{ colorTypeName }}.init(cgColor:)), + opacity: shadowOpacity + ) + } + + set { + shadowOffset = newValue.offset + shadowRadius = newValue.radius + shadowColor = newValue.color?.cgColor + shadowOpacity = newValue.opacity + } + } + + // MARK: - Initializers + + convenience init(shadow: Shadow) { + self.init() + + self.shadow = shadow + } +} + +{{ accessModifier }} extension {{ viewTypeName }} { + + // MARK: - Instance Properties + + var shadow: {{ shadowTypeName }} { + get { layer.shadow } + set { layer.shadow = newValue } + } +} + +private extension {{ bezierPathTypeName }} { + + // MARK: - Initializers + + convenience init( + roundedRect rect: CGRect, + byRoundingCorners layerCorners: CACornerMask, + cornerRadii: CGSize + ) { + #if canImport(UIKit) + let cornerMaskMap: KeyValuePairs = [ + .layerMinXMinYCorner: .topLeft, + .layerMinXMaxYCorner: .bottomLeft, + .layerMaxXMinYCorner: .topRight, + .layerMaxXMaxYCorner: .bottomRight + ] + + let rectCorners = cornerMaskMap + .lazy + .filter { layerCorners.contains($0.key) } + .reduce(into: UIRectCorner()) { result, corner in + result.insert(corner.value) + } + + self.init( + roundedRect: rect, + byRoundingCorners: rectCorners, + cornerRadii: cornerRadii + ) + #else + self.init( + roundedRect: NSRectFromCGRect(rect), + xRadius: cornerRadii.width, + yRadius: cornerRadii.height + ) + #endif + } +} + +{{ classAccessModifier }} class {{ shadowStyleLayerTypeName }}: CALayer { + + // MARK: - Instance Properties + + private var shadowLayers: [CALayer] = [] + private let backgroundLayer = CALayer() + + {{ accessModifier }} var shadowStyle: {{ styleTypeName }} { + didSet { updateShadowLayers() } + } + + {{ accessModifier }} override var backgroundColor: CGColor? { + get { backgroundLayer.backgroundColor } + set { backgroundLayer.backgroundColor = newValue } + } + + {{ accessModifier }} override var cornerRadius: CGFloat { + didSet { backgroundLayer.cornerRadius = cornerRadius } + } + + {{ accessModifier }} override var maskedCorners: CACornerMask { + didSet { backgroundLayer.maskedCorners = maskedCorners } + } + + // MARK: - Initializers + + {{ accessModifier }} init(shadowStyle: {{ styleTypeName }}) { + self.shadowStyle = shadowStyle + + super.init() + + configureShadowLayers() + configureBackgroundLayer() + } + + {{ accessModifier }} override convenience init() { + self.init(shadowStyle: .clear) + } + + {{ accessModifier }} override convenience init(layer: Any) { + if let layer = layer as? {{ shadowStyleLayerTypeName }} { + self.init(shadowStyle: layer.shadowStyle) + } else { + self.init(shadowStyle: .clear) + } + } + + {{ accessModifier }} required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Instance Methods + + private func configureShadowLayers() { + shadowLayers = shadowStyle + .shadows + .map { CALayer(shadow: $0) } + + shadowLayers.reversed().forEach { shadowLayer in + insertSublayer(shadowLayer, at: 0) + } + } + + private func configureBackgroundLayer() { + backgroundLayer.masksToBounds = true + + addSublayer(backgroundLayer) + } + + private func updateShadowLayers() { + shadowLayers.forEach { $0.removeFromSuperlayer() } + + configureShadowLayers() + } + + private func layoutShadowLayers() { + shadowLayers.forEach { shadowLayer in + shadowLayer.frame = bounds + + shadowLayer.shadowPath = {{ bezierPathTypeName }}( + roundedRect: bounds, + byRoundingCorners: maskedCorners, + cornerRadii: CGSize( + width: cornerRadius, + height: cornerRadius + ) + ).cgPath + } + } + + private func layoutBackgroundLayer() { + backgroundLayer.frame = bounds + } + + {{ classAccessModifier }} override func layoutSublayers() { + super.layoutSublayers() + + layoutShadowLayers() + layoutBackgroundLayer() + } +} + +{{ classAccessModifier }} class {{ shadowStyleViewTypeName }}: {{ viewTypeName }} { + + // MARK: - Type Properties + + {{ accessModifier }} override class var layerClass: AnyClass { + {{ shadowStyleLayerTypeName }}.self + } + + // MARK: - Instance Properties + + {{ accessModifier }} var shadowStyleLayer: {{ shadowStyleLayerTypeName }} { + layer as! {{ shadowStyleLayerTypeName }} + } + + {{ accessModifier }} var shadowStyle: {{ styleTypeName }} { + get { shadowStyleLayer.shadowStyle } + set { shadowStyleLayer.shadowStyle = newValue } + } + + {{ accessModifier }} override var backgroundColor: {{ colorTypeName }}? { + get { shadowStyleLayer.backgroundColor.map({{ colorTypeName }}.init(cgColor:)) } + set { shadowStyleLayer.backgroundColor = newValue?.cgColor } + } +} +{% else %} +// No shadow style found +{% endif %} diff --git a/Templates/SpacingTokens.stencil b/Templates/SpacingTokens.stencil new file mode 100644 index 0000000..27c76e5 --- /dev/null +++ b/Templates/SpacingTokens.stencil @@ -0,0 +1,49 @@ +{% include "FileHeader.stencil" %} +{% if semanticSpacings or coreSpacings %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"SpacingTokens" }}{% endset %} +{% set edgeInsetsTypeName %}{{ options.edgeInsetsTypeName|default:"UIEdgeInsets" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro spacingProperty spacing %} + /// {{ spacing.path|join:"." }} + /// + /// Value: {{ spacing.value }} + {% set values spacing.value|split:" " %} + {% if values.count == 1 %} + {{ accessModifier }} var {% call propertyName spacing.path|join:"_" %}: CGFloat { + {{ spacing.value }} + } + {% elif values.count > 1 %} + {{ accessModifier }} var {% call propertyName spacing.path|join:"_" %}: {{ edgeInsetsTypeName }} { + {% if values.count == 2 %} + {{ edgeInsetsTypeName }}(top: {{ values.0 }}, left: {{ values.1 }}, bottom: {{ values.0 }}, right: {{ values.1 }}) + {% elif values.count == 3 %} + {{ edgeInsetsTypeName }}(top: {{ values.0 }}, left: {{ values.1 }}, bottom: {{ values.2 }}, right: {{ values.1 }}) + {% elif values.count == 4 %} + {{ edgeInsetsTypeName }}(top: {{ values.0 }}, left: {{ values.3 }}, bottom: {{ values.2 }}, right: {{ values.1 }}) + {% endif %} + } + {% endif %} +{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ tokenTypeName }} { + + // MARK: - Instance Properties + {% for spacing in semanticSpacings %} + + {% call spacingProperty spacing %} + {% endfor %} + {% for spacing in coreSpacings %} + + {% call spacingProperty spacing %} + {% endfor %} +} +{% else %} +// No spacing tokens found +{% endif %} diff --git a/Templates/Spacings.stencil b/Templates/Spacings.stencil deleted file mode 100644 index a2510cf..0000000 --- a/Templates/Spacings.stencil +++ /dev/null @@ -1,17 +0,0 @@ -// swiftlint:disable all -{% if spacings %} -import Foundation - -public enum Spacings { -{% for spacing in spacings %} - - /// {{spacing.name}} - /// - /// Value: {{spacing.value}}. - public static let {{spacing.name}}: CGFloat = {{spacing.value}} -{% endfor %} -} -{% else %} -// No spacings found -{% endif %} -// swiftlint:enable all diff --git a/Templates/TextStyles.stencil b/Templates/TextStyles.stencil index 0c36035..d2dae1e 100644 --- a/Templates/TextStyles.stencil +++ b/Templates/TextStyles.stencil @@ -1,113 +1,191 @@ -// swiftlint:disable all +{% include "FileHeader.stencil" %} {% if textStyles %} -{% macro rgbaHex color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %} -{% macro rgbHexString color %}#{{color.red}}{{color.green}}{{color.blue}}{% endmacro %} -{% macro rgbaHexString color %}{% call rgbHexString color %}{{color.alpha}}{% endmacro %} -{% macro rgbString color %}{{color.red|hexToInt}} {{color.green|hexToInt}} {{color.blue|hexToInt}}{% endmacro %} -{% macro rgbaString color %}{% call rgbString color %}, {{color.alpha|hexToInt|int255toFloat|percent}}{% endmacro %} -{% macro colorValue color %}hex: {% call rgbaHexString color %}; rgba: {% call rgbaString color %}{% endmacro %} -{% macro colorStyle color %}{% if color.name %}{{color.name}}; {% endif %}{% endmacro %} -{% macro colorDescription color %}{% call colorStyle color %}{% call colorValue color %}{% endmacro %} -{% macro fontName textStyle %}{{textStyle.fontFamily}} ({{textStyle.fontPostScriptName}}); {% endmacro %} -{% macro fontWeight textStyle %}weight {{textStyle.fontWeight}}; {% endmacro %} -{% macro fontSize textStyle %}size {{textStyle.fontSize}}{% endmacro %} -{% macro fontDescription textStyle %}{% call fontName textStyle %}{% call fontWeight textStyle %}{% call fontSize textStyle %}{% endmacro %} -{% macro textStyleName textStyle %}{{textStyle.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endmacro %} -import Foundation +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set styleTypeName %}{{ options.styleTypeName|default:"TextStyle" }}{% endset %} +{% set fontTypeName %}{{ options.fontTypeName|default:"UIFont" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set strikethroughStyle %}{{ options.strikethroughStyle|default:"single" }}{% endset %} +{% set underlineStyle %}{{ options.underlineStyle|default:"single" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% set optionalFontTypeName %}{{ fontTypeName }}?{% endset %} +{% set optionalColorTypeName %}{{ colorTypeName }}?{% endset %} +{% macro styleMutator propertyName propertyTypeName %} + {% set methodName %}with{{ propertyName|upperFirstLetter }}{% endset %} + {{ accessModifier }} func {{ methodName }}(_ {{ propertyName }}: {{ propertyTypeName }}) -> {{ styleTypeName }} { + return {{ styleTypeName }}( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } +{% endmacro %} + +#if canImport(UIKit) import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ styleTypeName }}: Equatable { -public struct TextStyle: Equatable { + // MARK: - Nested Types - public let font: UIFont - public let textColor: UIColor - public let paragraphSpacing: CGFloat? - public let paragraphIndent: CGFloat? - public let lineHeight: CGFloat? - public let letterSpacing: CGFloat? + {{ accessModifier }} enum ValidationError: Error, CustomStringConvertible { + case fontNotFound(name: String, size: Double) - public var actualLineHeight: CGFloat { - return lineHeight ?? font.lineHeight + {{ accessModifier }} var description: String { + switch self { + case let .fontNotFound(name, size): + return "Font '\(name) \(size)' couldn't be loaded" + } + } } - public init( - font: UIFont, - textColor: UIColor, + // MARK: - Type Properties +{% for style in textStyles %} + + /// {{ style.name }} + /// + /// Font: {{ style.font|fontInfo }} + /// Color: {% if style.color.styleName %}{{ style.color.styleName }}; {% endif %}{{ style.color.color|colorInfo|lowerFirstWord }} + /// Strikethrough: {{ style.strikethrough }} + /// Underline: {{ style.underline }} + /// Paragraph spacing: {{ style.paragraphSpacing|default:"default" }} + /// Paragraph indent: {{ style.paragraphIndent|default:"default" }} + /// Line height: {{ style.lineHeight|default:"default" }} + /// Letter spacing: {{ style.letterSpacing|default:"default" }} + {{ accessModifier }} static let {% call propertyName style.name %} = {{ styleTypeName }}( + font: {{ fontTypeName }}{{ style.font|initializer:options.usingSystemFonts }}, + color: {{ colorTypeName }}( + red: {{ style.color.color.red }}, + green: {{ style.color.color.green }}, + blue: {{ style.color.color.blue }}, + alpha: {{ style.color.color.alpha }} + ), + strikethrough: {{ style.strikethrough }}, + underline: {{ style.underline }}, + paragraphSpacing: {{ style.paragraphSpacing|default:"nil" }}, + paragraphIndent: {{ style.paragraphIndent|default:"nil" }}, + lineHeight: {{ style.lineHeight|default:"nil" }}, + letterSpacing: {{ style.letterSpacing|default:"nil" }} + ) +{% endfor %} + + // MARK: - Type Methods + + {{ accessModifier }} static func validate() throws { + {% for style in textStyles %} + {% if not ( options.usingSystemFonts and style.font|isSystemFont ) %} + guard {{ fontTypeName }}(name: "{{ style.font.name }}", size: {{ style.font.size }}) != nil else { + throw ValidationError.fontNotFound(name: "{{ style.font.name }}", size: {{ style.font.size }}) + } + + {% endif %} + {% endfor %} + print("All text styles are valid") + } + + // MARK: - Instance Properties + + {{ accessModifier }} let font: {{ optionalFontTypeName }} + {{ accessModifier }} let color: {{ optionalColorTypeName }} + {{ accessModifier }} let backgroundColor: {{ optionalColorTypeName }} + {{ accessModifier }} let strikethrough: Bool + {{ accessModifier }} let underline: Bool + {{ accessModifier }} let paragraphSpacing: CGFloat? + {{ accessModifier }} let paragraphIndent: CGFloat? + {{ accessModifier }} let lineHeight: CGFloat? + {{ accessModifier }} let letterSpacing: CGFloat? + {{ accessModifier }} let lineBreakMode: NSLineBreakMode? + {{ accessModifier }} let alignment: NSTextAlignment? + + // MARK: - Initializers + + {{ accessModifier }} init( + font: {{ optionalFontTypeName }} = nil, + color: {{ optionalColorTypeName }} = nil, + backgroundColor: {{ optionalColorTypeName }} = nil, + strikethrough: Bool = false, + underline: Bool = false, paragraphSpacing: CGFloat? = nil, paragraphIndent: CGFloat? = nil, lineHeight: CGFloat? = nil, - letterSpacing: CGFloat? = nil + letterSpacing: CGFloat? = nil, + lineBreakMode: NSLineBreakMode? = nil, + alignment: NSTextAlignment? = nil ) { self.font = font - self.textColor = textColor + self.color = color + self.backgroundColor = backgroundColor + self.strikethrough = strikethrough + self.underline = underline self.paragraphSpacing = paragraphSpacing self.paragraphIndent = paragraphIndent self.lineHeight = lineHeight self.letterSpacing = letterSpacing + self.lineBreakMode = lineBreakMode + self.alignment = alignment } - public init( - fontName: String, - fontSize: CGFloat, - textColor: UIColor, - paragraphSpacing: CGFloat? = nil, - paragraphIndent: CGFloat? = nil, - lineHeight: CGFloat? = nil, - letterSpacing: CGFloat? = nil - ) { - self.init( - font: UIFont(name: fontName, size: fontSize) ?? UIFont.systemFont(ofSize: fontSize), - textColor: textColor, - paragraphSpacing: paragraphSpacing, - paragraphIndent: paragraphIndent, - lineHeight: lineHeight, - letterSpacing: letterSpacing - ) - } + // MARK: - Instance Methods - public func withTextColor(_ textColor: UIColor) -> TextStyle { - return TextStyle( - font: font, - textColor: textColor, - paragraphSpacing: paragraphSpacing, - paragraphIndent: paragraphIndent, - lineHeight: lineHeight, - letterSpacing: letterSpacing - ) - } + private func attributes(paragraphStyle: NSParagraphStyle?) -> [NSAttributedString.Key: Any] { + var attributes: [NSAttributedString.Key: Any] = [:] - public func attributes( - textColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - alignment: NSTextAlignment? = nil, - lineBreakMode: NSLineBreakMode? = nil, - ignoringParagraphStyle: Bool = false - ) -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] = [ - .font: font, - .foregroundColor: textColor ?? self.textColor, - ] + if let paragraphStyle = paragraphStyle { + attributes[.paragraphStyle] = paragraphStyle + } + + if let font = font { + attributes[.font] = font + } + + if let color = color { + attributes[.foregroundColor] = color + } if let backgroundColor = backgroundColor { attributes[.backgroundColor] = backgroundColor } - if let letterSpacing = letterSpacing { - attributes[.kern] = NSNumber(value: Float(letterSpacing)) + if strikethrough { + attributes[.strikethroughStyle] = NSUnderlineStyle.{{ strikethroughStyle }}.rawValue } - if ignoringParagraphStyle { - return attributes + if underline { + attributes[.underlineStyle] = NSUnderlineStyle.{{ underlineStyle }}.rawValue } + if let letterSpacing = letterSpacing { + attributes[.kern] = letterSpacing + } + + return attributes + } + + // MARK: - + + {{ accessModifier }} func paragraphStyle() -> NSParagraphStyle { let paragraphStyle = NSMutableParagraphStyle() if let lineHeight = lineHeight { - let paragraphLineSpacing = (lineHeight - font.lineHeight) / 2.0 - let paragraphLineHeight = lineHeight - paragraphLineSpacing + if let font = font { + paragraphStyle.lineSpacing = (lineHeight - font.lineHeight) * 0.5 + paragraphStyle.minimumLineHeight = lineHeight - paragraphStyle.lineSpacing + } else { + paragraphStyle.lineSpacing = 0.0 + paragraphStyle.minimumLineHeight = lineHeight + } - paragraphStyle.lineSpacing = paragraphLineSpacing - paragraphStyle.minimumLineHeight = paragraphLineHeight - paragraphStyle.maximumLineHeight = paragraphLineHeight + paragraphStyle.maximumLineHeight = paragraphStyle.minimumLineHeight } if let paragraphSpacing = paragraphSpacing { @@ -118,76 +196,65 @@ public struct TextStyle: Equatable { paragraphStyle.firstLineHeadIndent = paragraphIndent } + if let lineBreakMode = lineBreakMode { + paragraphStyle.lineBreakMode = lineBreakMode + } + if let alignment = alignment { paragraphStyle.alignment = alignment } - if let lineBreakMode = lineBreakMode { - paragraphStyle.lineBreakMode = lineBreakMode - } + return paragraphStyle + } - attributes[.paragraphStyle] = paragraphStyle + {{ accessModifier }} func attributes(includingParagraphStyle: Bool = true) -> [NSAttributedString.Key: Any] { + if includingParagraphStyle { + return attributes(paragraphStyle: paragraphStyle()) + } else { + return attributes(paragraphStyle: nil) + } + } - return attributes + {{ accessModifier }} func attributedString( + _ string: String, + includingParagraphStyle: Bool = true + ) -> NSAttributedString { + return NSAttributedString(string: string, style: self, includingParagraphStyle: includingParagraphStyle) } -} -public extension TextStyle { -{% for textStyle in textStyles %} + // MARK: - - /// {{textStyle.name}} - /// - /// Font: {% call fontDescription textStyle %} - /// Text color: {% call colorDescription textStyle.textColor %} - /// Paragraph spacing: {{textStyle.paragraphSpacing|default:"default"}} - /// Paragraph indent: {{textStyle.paragraphIndent|default:"default"}} - /// Line height: {{textStyle.lineHeight|default:"default"}} - /// Letter spacing: {{textStyle.letterSpacing|default:"default"}} - static let {% call textStyleName textStyle %} = TextStyle( - fontName: "{{textStyle.fontPostScriptName}}", - fontSize: {{textStyle.fontSize}}, - textColor: UIColor(rgbaHex: {% call rgbaHex textStyle.textColor %}), - paragraphSpacing: {{textStyle.paragraphSpacing|default:"nil"}}, - paragraphIndent: {{textStyle.paragraphIndent|default:"nil"}}, - lineHeight: {{textStyle.lineHeight|default:"nil"}}, - letterSpacing: {{textStyle.letterSpacing|default:"nil"}} - ) -{% endfor %} -} + {% call styleMutator "font" optionalFontTypeName %} -public extension String { + {% call styleMutator "color" optionalColorTypeName %} - func styled( - _ textStyle: TextStyle, - textColor: UIColor? = nil, - backgroundColor: UIColor? = nil, - alignment: NSTextAlignment? = nil, - lineBreakMode: NSLineBreakMode? = nil - ) -> NSAttributedString { - return NSAttributedString( - string: self, - attributes: textStyle.attributes( - textColor: textColor, - backgroundColor: backgroundColor, - alignment: alignment, - lineBreakMode: lineBreakMode - ) - ) - } + {% call styleMutator "backgroundColor" optionalColorTypeName %} + + {% call styleMutator "strikethrough" "Bool" %} + + {% call styleMutator "underline" "Bool" %} + + {% call styleMutator "paragraphSpacing" "CGFloat?" %} + + {% call styleMutator "paragraphIndent" "CGFloat?" %} + + {% call styleMutator "lineHeight" "CGFloat?" %} + + {% call styleMutator "letterSpacing" "CGFloat?" %} + + {% call styleMutator "lineBreakMode" "NSLineBreakMode?" %} + + {% call styleMutator "alignment" "NSTextAlignment?" %} } -private extension UIColor { +{{ accessModifier }} extension NSAttributedString { - convenience init(rgbaHex: UInt32) { - self.init( - red: CGFloat((rgbaHex >> 24) & 0xFF) / 255.0, - green: CGFloat((rgbaHex >> 16) & 0xFF) / 255.0, - blue: CGFloat((rgbaHex >> 8) & 0xFF) / 255.0, - alpha: CGFloat(rgbaHex & 0xFF) / 255.0 - ) + // MARK: - Initializers + + convenience init(string: String, style: {{ styleTypeName }}, includingParagraphStyle: Bool = true) { + self.init(string: string, attributes: style.attributes(includingParagraphStyle: includingParagraphStyle)) } } {% else %} // No text style found {% endif %} -// swiftlint:enable all diff --git a/Templates/Theme.stencil b/Templates/Theme.stencil new file mode 100644 index 0000000..4311be6 --- /dev/null +++ b/Templates/Theme.stencil @@ -0,0 +1,114 @@ +{% include "FileHeader.stencil" %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set themeTypeName %}{{ options.styleTypeName|default:"Theme" }}{% endset %} +{% set colorTokensTypeName %}{{ options.tokenTypeName|default:"ColorTokens" }}{% endset %} +{% set boxShadowTokensTypeName %}{{ options.tokenTypeName|default:"BoxShadowTokens" }}{% endset %} +{% set typographyTokensTypeName %}{{ options.tokenTypeName|default:"TypographyTokens" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set shadowTypeName %}{{ options.shadowTypeName|default:"Shadow" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord }}{% endmacro %} +{% macro typeName name %}{{ name|swiftIdentifier:"pretty"|upperFirstLetter|escapeReservedKeywords }}{% endmacro %} +{% macro hexColor color %}{{ color|fullHex|uppercase|replace:"#","0x" }}{% endmacro %} +{% macro argumentsBlock item theme parentTypeName %} +{% for color in item.colors %} +{% call propertyName color.path.last %}: {{ colorTypeName }}(hex: {% call hexColor color.value %}){% if not forloop.last %},{% endif %} +{% endfor %} +{% for child in item.children %} +{% set childTypeName %}{% if parentTypeName %}{{ parentTypeName }}.{% endif %}{% call typeName child.name %}{% endset %} +{% call propertyName child.name %}: {{ childTypeName }}( + {% filter indent:4 %} + {% call argumentsBlock child theme childTypeName %} + {% endfilter %} +){% if not forloop.last %},{% endif %} +{% endfor %} +{% endmacro %} +{% macro colorsArgumentsBlock colors theme %} +{% if colors %} +colors: {{ colorTokensTypeName }}( + {% filter indent:4 %} + {% call argumentsBlock colors theme colorTokensTypeName %} + {% endfilter %} +), +{% endif %} +{% endmacro %} +{% macro shadowsArgumentsBlock boxShadows theme %} +{% if boxShadows %} +shadows: {{ boxShadowTokensTypeName }}( +{% for boxShadow in boxShadows %} + {% call propertyName boxShadow.path.last %}: {{ shadowTypeName }}( + offset: CGSize(width: {{ boxShadow.x }}, height: {{ boxShadow.y }}), + radius: {{ boxShadow.blur }}, + color: {{ colorTypeName }}(hex: {% call hexColor boxShadow.color %}), + opacity: 1.0 + ){% if not forloop.last %},{% endif %} +{% endfor %} +) +{% endif %} +{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ themeTypeName }} { + + {% if dictThemedColors[fallbackTheme] %} + {{ accessModifier }} let colors: {{ colorTokensTypeName }} + {% endif %} + {% if dictThemedBoxShadows[fallbackTheme] %} + {{ accessModifier }} let shadows: {{ boxShadowTokensTypeName }} + {% endif %} + {{ accessModifier }} let typographies: {{ typographyTokensTypeName }} + + init( + {% if dictThemedColors[fallbackTheme] %} + colors: {{ colorTokensTypeName }}, + {% endif %} + {% if dictThemedBoxShadows[fallbackTheme] %} + shadows: {{ boxShadowTokensTypeName }}, + {% endif %} + typographies: {{ typographyTokensTypeName }} = {{ typographyTokensTypeName }}() + ) { + {% if dictThemedColors[fallbackTheme] %} + self.colors = colors + {% endif %} + {% if dictThemedBoxShadows[fallbackTheme] %} + self.shadows = shadows + {% endif %} + self.typographies = typographies + } +} + +extension {{ themeTypeName }} { + + {% for theme in themes %} + {% set themePropertyName %}{{ theme.name||swiftIdentifier:"pretty"|lowerFirstWord }}{% endset %} + {{ accessModifier }} static let {{ themePropertyName }} = Self( + {% filter indent:8 %} + {% call colorsArgumentsBlock dictThemedColors[theme.name] theme.name %} + {% call shadowsArgumentsBlock dictThemedBoxShadows[theme.name] theme.name %} + {% endfilter %} + ) + {% endfor %} +} + +{% if dictThemedColors or dictThemedBoxShadows %} +private extension {{ colorTypeName }} { + + convenience init(hex: UInt32) { + let red = UInt8((hex >> 24) & 0xFF) + let green = UInt8((hex >> 16) & 0xFF) + let blue = UInt8((hex >> 8) & 0xFF) + let alpha = UInt8(hex & 0xFF) + + self.init( + red: CGFloat(red) / 255.0, + green: CGFloat(green) / 255.0, + blue: CGFloat(blue) / 255.0, + alpha: CGFloat(alpha) / 255.0 + ) + } +} +{% endif %} diff --git a/Templates/TypographyTokens.stencil b/Templates/TypographyTokens.stencil new file mode 100644 index 0000000..dac669c --- /dev/null +++ b/Templates/TypographyTokens.stencil @@ -0,0 +1,245 @@ +{% include "FileHeader.stencil" %} +{% if typographies %} +{% set accessModifier %}{% if options.publicAccess %}public{% else %}internal{% endif %}{% endset %} +{% set tokenTypeName %}{{ options.tokenTypeName|default:"TypographyTokens" }}{% endset %} +{% set styleTypeName %}{{ options.styleTypeName|default:"TextStyle" }}{% endset %} +{% set fontTypeName %}{{ options.fontTypeName|default:"UIFont" }}{% endset %} +{% set colorTypeName %}{{ options.colorTypeName|default:"UIColor" }}{% endset %} +{% set optionalFontTypeName %}{{ fontTypeName }}?{% endset %} +{% set optionalColorTypeName %}{{ colorTypeName }}?{% endset %} +{% set strikethroughStyle %}{{ options.strikethroughStyle|default:"single" }}{% endset %} +{% set underlineStyle %}{{ options.underlineStyle|default:"single" }}{% endset %} +{% macro propertyName name %}{{ name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords }}{% endmacro %} +{% macro styleMutator propertyName propertyTypeName %} + {% set methodName %}with{{ propertyName|upperFirstLetter }}{% endset %} + {{ accessModifier }} func {{ methodName }}(_ {{ propertyName }}: {{ propertyTypeName }}) -> {{ styleTypeName }} { + return {{ styleTypeName }}( + font: font, + color: color, + backgroundColor: backgroundColor, + strikethrough: strikethrough, + underline: underline, + paragraphSpacing: paragraphSpacing, + paragraphIndent: paragraphIndent, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + lineBreakMode: lineBreakMode, + alignment: alignment + ) + } +{% endmacro %} + +#if canImport(UIKit) +import UIKit +#else +import AppKit +#endif + +{{ accessModifier }} struct {{ tokenTypeName }} { + {% for typography in typographies %} + + /// {{ typography.path|join:" " }} + /// + /// Font: {{ typography.fontFamily.value }} {{ typography.fontWeight.value }} + /// Strikethrough: {{ typography.textDecoration.value == "strikethrough" }} + /// Underline: {{ typography.textDecoration.value == "underline" }} + /// Paragraph spacing: {{ typography.paragraphSpacing.value }} + /// Paragraph indent: {{ typography.paragraphIndent.value|default:"default" }} + /// Line height: {{ typography.lineHeight.value }} + /// Letter spacing: {{ typography.letterSpacing.value|default:"default" }} + {{ accessModifier }} let {% call propertyName typography.path|join:"_" %} = {{ styleTypeName }}( + font: {{ fontTypeName }}(name: "{{ typography.fontFamily.value }}-{{ typography.fontWeight.value|replace:" ","" }}", size: {{ typography.fontSize.value }}), + color: .label, + strikethrough: {{ typography.textDecoration.value == "strikethrough" }}, + underline: {{ typography.textDecoration.value == "underline" }}, + paragraphSpacing: {{ typography.paragraphSpacing.value }}, + paragraphIndent: {{ typography.paragraphIndent.value|default:"nil"|replace:"px","" }}, + lineHeight: {{ typography.lineHeight.value }}, + letterSpacing: {{ typography.letterSpacing.value|default:"nil" }} + ) + {% endfor %} +} + +{{ accessModifier }} struct {{ styleTypeName }}: Equatable { + + {{ accessModifier }} enum ValidationError: Error, CustomStringConvertible { + case fontNotFound(name: String, size: Double) + + {{ accessModifier }} var description: String { + switch self { + case let .fontNotFound(name, size): + return "Font '\(name) \(size)' couldn't be loaded" + } + } + } + + {{ accessModifier }} static func validate() throws { + {% for typography in typographies %} + guard {{ fontTypeName }}(name: "{{ typography.fontFamily.value }}-{{ typography.fontWeight.value|replace:" ","" }}", size: {{ typography.fontSize.value }}) != nil else { + throw ValidationError.fontNotFound(name: "{{ typography.fontFamily.value }}-{{ typography.fontWeight.value|replace:" ","" }}", size: {{ typography.fontSize.value }}) + } + + {% endfor %} + print("All text styles are valid") + } + + {{ accessModifier }} let font: {{ optionalFontTypeName }} + {{ accessModifier }} let color: {{ optionalColorTypeName }} + {{ accessModifier }} let backgroundColor: {{ optionalColorTypeName }} + {{ accessModifier }} let strikethrough: Bool + {{ accessModifier }} let underline: Bool + {{ accessModifier }} let paragraphSpacing: CGFloat? + {{ accessModifier }} let paragraphIndent: CGFloat? + {{ accessModifier }} let lineHeight: CGFloat? + {{ accessModifier }} let letterSpacing: CGFloat? + {{ accessModifier }} let lineBreakMode: NSLineBreakMode? + {{ accessModifier }} let alignment: NSTextAlignment? + + {{ accessModifier }} init( + font: {{ optionalFontTypeName }} = nil, + color: {{ optionalColorTypeName }} = nil, + backgroundColor: {{ optionalColorTypeName }} = nil, + strikethrough: Bool = false, + underline: Bool = false, + paragraphSpacing: CGFloat? = nil, + paragraphIndent: CGFloat? = nil, + lineHeight: CGFloat? = nil, + letterSpacing: CGFloat? = nil, + lineBreakMode: NSLineBreakMode? = nil, + alignment: NSTextAlignment? = nil + ) { + self.font = font + self.color = color + self.backgroundColor = backgroundColor + self.strikethrough = strikethrough + self.underline = underline + self.paragraphSpacing = paragraphSpacing + self.paragraphIndent = paragraphIndent + self.lineHeight = lineHeight + self.letterSpacing = letterSpacing + self.lineBreakMode = lineBreakMode + self.alignment = alignment + } + + private func attributes(paragraphStyle: NSParagraphStyle?) -> [NSAttributedString.Key: Any] { + var attributes: [NSAttributedString.Key: Any] = [:] + + if let paragraphStyle = paragraphStyle { + attributes[.paragraphStyle] = paragraphStyle + } + + if let font = font { + attributes[.font] = font + } + + if let color = color { + attributes[.foregroundColor] = color + } + + if let backgroundColor = backgroundColor { + attributes[.backgroundColor] = backgroundColor + } + + if strikethrough { + attributes[.strikethroughStyle] = NSUnderlineStyle.{{ strikethroughStyle }}.rawValue + } + + if underline { + attributes[.underlineStyle] = NSUnderlineStyle.{{ underlineStyle }}.rawValue + } + + if let letterSpacing = letterSpacing { + attributes[.kern] = letterSpacing + } + + return attributes + } + + {{ accessModifier }} func paragraphStyle() -> NSParagraphStyle { + let paragraphStyle = NSMutableParagraphStyle() + + if let lineHeight = lineHeight { + if let font = font { + paragraphStyle.lineSpacing = (lineHeight - font.lineHeight) * 0.5 + paragraphStyle.minimumLineHeight = lineHeight - paragraphStyle.lineSpacing + } else { + paragraphStyle.lineSpacing = 0.0 + paragraphStyle.minimumLineHeight = lineHeight + } + + paragraphStyle.maximumLineHeight = paragraphStyle.minimumLineHeight + } + + if let paragraphSpacing = paragraphSpacing { + paragraphStyle.paragraphSpacing = paragraphSpacing + } + + if let paragraphIndent = paragraphIndent { + paragraphStyle.firstLineHeadIndent = paragraphIndent + } + + if let lineBreakMode = lineBreakMode { + paragraphStyle.lineBreakMode = lineBreakMode + } + + if let alignment = alignment { + paragraphStyle.alignment = alignment + } + + return paragraphStyle + } + + {{ accessModifier }} func attributes(includingParagraphStyle: Bool = true) -> [NSAttributedString.Key: Any] { + if includingParagraphStyle { + return attributes(paragraphStyle: paragraphStyle()) + } else { + return attributes(paragraphStyle: nil) + } + } + + {{ accessModifier }} func attributedString( + _ string: String, + includingParagraphStyle: Bool = true + ) -> NSAttributedString { + return NSAttributedString(string: string, style: self, includingParagraphStyle: includingParagraphStyle) + } + + {% call styleMutator "font" optionalFontTypeName %} + + {% call styleMutator "color" optionalColorTypeName %} + + {% call styleMutator "backgroundColor" optionalColorTypeName %} + + {% call styleMutator "strikethrough" "Bool" %} + + {% call styleMutator "underline" "Bool" %} + + {% call styleMutator "paragraphSpacing" "CGFloat?" %} + + {% call styleMutator "paragraphIndent" "CGFloat?" %} + + {% call styleMutator "lineHeight" "CGFloat?" %} + + {% call styleMutator "letterSpacing" "CGFloat?" %} + + {% call styleMutator "lineBreakMode" "NSLineBreakMode?" %} + + {% call styleMutator "alignment" "NSTextAlignment?" %} +} + +{{ accessModifier }} extension NSAttributedString { + + convenience init(string: String, style: {{ styleTypeName }}, includingParagraphStyle: Bool = true) { + self.init(string: string, attributes: style.attributes(includingParagraphStyle: includingParagraphStyle)) + } +} + +{{ accessModifier }} extension String { + + func styled(as {{ styleTypeName|lowerFirstWord }}: {{ styleTypeName }}) -> NSAttributedString { + NSAttributedString(string: self, style: {{ styleTypeName|lowerFirstWord }}) + } +} +{% else %} +// No typography tokens found +{% endif %} \ No newline at end of file diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml index 649f18d..4ad33b7 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -5,3 +5,4 @@ disabled_rules: - function_body_length - trailing_closure - type_body_length + - nesting diff --git a/Tests/FigmaGenTests.swift b/Tests/FigmaGenTests/FigmaGenTests.swift old mode 100755 new mode 100644 similarity index 68% rename from Tests/FigmaGenTests.swift rename to Tests/FigmaGenTests/FigmaGenTests.swift index ace1589..ee3f0bc --- a/Tests/FigmaGenTests.swift +++ b/Tests/FigmaGenTests/FigmaGenTests.swift @@ -1,9 +1,3 @@ -// -// FigmaGen -// Copyright © 2019 HeadHunter -// MIT Licence -// - import XCTest class FigmaGenTests: XCTestCase { diff --git a/Resources/Info.plist b/Tests/FigmaGenTests/Info.plist similarity index 62% rename from Resources/Info.plist rename to Tests/FigmaGenTests/Info.plist index 4ed3b9d..64d65ca 100644 --- a/Resources/Info.plist +++ b/Tests/FigmaGenTests/Info.plist @@ -3,11 +3,9 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) - CFBundleIconFile - CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -15,14 +13,10 @@ CFBundleName $(PRODUCT_NAME) CFBundlePackageType - APPL + $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - $(MARKETING_VERSION) + 1.0 CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - Copyright © 2019 HeadHunter. MIT Licence. + 1 diff --git a/Tests/FigmaGenTests/TokensResolverTests.swift b/Tests/FigmaGenTests/TokensResolverTests.swift new file mode 100644 index 0000000..84f1b58 --- /dev/null +++ b/Tests/FigmaGenTests/TokensResolverTests.swift @@ -0,0 +1,277 @@ +#if canImport(FigmaGen) +import XCTest +@testable import FigmaGen + +final class TokensResolverTests: XCTestCase { + + // MARK: - Instance Properties + + private let tokensResolver = DefaultTokensResolver() + + // MARK: - Instance Methods + + func testResolveValuesWithReferences() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .core(value: "2"), name: "core.x-base"), // 2 + TokenValue(type: .core(value: "{core.x-base} * 2"), name: "core.2-x-base"), // 4 + TokenValue(type: .spacing(value: "{core.2-x-base} * 1"), name: "core.space.1-x") // 4 + ], + semantic: [], + colors: [], + typography: [], + themedTokens: [:] + ) + + let value = "{core.space.1-x} + {core.space.1-x} / 2" + let expectedValue = "6" + + let actualValue = try tokensResolver.resolveValue(value, tokenValues: tokenValues, theme: .light) + + XCTAssertEqual(actualValue, expectedValue) + } + + func testResolveValuesWithoutReferences() throws { + let numberValue = "10" + let percentValue = "-2.50%" + let textValue = "none" + let pixelValue = "0px" + let colorValue = "#ffffff" + + let actualNumberValue = try tokensResolver.resolveValue(numberValue, tokenValues: .empty, theme: .light) + let actualPercentValue = try tokensResolver.resolveValue(percentValue, tokenValues: .empty, theme: .light) + let actualTextValue = try tokensResolver.resolveValue(textValue, tokenValues: .empty, theme: .light) + let actualPixelValue = try tokensResolver.resolveValue(pixelValue, tokenValues: .empty, theme: .light) + let actualColorValue = try tokensResolver.resolveValue(colorValue, tokenValues: .empty, theme: .light) + + XCTAssertEqual(actualNumberValue, numberValue) + XCTAssertEqual(actualPercentValue, percentValue) + XCTAssertEqual(actualTextValue, textValue) + XCTAssertEqual(actualPixelValue, pixelValue) + XCTAssertEqual(actualColorValue, colorValue) + } + + func testResolveRGBAColorWithReferences() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .opacity(value: "48%"), name: "core.opacity.48") + ], + semantic: [ + TokenValue(type: .opacity(value: "{core.opacity.48}"), name: "semantic.opacity.disabled") + ], + colors: [ + TokenValue(type: .core(value: "#ffffff"), name: "color.base.white") + ], + typography: [], + themedTokens: [:] + ) + + let value = "rgba({color.base.white}, {semantic.opacity.disabled})" + let expectedColor = Color(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.48) + + let actualColor = try tokensResolver.resolveRGBAColorValue(value, tokenValues: tokenValues, theme: .light) + + XCTAssertEqual(actualColor, expectedColor) + } + + func testResolveRGBAColorWithoutReferences() throws { + let value = "rgba(#FFFFFF, 48%)" + let expectedColor = Color(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.48) + + let actualColor = try tokensResolver.resolveRGBAColorValue(value, tokenValues: .empty, theme: .light) + + XCTAssertEqual(actualColor, expectedColor) + } + + func testResolveLinearGradientWithReferences() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .opacity(value: "0%"), name: "core.opacity.0") + ], + semantic: [ + TokenValue(type: .opacity(value: "{core.opacity.0}"), name: "semantic.opacity.transparent") + ], + colors: [ + TokenValue(type: .color(value: "#d64030"), name: "color.base.red.50") + ], + typography: [], + themedTokens: [:] + ) + + let firstColor = "rgba({color.base.red.50}, {semantic.opacity.transparent})" + let secondColor = "{color.base.red.50}" + let value = "linear-gradient(0deg, \(firstColor) 0%, \(secondColor) 100%)" + + let expectedLinearGradient = LinearGradient( + angle: "0deg", + colorStopList: [ + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 0.0 + ), + percentage: "0%" + ), + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 1.0 + ), + percentage: "100%" + ) + ] + ) + + let actualLinearGradient = try tokensResolver.resolveLinearGradientValue( + value, + tokenValues: tokenValues, + theme: .light + ) + + XCTAssertEqual(actualLinearGradient, expectedLinearGradient) + } + + func testResolveLinearGradientWithoutReferences() throws { + let value = "linear-gradient(0deg, rgba(#d64030, 0%) 0%, #d64030 100%)" + + let expectedLinearGradient = LinearGradient( + angle: "0deg", + colorStopList: [ + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 0.0 + ), + percentage: "0%" + ), + LinearGradient.LinearColorStop( + color: Color( + red: 0.8392156862745098, + green: 0.25098039215686274, + blue: 0.18823529411764706, + alpha: 1.0 + ), + percentage: "100%" + ) + ] + ) + + let actualLinearGradient = try tokensResolver.resolveLinearGradientValue( + value, + tokenValues: .empty, + theme: .light + ) + + XCTAssertEqual(actualLinearGradient, expectedLinearGradient) + } + + func testResolveHexColorWithReferences() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .opacity(value: "48%"), name: "core.opacity.48") + ], + semantic: [ + TokenValue(type: .opacity(value: "{core.opacity.48}"), name: "semantic.opacity.disabled") + ], + colors: [ + TokenValue(type: .color(value: "#ffffff"), name: "color.base.white"), + TokenValue(type: .color(value: "#111"), name: "color.base.gray.5") + ], + typography: [], + themedTokens: [:] + ) + + let value1 = "rgba({color.base.white}, {semantic.opacity.disabled})" + let expectedHexColor1 = "#FFFFFF7A" + + let value2 = "rgba( {color.base.gray.5} , {semantic.opacity.disabled})" + let expectedHexColor2 = "#1111117A" + + let actualHexColor1 = try tokensResolver.resolveHexColorValue(value1, tokenValues: tokenValues, theme: .light) + let actualHexColor2 = try tokensResolver.resolveHexColorValue(value2, tokenValues: tokenValues, theme: .light) + + XCTAssertEqual(actualHexColor1, expectedHexColor1) + XCTAssertEqual(actualHexColor2, expectedHexColor2) + } + + func testResolveHexColorWithoutReferences() throws { + let value = "rgba(#FFFFFF, 48%)" + let expectedHexColor = "#FFFFFF7A" + + let actualHexColor = try tokensResolver.resolveHexColorValue(value, tokenValues: .empty, theme: .light) + + XCTAssertEqual(actualHexColor, expectedHexColor) + } + + func testResolveBaseReference() throws { + let tokenValues = TokenValues( + core: [], + semantic: [], + colors: [ + TokenValue(type: .color(value: "#000000"), name: "color.base.black") + ], + typography: [], + themedTokens: [ + .dark: [ + TokenValue(type: .color(value: "{color.base.black}"), name: "color.background.primary"), + TokenValue(type: .color(value: "{color.background.primary}"), name: "color.background.primary.nested") + ] + ] + ) + + let value = "{color.background.primary.nested}" + let expectedBaseReference = "{color.base.black}" + + let actualBaseReference = try tokensResolver.resolveBaseReference(value, tokenValues: tokenValues.tokens(for: .dark)) + + XCTAssertEqual(actualBaseReference, expectedBaseReference) + } + + func testResolveBaseReferenceWithOpacity() throws { + let tokenValues = TokenValues( + core: [ + TokenValue(type: .opacity(value: "48%"), name: "core.opacity.48") + ], + semantic: [ + TokenValue(type: .opacity(value: "{core.opacity.48}"), name: "semantic.opacity.disabled") + ], + colors: [ + TokenValue(type: .color(value: "#000000"), name: "color.base.black") + ], + typography: [], + themedTokens: [ + .dark: [ + TokenValue(type: .color(value: "{color.base.black}"), name: "color.background.primary"), + TokenValue(type: .color(value: "{color.background.primary}"), name: "color.background.primary.nested") + ] + ] + ) + + let value = "rgba( {color.background.primary.nested}, {semantic.opacity.disabled})" + let expectedBaseReference = "rgba( {color.base.black}, {semantic.opacity.disabled})" + + let actualBaseReference = try tokensResolver.resolveBaseReference(value, tokenValues: tokenValues.tokens(for: .dark)) + + XCTAssertEqual(actualBaseReference, expectedBaseReference) + } +} + +extension TokenValues { + + // MARK: - Type Properties + + static let empty = Self( + core: [], + semantic: [], + colors: [], + typography: [], + themedTokens: [:] + ) +} +#endif diff --git a/Tests/FigmaGenTests/XCTestManifests.swift b/Tests/FigmaGenTests/XCTestManifests.swift new file mode 100644 index 0000000..715c7b0 --- /dev/null +++ b/Tests/FigmaGenTests/XCTestManifests.swift @@ -0,0 +1,18 @@ +#if !canImport(ObjectiveC) +import XCTest + +extension FigmaGenTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__FigmaGenTests = [ + ("testExample", testExample) + ] +} + +public func __allTests() -> [XCTestCaseEntry] { + [ + testCase(FigmaGenTests.__allTests__FigmaGenTests) + ] +} +#endif diff --git a/Tests/FigmaGenToolsTests/FigmaGenToolsTests.swift b/Tests/FigmaGenToolsTests/FigmaGenToolsTests.swift new file mode 100644 index 0000000..2e25470 --- /dev/null +++ b/Tests/FigmaGenToolsTests/FigmaGenToolsTests.swift @@ -0,0 +1,10 @@ +import XCTest + +class FigmaGenToolsTests: XCTestCase { + + // MARK: - Instance Methods + + func testExample() { + XCTAssertTrue(true) + } +} diff --git a/Tests/FigmaGenToolsTests/Info.plist b/Tests/FigmaGenToolsTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/Tests/FigmaGenToolsTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Tests/FigmaGenToolsTests/XCTestManifests.swift b/Tests/FigmaGenToolsTests/XCTestManifests.swift new file mode 100644 index 0000000..0d83587 --- /dev/null +++ b/Tests/FigmaGenToolsTests/XCTestManifests.swift @@ -0,0 +1,18 @@ +#if !canImport(ObjectiveC) +import XCTest + +extension FigmaGenToolsTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__FigmaGenToolsTests = [ + ("testExample", testExample) + ] +} + +public func __allTests() -> [XCTestCaseEntry] { + [ + testCase(FigmaGenToolsTests.__allTests__FigmaGenToolsTests) + ] +} +#endif diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..1d1fb4f --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,10 @@ +import XCTest + +import FigmaGenTests +import FigmaGenToolsTests + +var tests = [XCTestCaseEntry]() +tests += FigmaGenTests.__allTests() +tests += FigmaGenToolsTests.__allTests() + +XCTMain(tests) diff --git a/tokens.json b/tokens.json new file mode 100644 index 0000000..936090c --- /dev/null +++ b/tokens.json @@ -0,0 +1,176 @@ +{ + "core": { + "core": { + "fontSize": { + "10": { + "value": "10", + "type": "fontSizes" + }, + "12": { + "value": "12", + "type": "fontSizes" + } + }, + "lineHeights": { + "12": { + "value": "12", + "type": "lineHeights" + }, + "14": { + "value": "14", + "type": "lineHeights" + }, + "16": { + "value": "16", + "type": "lineHeights" + } + }, + "paragraphSpacing": { + "0": { + "value": "0", + "type": "paragraphSpacing" + }, + "4": { + "value": "4", + "type": "paragraphSpacing" + } + }, + "a11yScale": { + "0": { + "value": "0", + "type": "a11yScales" + }, + "1": { + "value": "1", + "type": "a11yScales" + } + }, + "scale": { + "1": { + "value": "1", + "type": "scaling" + }, + "2": { + "value": "0.96", + "type": "scaling" + } + }, + "size": { + "0-x": { + "value": "{core.2-x-base} * 0", + "type": "sizing" + }, + "1-x": { + "value": "{core.2-x-base} * 1", + "type": "sizing" + } + }, + "borderRadius": { + "0-x": { + "value": "{core.2-x-base} * 0", + "type": "borderRadius" + }, + "1-x": { + "value": "{core.2-x-base} * 1", + "type": "borderRadius" + } + }, + "space": { + "0-x": { + "value": "{core.2-x-base} * 0", + "type": "spacing" + }, + "1-x": { + "value": "{core.2-x-base} * 1", + "type": "spacing" + } + }, + "x-base": { + "value": "2", + "type": "core" + }, + "2-x-base": { + "value": "{core.x-base} * 2", + "type": "core" + }, + "opacity": { + "0": { + "value": "0%", + "type": "opacity" + }, + "16": { + "value": "16%", + "type": "opacity" + } + }, + "letterSpacing": { + "0": { + "value": "0.00%", + "type": "letterSpacing" + }, + "negative": { + "50": { + "value": "-0.50%", + "type": "letterSpacing" + } + }, + "positive": { + "50": { + "value": "0.50%", + "type": "letterSpacing" + } + } + }, + "textCase": { + "none": { + "value": "none", + "type": "textCase" + } + }, + "textDecoration": { + "none": { + "value": "none", + "type": "textDecoration" + } + }, + "paragraphIndent": { + "0": { + "value": "0px", + "type": "dimension" + } + }, + "borderWidth": { + "10": { + "value": "1", + "type": "borderWidth" + }, + "15": { + "value": "1.5", + "type": "borderWidth" + } + } + } + }, + "semantic": { + "semantic": { }, + "static": { } + }, + "colors": { + "color": { + "base": { + "white": { + "value": "#ffffff", + "type": "color" + }, + "black": { + "value": "#000000", + "type": "color" + } + } + } + }, + "typography": { }, + "hh-day": { }, + "hh-night": { }, + "zp-day": { } +} \ No newline at end of file