mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
566 lines
23 KiB
Clojure
566 lines
23 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns app.main.data.workspace.tokens.application
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.files.tokens :as cft]
|
|
[app.common.types.shape.layout :as ctsl]
|
|
[app.common.types.shape.radius :as ctsr]
|
|
[app.common.types.stroke :as cts]
|
|
[app.common.types.text :as txt]
|
|
[app.common.types.token :as ctt]
|
|
[app.common.types.tokens-lib :as ctob]
|
|
[app.common.types.typography :as cty]
|
|
[app.common.uuid :as uuid]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.style-dictionary :as sd]
|
|
[app.main.data.tinycolor :as tinycolor]
|
|
[app.main.data.workspace :as udw]
|
|
[app.main.data.workspace.colors :as wdc]
|
|
[app.main.data.workspace.shape-layout :as dwsl]
|
|
[app.main.data.workspace.shapes :as dwsh]
|
|
[app.main.data.workspace.transforms :as dwtr]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.main.fonts :as fonts]
|
|
[app.main.store :as st]
|
|
[beicon.v2.core :as rx]
|
|
[clojure.set :as set]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(declare token-properties)
|
|
|
|
;; Events to update the value of attributes with applied tokens ---------------------------------------------------------
|
|
|
|
;; (note that dwsh/update-shapes function returns an event)
|
|
|
|
(defn update-shape-radius-all
|
|
([value shape-ids attributes] (update-shape-radius-all value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
|
|
(when (number? value)
|
|
(dwsh/update-shapes shape-ids
|
|
(fn [shape]
|
|
(ctsr/set-radius-to-all-corners shape value))
|
|
{:reg-objects? true
|
|
:ignore-touched true
|
|
:page-id page-id
|
|
:attrs ctt/border-radius-keys}))))
|
|
|
|
(defn update-shape-radius-for-corners
|
|
([value shape-ids attributes] (update-shape-radius-for-corners value shape-ids attributes nil))
|
|
([value shape-ids attributes page-id]
|
|
(when (number? value)
|
|
(dwsh/update-shapes shape-ids
|
|
(fn [shape]
|
|
(ctsr/set-radius-for-corners shape attributes value))
|
|
{:reg-objects? true
|
|
:ignore-touched true
|
|
:page-id page-id
|
|
:attrs ctt/border-radius-keys}))))
|
|
|
|
(defn update-opacity
|
|
([value shape-ids attributes] (update-opacity value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
|
|
(when (<= 0 value 1)
|
|
(dwsh/update-shapes shape-ids
|
|
#(assoc % :opacity value)
|
|
{:ignore-touched true
|
|
:page-id page-id}))))
|
|
|
|
;; FIXME: if attributes are not always present, maybe we have an
|
|
;; options here for pass optional value and preserve the correct and
|
|
;; uniform api across all functions (?)
|
|
|
|
(defn update-rotation
|
|
([value shape-ids attributes] (update-rotation value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
|
|
(ptk/reify ::update-shape-rotation
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(when (number? value)
|
|
(rx/of
|
|
(udw/trigger-bounding-box-cloaking shape-ids)
|
|
(udw/increase-rotation shape-ids value nil
|
|
{:page-id page-id
|
|
:ignore-touched true})))))))
|
|
|
|
(defn update-stroke-width
|
|
([value shape-ids attributes] (update-stroke-width value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions
|
|
(when (number? value)
|
|
(dwsh/update-shapes shape-ids
|
|
(fn [shape]
|
|
(if (seq (:strokes shape))
|
|
(assoc-in shape [:strokes 0 :stroke-width] value)
|
|
(let [stroke (assoc cts/default-stroke :stroke-width value)]
|
|
(assoc shape :strokes [stroke]))))
|
|
{:reg-objects? true
|
|
:ignore-touched true
|
|
:page-id page-id
|
|
:attrs [:strokes]}))))
|
|
|
|
(defn update-color [f value shape-ids page-id]
|
|
(when-let [tc (tinycolor/valid-color value)]
|
|
(let [hex (tinycolor/->hex-string tc)
|
|
opacity (tinycolor/alpha tc)]
|
|
(f shape-ids {:color hex :opacity opacity} 0 {:ignore-touched true
|
|
:page-id page-id}))))
|
|
|
|
(defn- value->color
|
|
"Transform a token color value into penpot color data structure"
|
|
[color]
|
|
(when-let [tc (tinycolor/valid-color color)]
|
|
(let [hex (tinycolor/->hex-string tc)
|
|
opacity (tinycolor/alpha tc)]
|
|
{:color hex :opacity opacity})))
|
|
|
|
(defn update-fill
|
|
([value shape-ids attributes]
|
|
(update-fill value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id] ;; The attributes param is needed to have the same arity that other update functions
|
|
(ptk/reify ::update-fill
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-let [color (value->color value)]
|
|
(let [update-fn #(wdc/assoc-shape-fill %1 0 %2)]
|
|
(wdc/transform-fill state shape-ids color update-fn {:ignore-touched true
|
|
:page-id page-id})))))))
|
|
|
|
(defn update-stroke-color
|
|
([value shape-ids attributes]
|
|
(update-stroke-color value shape-ids attributes nil))
|
|
|
|
;; The attributes param is needed to have the same arity that other update functions
|
|
([value shape-ids _attributes page-id]
|
|
(when-let [color (value->color value)]
|
|
(dwsh/update-shapes shape-ids
|
|
#(wdc/update-shape-stroke-color % 0 color)
|
|
{:page-id page-id
|
|
:ignore-touched true
|
|
:changed-sub-attr [:stroke-color]}))))
|
|
|
|
(defn update-fill-stroke
|
|
([value shape-ids attributes]
|
|
(update-fill-stroke value shape-ids attributes nil))
|
|
([value shape-ids attributes page-id]
|
|
(ptk/reify ::update-fill-stroke
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of
|
|
(when (:fill attributes) (update-fill value shape-ids attributes page-id))
|
|
(when (:stroke-color attributes) (update-stroke-color value shape-ids attributes page-id)))))))
|
|
|
|
(defn update-shape-dimensions
|
|
([value shape-ids attributes] (update-shape-dimensions value shape-ids attributes nil))
|
|
([value shape-ids attributes page-id]
|
|
(ptk/reify ::update-shape-dimensions
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(when (number? value)
|
|
(rx/of
|
|
(when (:width attributes) (dwtr/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id}))
|
|
(when (:height attributes) (dwtr/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id}))))))))
|
|
|
|
(defn- attributes->layout-gap [attributes value]
|
|
(let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap})
|
|
(zipmap (repeat value)))]
|
|
{:layout-gap layout-gap}))
|
|
|
|
(defn- shape-ids-with-layout [state page-id shape-ids]
|
|
(->> (dsh/lookup-shapes state page-id shape-ids)
|
|
(eduction
|
|
(filter ctsl/any-layout?)
|
|
(map :id))))
|
|
|
|
(defn- shape-ids-with-layout-parent [state page-id shape-ids]
|
|
(let [objects (dsh/lookup-page-objects state)]
|
|
(->> (dsh/lookup-shapes state page-id shape-ids)
|
|
(eduction
|
|
(filter #(ctsl/any-layout-immediate-child? objects %))
|
|
(map :id)))))
|
|
|
|
(defn update-layout-item-margin
|
|
([value shape-ids attrs] (update-layout-item-margin value shape-ids attrs nil))
|
|
([value shape-ids attrs page-id]
|
|
(ptk/reify ::update-layout-item-margin
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (number? value)
|
|
(let [ids-with-layout-parent (shape-ids-with-layout-parent state (or page-id (:current-page-id state)) shape-ids)]
|
|
(rx/of
|
|
(dwsl/update-layout ids-with-layout-parent
|
|
{:layout-item-margin (zipmap attrs (repeat value))}
|
|
{:ignore-touched true
|
|
:page-id page-id}))))))))
|
|
|
|
(defn update-layout-padding
|
|
([value shape-ids attrs] (update-layout-padding value shape-ids attrs nil))
|
|
([value shape-ids attrs page-id]
|
|
(ptk/reify ::update-layout-padding
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (number? value)
|
|
(let [ids-with-layout (shape-ids-with-layout state (or page-id (:current-page-id state)) shape-ids)]
|
|
(rx/of
|
|
(dwsl/update-layout ids-with-layout
|
|
{:layout-padding (zipmap attrs (repeat value))}
|
|
{:ignore-touched true
|
|
:page-id page-id}))))))))
|
|
|
|
(defn update-layout-spacing
|
|
([value shape-ids attributes] (update-layout-spacing value shape-ids attributes nil))
|
|
([value shape-ids attributes page-id]
|
|
(ptk/reify ::update-layout-spacing
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (number? value)
|
|
(let [ids-with-layout (shape-ids-with-layout state (or page-id (:current-page-id state)) shape-ids)
|
|
layout-attributes (attributes->layout-gap attributes value)]
|
|
(rx/of
|
|
(dwsl/update-layout ids-with-layout
|
|
layout-attributes
|
|
{:ignore-touched true
|
|
:page-id page-id}))))))))
|
|
|
|
(defn update-shape-position
|
|
([value shape-ids attributes] (update-shape-position value shape-ids attributes nil))
|
|
([value shape-ids attributes page-id]
|
|
(ptk/reify ::update-shape-position
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when (number? value)
|
|
(let [page-id (or page-id (get state :current-page-id))]
|
|
(->> (rx/from shape-ids)
|
|
(rx/map #(dwtr/update-position % (zipmap attributes (repeat value))
|
|
{:ignore-touched true
|
|
:page-id page-id})))))))))
|
|
|
|
(defn update-layout-sizing-limits
|
|
([value shape-ids attributes] (update-layout-sizing-limits value shape-ids attributes nil))
|
|
([value shape-ids attributes page-id]
|
|
(ptk/reify ::update-layout-sizing-limits
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(when (number? value)
|
|
(let [props (-> {:layout-item-min-w value
|
|
:layout-item-min-h value
|
|
:layout-item-max-w value
|
|
:layout-item-max-h value}
|
|
(select-keys attributes))]
|
|
(rx/of
|
|
(dwsl/update-layout-child shape-ids props {:ignore-touched true
|
|
:page-id page-id}))))))))
|
|
|
|
(defn generate-text-shape-update
|
|
[txt-attrs shape-ids page-id]
|
|
(let [update-node? (fn [node]
|
|
(or (txt/is-text-node? node)
|
|
(txt/is-paragraph-node? node)))
|
|
update-fn (fn [node _]
|
|
(-> node
|
|
(d/txt-merge txt-attrs)
|
|
(cty/remove-typography-from-node)))]
|
|
(dwsh/update-shapes shape-ids
|
|
#(txt/update-text-content % update-node? update-fn nil)
|
|
{:ignore-touched true
|
|
:page-id page-id})))
|
|
|
|
(defn update-line-height
|
|
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id]
|
|
(when (number? value)
|
|
(generate-text-shape-update {:line-height value} shape-ids page-id))))
|
|
|
|
(defn update-letter-spacing
|
|
([value shape-ids attributes] (update-letter-spacing value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id]
|
|
(when (number? value)
|
|
(generate-text-shape-update {:letter-spacing (str value)} shape-ids page-id))))
|
|
|
|
(defn update-font-family
|
|
([value shape-ids attributes] (update-font-family value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id]
|
|
(let [font-family (first value)
|
|
font (some-> font-family
|
|
(fonts/find-font-family))
|
|
text-attrs (if font
|
|
{:font-id (:id font)
|
|
:font-family (:family font)}
|
|
{:font-id (str uuid/zero)
|
|
:font-family font-family})]
|
|
(when text-attrs
|
|
(generate-text-shape-update text-attrs shape-ids page-id)))))
|
|
|
|
(defn update-font-size
|
|
([value shape-ids attributes] (update-font-size value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id]
|
|
(when (number? value)
|
|
(generate-text-shape-update {:font-size (str value)} shape-ids page-id))))
|
|
|
|
(defn update-text-case
|
|
([value shape-ids attributes] (update-text-case value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id]
|
|
(when (string? value)
|
|
(generate-text-shape-update {:text-transform value} shape-ids page-id))))
|
|
|
|
(defn update-text-decoration
|
|
([value shape-ids attributes] (update-text-decoration value shape-ids attributes nil))
|
|
([value shape-ids _attributes page-id]
|
|
(when (string? value)
|
|
(let [css-value (case value
|
|
"strike-through" "line-through"
|
|
value)]
|
|
(generate-text-shape-update {:text-decoration css-value} shape-ids page-id)))))
|
|
|
|
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
|
|
|
|
(defn apply-token
|
|
"Apply `attributes` that match `token` for `shape-ids`.
|
|
|
|
Optionally remove attributes from `attributes-to-remove`,
|
|
this is useful for applying a single attribute from an attributes set
|
|
while removing other applied tokens from this set."
|
|
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape]}]
|
|
(ptk/reify ::apply-token
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
;; We do not allow to apply tokens while text editor is open.
|
|
(when (empty? (get state :workspace-editor-state))
|
|
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
|
(get :tokens-lib)
|
|
(ctob/get-tokens-in-active-sets))]
|
|
(->> (sd/resolve-tokens tokens)
|
|
(rx/mapcat
|
|
(fn [resolved-tokens]
|
|
(let [undo-id (js/Symbol)
|
|
objects (dsh/lookup-page-objects state)
|
|
selected-shapes (select-keys objects shape-ids)
|
|
|
|
shape-ids (or (->> selected-shapes
|
|
(filter (fn [[_ shape]]
|
|
(or
|
|
(and (ctsl/any-layout-immediate-child? objects shape)
|
|
(some ctt/spacing-margin-keys attributes))
|
|
(ctt/any-appliable-attr? attributes (:type shape)))))
|
|
(keys))
|
|
[])
|
|
|
|
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
|
|
tokenized-attributes (cft/attributes-map attributes token)]
|
|
(rx/of
|
|
(st/emit! (ptk/event ::ev/event {::ev/name "apply-tokens"}))
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dwsh/update-shapes shape-ids (fn [shape]
|
|
(cond-> shape
|
|
attributes-to-remove
|
|
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
|
:always
|
|
(update :applied-tokens merge tokenized-attributes))))
|
|
(when on-update-shape
|
|
(on-update-shape resolved-value shape-ids attributes))
|
|
(dwu/commit-undo-transaction undo-id)))))))))))
|
|
|
|
(defn apply-spacing-token
|
|
"Handles edge-case for spacing token when applying token via toggle button.
|
|
Splits out `shape-ids` into seperate default actions:
|
|
- Layouts take the `default` update function
|
|
- Shapes inside layout will only take margin"
|
|
[{:keys [token shapes]}]
|
|
(ptk/reify ::apply-spacing-token
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
|
|
{:keys [attributes on-update-shape]}
|
|
(get token-properties (:type token))
|
|
|
|
{:keys [other frame-children]}
|
|
(group-by #(if (ctsl/any-layout-immediate-child? objects %) :frame-children :other) shapes)]
|
|
|
|
(rx/of
|
|
(apply-token {:attributes attributes
|
|
:token token
|
|
:shape-ids (map :id other)
|
|
:on-update-shape on-update-shape})
|
|
(apply-token {:attributes ctt/spacing-margin-keys
|
|
:token token
|
|
:shape-ids (map :id frame-children)
|
|
:on-update-shape update-layout-item-margin}))))))
|
|
|
|
(defn unapply-token
|
|
"Removes `attributes` that match `token` for `shape-ids`.
|
|
|
|
Doesn't update shape attributes."
|
|
[{:keys [attributes token shape-ids] :as _props}]
|
|
(ptk/reify ::unapply-token
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of
|
|
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
|
|
(dwsh/update-shapes
|
|
shape-ids
|
|
(fn [shape]
|
|
(update shape :applied-tokens remove-token))))))))
|
|
|
|
(defn toggle-token
|
|
[{:keys [token shapes]}]
|
|
(ptk/reify ::on-toggle-token
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [{:keys [attributes all-attributes on-update-shape]}
|
|
(get token-properties (:type token))
|
|
|
|
unapply-tokens?
|
|
(cft/shapes-token-applied? token shapes (or all-attributes attributes))
|
|
|
|
shape-ids (map :id shapes)]
|
|
(if unapply-tokens?
|
|
(rx/of
|
|
(unapply-token {:attributes (or all-attributes attributes)
|
|
:token token
|
|
:shape-ids shape-ids}))
|
|
(rx/of
|
|
(case (:type token)
|
|
:spacing
|
|
(apply-spacing-token {:token token
|
|
:shapes shapes})
|
|
(apply-token {:attributes attributes
|
|
:token token
|
|
:shape-ids shape-ids
|
|
:on-update-shape on-update-shape}))))))))
|
|
|
|
;; Map token types to different properties used along the cokde ---------------------------------------------
|
|
|
|
;; FIXME: the values should be lazy evaluated, probably a function,
|
|
;; becasue on future we will need to translate that labels and that
|
|
;; can not be done statically
|
|
|
|
(def token-properties
|
|
"A map of default properties by token type"
|
|
(d/ordered-map
|
|
:border-radius
|
|
{:title "Border Radius"
|
|
:attributes ctt/border-radius-keys
|
|
:on-update-shape update-shape-radius-all
|
|
:modal {:key :tokens/border-radius
|
|
:fields [{:label "Border Radius"
|
|
:key :border-radius}]}}
|
|
|
|
:color
|
|
{:title "Color"
|
|
:attributes #{:fill}
|
|
:all-attributes ctt/color-keys
|
|
:on-update-shape update-fill-stroke
|
|
:modal {:key :tokens/color
|
|
:fields [{:label "Color" :key :color}]}}
|
|
|
|
:font-size
|
|
{:title "Font Size"
|
|
:attributes ctt/font-size-keys
|
|
:on-update-shape update-font-size
|
|
:modal {:key :tokens/font-size
|
|
:fields [{:label "Font Size"
|
|
:key :font-size}]}}
|
|
|
|
:letter-spacing
|
|
{:title "Letter Spacing"
|
|
:attributes ctt/letter-spacing-keys
|
|
:on-update-shape update-letter-spacing
|
|
:modal {:key :tokens/letter-spacing
|
|
:fields [{:label "Letter Spacing"
|
|
:key :letter-spacing}]}}
|
|
|
|
:font-family
|
|
{:title "Font Family"
|
|
:attributes ctt/font-family-keys
|
|
:on-update-shape update-font-family
|
|
:modal {:key :tokens/font-family
|
|
:fields [{:label "Font Family"
|
|
:key :font-family}]}}
|
|
|
|
:text-case
|
|
{:title "Text Case"
|
|
:attributes ctt/text-case-keys
|
|
:on-update-shape update-text-case
|
|
:modal {:key :tokens/text-case
|
|
:fields [{:label "Text Case"
|
|
:key :text-case}]}}
|
|
|
|
:text-decoration
|
|
{:title "Text Decoration"
|
|
:attributes ctt/text-decoration-keys
|
|
:on-update-shape update-text-decoration
|
|
:modal {:key :tokens/text-decoration
|
|
:fields [{:label "Text Decoration"
|
|
:key :text-decoration}]}}
|
|
|
|
:stroke-width
|
|
{:title "Stroke Width"
|
|
:attributes ctt/stroke-width-keys
|
|
:on-update-shape update-stroke-width
|
|
:modal {:key :tokens/stroke-width
|
|
:fields [{:label "Stroke Width"
|
|
:key :stroke-width}]}}
|
|
|
|
:sizing
|
|
{:title "Sizing"
|
|
:attributes #{:width :height}
|
|
:all-attributes ctt/sizing-keys
|
|
:on-update-shape update-shape-dimensions
|
|
:modal {:key :tokens/sizing
|
|
:fields [{:label "Sizing"
|
|
:key :sizing}]}}
|
|
:dimensions
|
|
{:title "Dimensions"
|
|
:attributes #{:width :height}
|
|
:all-attributes (set/union
|
|
ctt/spacing-keys
|
|
ctt/sizing-keys
|
|
ctt/border-radius-keys
|
|
ctt/axis-keys
|
|
ctt/stroke-width-keys)
|
|
:on-update-shape update-shape-dimensions
|
|
:modal {:key :tokens/dimensions
|
|
:fields [{:label "Dimensions"
|
|
:key :dimensions}]}}
|
|
|
|
:opacity
|
|
{:title "Opacity"
|
|
:attributes ctt/opacity-keys
|
|
:on-update-shape update-opacity
|
|
:modal {:key :tokens/opacity
|
|
:fields [{:label "Opacity"
|
|
:key :opacity}]}}
|
|
|
|
:number
|
|
{:title "Number"
|
|
:attributes ctt/rotation-keys
|
|
:all-attributes ctt/number-keys
|
|
:on-update-shape update-rotation
|
|
:modal {:key :tokens/number
|
|
:fields [{:label "Number"
|
|
:key :number}]}}
|
|
|
|
:rotation
|
|
{:title "Rotation"
|
|
:attributes ctt/rotation-keys
|
|
:on-update-shape update-rotation
|
|
:modal {:key :tokens/rotation
|
|
:fields [{:label "Rotation"
|
|
:key :rotation}]}}
|
|
:spacing
|
|
{:title "Spacing"
|
|
:attributes #{:column-gap :row-gap}
|
|
:all-attributes ctt/spacing-keys
|
|
:on-update-shape update-layout-spacing
|
|
:modal {:key :tokens/spacing
|
|
:fields [{:label "Spacing"
|
|
:key :spacing}]}}))
|
|
|
|
(defn get-token-properties [token]
|
|
(get token-properties (:type token)))
|