Skip to content

Tool that converts your Figma tokens to any language made with Rust

Notifications You must be signed in to change notification settings

vrrashkov/TokenParser

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Token Parser

Crates.io

Overview

Token Parser is a universal tool for generating runnable code for any language from your Figma Tokens. It is written in Rust so you have the freedom to use it anywhere you would like without having node.js or anything else installed other than the executable on your system. The full configuration is happening through a configuration yaml file from which you can customize to build for as many different languages as you want from a single place.

Tested with

  1. Figma Variables

  2. Figma Studio Tokens

  3. lukasoppermann/design-tokens

You can use tokens from multiple sources as long as they are with the correct json structure!

Setup

You can get the whole project and build it yourself or if you don't have Rust or just don't want to deal with the builds yourself, go in the Release section and get the executables from there.

  1. Setup the assts/design_tokens_config.yaml file
  2. Run with: for windows (WIN_design_token_parser.exe) for MAC (MAC_design_token_parser) you can find them in Release section
   .\WIN_design_token_parser.exe --generate --config "path/design_tokens_config.yaml"

That's all, your files will be generated and ready to use

The process for generating the usable tokens is split into two.

  • Converting the Figma tokens to usable json files (similar to style-dictionary)

  • Generating the end files for the langages from the previously generated json files

If you have already generated the usable json files you can just run the end code generation by running.

 WIN_design_token_parser.exe --config "path/design_tokens_config.yaml"

Homebrew

  • brew tap vrrashkov/tokenparser
  • brew install tokenparser
  • Configure the yaml config file
  • design_token_parser --generate --config "design_tokens_config.yaml"

Configuration

Input/Output paths for loading and generation
global:
  # Figma source paths
  # These are the pure files from Figma, they can contain aliases
  # For example if we have aliases we will need the actual value and not the alias
  # Separating different files is necessary in case there are duplicate trees but different values/aliases
  # So if we have button-md and button-big with the same trees but different values with aliases that need to be accesed from core.json
  # this should be the setup
  # Look at the figma/variables and figma/generated_styles for better understanding how it works
  figma_source_paths:
    - combine:
        file_name: "button-lg"
        files:
          - "assets/figma/variables/button-lg.json"
          - "assets/figma/variables/core-.json"
    - combine:
        file_name: "button-md"
        files:
          - "assets/figma/variables/button-md.json"
          - "assets/figma/variables/core-.json"
    - combine:
        file_name: "button-sm"
        files:
          - "assets/figma/variables/button-sm.json"
          - "assets/figma/variables/core-.json"
    - combine:
        file_name: "color-light"
        files:
          - "assets/figma/variables/color-accent-primary.json"
          - "assets/figma/variables/color-accent-secondary.json"
          - "assets/figma/variables/color-status-success.json"
          - "assets/figma/variables/color-status-danger.json"
          - "assets/figma/variables/color-light.json"
          - "assets/figma/variables/palette-.json"
    - combine:
        file_name: "color-dark"
        files:
          - "assets/figma/variables/color-accent-primary.json"
          - "assets/figma/variables/color-accent-secondary.json"
          - "assets/figma/variables/color-status-success.json"
          - "assets/figma/variables/color-status-danger.json"
          - "assets/figma/variables/color-dark.json"
          - "assets/figma/variables/palette-.json"
  # file_name: If set this will be the name of the merged file
  # if not, than the first file name will be used
  figma_output_paths:
    - combine:
        file_name: "button-lg"
        merge: ["button-lg"]
        files:
          - path: "assets/figma/variables/button-lg.json"
    - combine:
        file_name: "button-md"
        merge: ["button-md"]
        files:
          - path: "assets/figma/variables/button-md.json"
    - combine:
        file_name: "button-sm"
        merge: ["button-sm"]
        files:
          - path: "assets/figma/variables/button-sm.json"
    - combine:
        file_name: "color-light"
        merge: ["color-light"]
        files:
          - path: "assets/figma/variables/color-accent-primary.json"
            # if mode is set this will wrap the whole json object with a parent of the mode's value
            # this helps if you have similar trees in multiple files
            mode: "primary"
          - path: "assets/figma/variables/color-accent-secondary.json"
            mode: "secondary"
          - path: "assets/figma/variables/color-status-success.json"
            mode: "success"
          - path: "assets/figma/variables/color-status-danger.json"
            mode: "danger"
          - path: "assets/figma/variables/color-light.json"
    - combine:
        file_name: "color-dark"
        merge: ["color-dark"]
        files:
          - path: "assets/figma/variables/color-accent-primary.json"
            mode: "primary"
          - path: "assets/figma/variables/color-accent-secondary.json"
            mode: "secondary"
          - path: "assets/figma/variables/color-status-success.json"
            mode: "success"
          - path: "assets/figma/variables/color-status-danger.json"
            mode: "danger"
          - path: "assets/figma/variables/color-dark.json"
  #Output path
  style_output_path: "assets/generated_styles/"
