penpot/frontend/src/app/main/data/tokenscript.cljs

171 lines
5.8 KiB
Clojure

(ns app.main.data.tokenscript
(:require
["@penpot/tokenscript" :refer [BaseSymbolType
ColorSymbol
ListSymbol NumberSymbol
NumberWithUnitSymbol
ProcessorError
processTokens
TokenSymbol
makeConfig]]
[app.common.logging :as l]
[app.common.time :as ct]
[app.main.data.workspace.tokens.errors :as wte]))
(l/set-level! :debug)
;; Config ----------------------------------------------------------------------
(def config (makeConfig))
;; Class predicates ------------------------------------------------------------
;; Predicates to get information about the tokenscript interpreter symbol type
;; Or to determine the error
(defn tokenscript-symbol? [v]
(instance? BaseSymbolType v))
(defn structured-token? [v]
(instance? TokenSymbol v))
(defn structured-record-token? [^js v]
(and (structured-token? v) (instance? js/Map (.-value v))))
(defn structured-array-token? [^js v]
(and (structured-token? v) (instance? js/Array (.-value v))))
(defn number-with-unit-symbol? [v]
(instance? NumberWithUnitSymbol v))
(defn number-symbol? [v]
(instance? NumberSymbol v))
(defn list-symbol? [v]
(instance? ListSymbol v))
(defn color-symbol? [v]
(instance? ColorSymbol v))
(defn processor-error? [err]
(instance? ProcessorError err))
;; Conversion Tools ------------------------------------------------------------
;; Helpers to convert tokenscript symbols to penpot accepted formats
(defn color-symbol->hex-string [^js v]
(when (color-symbol? v)
(.toString (.to v "hex"))))
(defn color-alpha [^js v]
(if (.isHex v)
1
(or (.getAttribute v "alpha") 1)))
(defn color-symbol->penpot-color [^js v]
{:color (color-symbol->hex-string v)
:opacity (color-alpha v)})
(defn rem-number-with-unit? [v]
(and (number-with-unit-symbol? v)
(= (.-unit v) "rem")))
(defn percent-number-with-unit? [v]
(and (number-with-unit-symbol? v)
(= (.-unit v) "%")))
(defn rem->px [^js v]
(* (.-value v) 16))
(declare tokenscript-symbols->penpot-unit)
(defn structured-token->penpot-map
"Converts structured token (record or array) to penpot map format.
Structured tokens are non-primitive token types like `typography` or `box-shadow`."
[^js token-symbol]
(if (instance? js/Array (.-value token-symbol))
(mapv tokenscript-symbols->penpot-unit (.-value token-symbol))
(let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
(into {} (map (fn [[k v :as V]]
[(keyword k) (tokenscript-symbols->penpot-unit v)])
entries)))))
(defn tokenscript-symbols->penpot-unit [^js v]
(cond
(nil? v) nil
(structured-token? v) (structured-token->penpot-map v)
(list-symbol? v) (structured-token->penpot-map v)
(color-symbol? v) (.-value (.to v "hex"))
(rem-number-with-unit? v) (rem->px v)
(percent-number-with-unit? v) (/ (.-value v) 100)
:else (.-value v)))
;; Processors ------------------------------------------------------------------
;; The processor resolves tokens
;; resolved/error tokens get put back into a clojure structure directly during build time
;; For updating tokens we use the `TokenResolver` crud methods from the processing result
;; The `TokenResolver` has detailed information for each tokens dependency graph
(defn create-token-builder
"Collects resolved tokens during build time into a clojure structure.
Returns Tokenscript Symbols in `:resolved-value` key."
[tokens]
(let [output (volatile! tokens)
;; When a token is resolved (No parsing / reference errors) we assing `:resolved-value` for the original token
on-resolve
(fn [^js/String token-name ^js/Symbol resolved-symbol]
(vswap! output assoc-in [token-name :resolved-value] resolved-symbol))
;; When a token contains any errors we assing `:errors` for the original token
on-error
(fn [^js/String token-name ^js/Error _error ^js/String _original-value]
(let [value (get tokens token-name)
default-error [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]]
(vswap! output assoc-in [token-name :errors] default-error)))
;; Extract the atom value
get-result
(fn [] @output)]
#js {:onResolve on-resolve
:onError on-error
:getResult get-result}))
(defn clj->token->tokenscript-token
"Convert penpot token into a format that tokenscript can handle."
[{:keys [type value]}]
#js {"$type" (name type)
"$value" (clj->js value)})
(defn clj-tokens->tokenscript-tokens
"Convert penpot map of tokens into tokenscript map structure.
tokenscript accepts a map of [token-name {\"$type\": string, \"$value\": any}]"
[tokens]
(let [token-map (js/Map.)]
(doseq [[k token] tokens]
(.set token-map k (clj->token->tokenscript-token token)))
token-map))
(defn process-tokens
"Builds tokens using `tokenscript`."
[tokens]
(let [input (clj-tokens->tokenscript-tokens tokens)
result (processTokens input #js {:config config
:builder (create-token-builder tokens)})]
result))
(defn update-token
[tokens token]
(let [result (process-tokens tokens)
resolver (.-resolver result)]
(.updateToken resolver #js {:tokenPath (:name token)
:tokenData (clj->token->tokenscript-token token)})))
;; Main ------------------------------------------------------------------------
(defn resolve-tokens [tokens]
(let [tpoint (ct/tpoint-ms)
result (process-tokens tokens)
elapsed (tpoint)]
(l/dbg :hint "tokenscript/resolve-tokens" :elapsed elapsed)
(.-output result)))