Template config
templates:
  - settings_general:
      generate_file_path: "generated_templates"
      file_name:
        format: "DS{{style}}"
        extension: "swift"
        #case: "kebab"
    settings_custom:
      # For header and footer {{style}} is a secial variable that can be used
      header:
        - "import SwiftUI"
        - "public class DSCore{{style}} {"
      footer:
        - "}"
      template_type:
        # For themes
        - type: color
          value: "public static let {{variable_name | camel}} = {{value | color: 'Color(red: rgb_r_v1, green: rgb_g_v1, blue: rgb_b_v1, opacity: rgb_a_v1)'}}  {{description | optional: '// desc = %value'}}"
        # For Core
        - type: string
          value: "public static let {{variable_name | camel}} = {{value}}  {{description | optional: '// desc = %value'}}"
        - type: float
          value: "public static let {{variable_name | camel}} = CGFloat({{value | as_text_or_number}})  {{description | optional: '// desc = %value'}}"
        - type: boolean
          value: "public static let {{variable_name | camel}} = {{value}}  {{description | optional: '// des        c = %value'}}"
        - type: composition
          value: "{% if verticalPadding != '' %} test1: {{verticalPadding | optional: 'vertical-padding-test-first: %value'}} {% endif %}"
        - type: composition
          value: "{% if verticalPadding != '' %} test2: {{verticalPadding | optional: 'vartical-padding-test-second: %value'}} {% endif %}"
        - type: boxShadow
          value:
            - "{{variable_name}} {{color-0 | color: 'hex'}} blur: {{blur-0}} x: {{x-0}}"
            - "{{variable_name}} {{color-0 | color: 'hex'}} {{color-1 | color: 'hex'}}  blur: {{blur-0}} x: {{x-0}} blur: {{blur-1}} x: {{x-1}}"

You can use every type multiple times for more clean way of creating your values. There are many filters that can help you create the template you want (check them bellow). Also because this tool is using Liquid you can expect every filter/tags/blocks to be usable in your templates. As you can see from the above code there are if statements that check if a variable is present and if it is display something.

Optional values can be added with the optional filter. Instead of using if statements sometimes it's easier to just use the optional filter and display the value only if it exists.

Valid JSON

Both type of jsons are valid and represent the same structure becase of the forward slash, you can have infinite amount of nesting or no nesting at all.

{
  "size/XL": {
    "type": "float",
    "value": "56",
    "description": ""
  },
  "text/fr": {
    "type": "string",
    "value": "Some Text",
    "description": ""
  },
  "color/bg": {
    "type": "color",
    "value": "#000000",
    "description": ""
  }
}
{
  "size": {
    "XL": {
      "type": "float",
      "value": "56",
      "description": ""
    }
  },
  "text": {
    "fr": {
      "type": "string",
      "value": "Some Text",
      "description": ""
    }
  },
  "color": {
    "bg": {
      "type": "color",
      "value": "#000000",
      "description": ""
    }
  }
}
JSON value format

You can have as much nesting of the tree as you want as long as the end node contains valueand type(descriptionis optional)

Valid Examples and how to use JSON -> Template

   "XL": {
        "type": "float",
        "value": "56",
        "description": ""
    }
- type: float
  value: "public static let {{variable_name | camel}} = CGFloat({{value | as_text_or_number}})  {{description | optional: '// desc = %value'}}"
public static let xl = CGFloat(56)

"bold": {
   "type": "typography",
   "value": {
      "fontFamily": "Noir Pro",
      "fontSize": "16",
      "fontWeight": "Bold",
      "letterSpacing": "-0.41",
      "lineHeight": "23"
    }
}
- type: typography
  value: 'public static let {{variable_name | camel}} = TextStyle(name: "{{fontFamily}}", size: {{fontSize}}, weight: TextStyle.Weight({{fontWeight | as_text_or_number}}), lineHeight: {{lineHeight}})'
public static let bold = TextStyle(name: "Noir Pro", size: 16, weight: TextStyle.Weight("Bold"), lineHeight: 23)

    "tabBar": {
      "type": "boxShadow",
      "value": [
        {
          "blur": "20",
          "color": "rgba(0,0,0,0.1)",
          "spread": "0",
          "type": "dropShadow",
          "x": "0",
          "y": "0"
        },
        {
          "blur": "8",
          "color": "rgba(0,0,0,0.1)",
          "spread": "0",
          "type": "dropShadow",
          "x": "0",
          "y": "4"
        }
      ]
    }
- type: boxShadow
  value:
    - 'public static let {{variable_name | camel}} = Shadow(x: CGFloat({{x-0}}), y: CGFloat({{y-0}}), color: Color(hex: "{{color-0 | color: ''hex''}}"), radius: CGFloat({{blur-0}}))'
    - 'public static let {{variable_name | camel}} = [Shadow(x: CGFloat({{x-0}}), y: CGFloat({{y-0}}), color: Color(hex: "{{color-0 | color: ''hex''}}"), radius: CGFloat({{blur-0}})), Shadow(x: CGFloat({{x-1}}), y: CGFloat({{y-1}}), color: Color(hex: "{{color-1 | color: ''hex''}}"), radius: CGFloat({{blur-1}}))]'
public static let shadowTabBar = [Shadow(x: CGFloat(0), y: CGFloat(0), color: Color(hex: "#00000019"), radius: CGFloat(20)), Shadow(x: CGFloat(0), y: CGFloat(4), color: Color(hex: "#00000019"), radius: CGFloat(8))]

"dissolve": {
  "description": null,
  "type": "custom-transition",
  "value": {
    "duration": 0.45,
    "easingFunction": {
      "x1": 0.6968395709991455,
      "x2": 0.06683959811925888,
      "y1": 0.05232666060328483,
      "y2": 0.9323266744613647
    },
    "transitionType": "dissolve"
  }
}
# Object
# {{easingFunction | empty}} use empty filter to initialize variable if you need it in the scope of the template without printing it
- type: "custom-transition"
  value: "public static let {{variable_name | camel}} = CustomTransition(duration: {{duration}},
    {{easingFunction | empty}}
    x1: CGFloat({{easingFunction.x1}}),
    x2: CGFloat({{easingFunction.x2}}),
    y1: CGFloat({{easingFunction.y1}}),
    y2: CGFloat({{easingFunction.y2}}))
  "
public static let dissolve = CustomTransition(duration: 0.45,  x1: CGFloat(0.6968395709991455), x2: CGFloat(0.06683959811925888), y1: CGFloat(0.05232666060328483), y2: CGFloat(0.9323266744613647))

"gradient": {
  "single with multiple color stops": {
    "description": "Four color stops from yellow to red",
    "type": "custom-gradient",
    "value": {
      "gradientType": "radial",
      "rotation": 180,
      "stops": [
        {
          "color": "#ffb800ff",
          "position": 0
        },
        {
          "color": "#ff8a00ff",
          "position": 0.34
        },
        {
          "color": "#ff2e00ff",
          "position": 0.65
        },
        {
          "color": "#ff0000ff",
          "position": 1
        }
      ]
    }
  }
}
# Array
# {{stops | empty}} use empty filter to initialize variable if you need it in the scope of the template without printing it
- type: "custom-gradient"
  value: "public static let {{variable_name | camel}} = CustomGradient(gradientType: {{gradientType}}, rotation: {{rotation}},
  {{stops | empty}}

  color1: Style1(
    {% for stop in stops %}

    {{stop.color | color: 'Color(red: rgb_r_v1, green: rgb_g_v1, blue: rgb_b_v1, opacity: rgb_a_v1)'}}
    {% if forloop.last == false -%}, {% endif %}
    {% endfor -%}
  ),

  color2: Style2(
    {% assign colors = stops | map: 'color' %}
    {% for item in colors %}

    {{item | color: 'Color(red: rgb_r_v2, green: rgb_g_v2, blue: rgb_b_v2, opacity: rgb_a_v2)'}}
    {% if forloop.last == false -%}, {% endif %}
    {% endfor %}
  )
  "
public static let defaultGradientSingleWithMultipleColorStops = CustomGradient(gradientType: radial, rotation: 180,
color1: Style1(
Color(red: 1.000, green: 0.722, blue: 0.000, opacity: 1.000) ,
Color(red: 1.000, green: 0.541, blue: 0.000, opacity: 1.000) ,
Color(red: 1.000, green: 0.180, blue: 0.000, opacity: 1.000) ,
Color(red: 1.000, green: 0.000, blue: 0.000, opacity: 1.000)  ),
color2: Style2(
Color(red: 255, green: 184, blue: 0, opacity: 184) ,
Color(red: 255, green: 138, blue: 0, opacity: 138) ,
Color(red: 255, green: 46, blue: 0, opacity: 46) ,
Color(red: 255, green: 0, blue: 0, opacity: 0)   )
}

Use {{easingFunction | empty}} empty filter to initialize a variable without displaying it. This filter is unique to this library and necessary for a lot of the blocks that the liquid templating provides.

Keywords

You can use the keywords in the following way: {{variable_name | kebab }} name of the keyword and next to it you can add a filter, or multiple filters by separating them with | like this {{variable_name | kebab | no_space }}.

Special filters
name extra options/info
empty Initialize variable without displaying it's value
no_space If the value contains space, remove it. For example "Test No Space" will turnto "TestNoSpace"
as_text_or_number If the value is text it will add double quotes to it.
pascal,
kebab,
camel
Different case filters
color rgb_r_v1rgb_g_v1rgb_b_v1rgb_a_v1
rgb_r_v2rgb_g_v2rgb_b_v2rgb_a_v2
hex

v1 - values from 0 to 255
v2 - values from 0 to 1

You can get example of full configuration from the assets folder

Arrays have a bit different take on how you shuld template them. For example in the file the boxShadow type is working like this. Because we can expect array values with uknown length here is how you can handle them.

# All the color related values from above
# For every new line of the boxShadow value, a new index can be used. For example:
# On line 1 you have only values with index 0
# On line 2 you have values with index 0 and 1
# On line 3 you have values with index 0, 1 and 2 and etc..
# All possible variants should be made with a template
# If there is a missing one you will be notified with an error to add it
- type: boxShadow
  value:
    - "{{variable_name}} {{color-0 | color: 'hex'}} blur: {{blur-0}} x: {{x-0}}"
    - "{{variable_name}} {{color-0 | color: 'hex'}} {{color-1 | color: 'hex'}}  blur: {{blur-0}} x: {{x-0}} blur: {{blur-1}} x: {{x-1}}"
Liquid filters

Check the available filters/blocks and etc.. for the liquid templating from here:

More

For any ideas or issues please don't hasitate to ask/report.