mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 05:50:35 +00:00
✨ Resolve tokens with the tokenscript-interpreter package (#12348)
Replace the @penpot/tokenscript npm package (the TypeScript interpreter) with penpot/tokenscript-interpreter, a Clojure and ClojureScript interpreter that now lives in its own repository and compiles into the frontend build. The frontend adapter (app.main.data.tokenscript) resolves tokens with it behind the existing :tokenscript flag and maps its results to the values and errors the workspace uses: - Token values are validated by the interpreter's token types, passed by their DTCG names: `1foo` is not a dimension, a number has no unit, an opacity is between 0 and 1. Penpot only adds its own limits (font weight names, text decoration keywords). - Division by zero, incompatible units, unknown functions and function arguments out of range get messages of their own, and token pills show the message of their error. - Font family names are quoted before resolving, as in the adapter this replaces (#11750): `Red Hat` stays one name. The token form's value input passed the token name to the tokenscript resolver in place of the typed value, and no name at all for a new token. It now resolves the typed value under the form's token name, as the StyleDictionary path does. StyleDictionary stays the default until the interpreter is stable. frontend/deps.edn pins the interpreter by tag and commit, v0.1.0. Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
This commit is contained in:
parent
ba78ccb8c9
commit
9aa78a536b
@ -664,6 +664,14 @@
|
||||
(when (contains? text-decoration-values normalized-value)
|
||||
normalized-value)))
|
||||
|
||||
(def text-case-values #{"none" "uppercase" "lowercase" "capitalize"})
|
||||
|
||||
(defn number-out-of-bounds?
|
||||
"Whether a resolved numeric token value is outside the safe integer
|
||||
range."
|
||||
[n]
|
||||
(or (>= n sm/max-safe-int) (<= n sm/min-safe-int)))
|
||||
|
||||
(def font-weight-aliases
|
||||
{"100" #{"thin" "hairline"},
|
||||
"200" #{"ultra light" "extralight" "extraleicht" "extra-light" "ultra-light" "ultralight" "extra light"},
|
||||
|
||||
@ -30,6 +30,11 @@
|
||||
:git/sha "d726717"
|
||||
:git/url "https://github.com/funcool/rumext.git"}
|
||||
|
||||
penpot/tokenscript-interpreter
|
||||
{:git/tag "v0.1.0"
|
||||
:git/sha "224547d"
|
||||
:git/url "https://github.com/penpot/tokenscript-interpreter.git"}
|
||||
|
||||
instaparse/instaparse {:mvn/version "1.5.0"}
|
||||
garden/garden {:git/url "https://github.com/noprompt/garden"
|
||||
:git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"}
|
||||
|
||||
@ -56,7 +56,6 @@
|
||||
"@penpot/plugins-runtime": "link:../plugins/libs/plugins-runtime",
|
||||
"@penpot/svgo": "penpot/svgo#3.3.0",
|
||||
"@penpot/text-editor": "link:text-editor",
|
||||
"@penpot/tokenscript": "link:packages/tokenscript",
|
||||
"@penpot/ua-parser": "penpot/ua-parser#1.0.0",
|
||||
"@playwright/test": "1.63.0",
|
||||
"@storybook/addon-docs": "10.6.1",
|
||||
|
||||
3
frontend/packages/tokenscript/.gitignore
vendored
3
frontend/packages/tokenscript/.gitignore
vendored
@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
*.sublime-workspace
|
||||
/.yarn
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./schemas.js";
|
||||
export * from "@tokens-studio/tokenscript-interpreter";
|
||||
@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "@penpot/tokenscript",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@12.14.0+sha512.c18ba6d87993d3d164cb5e8948f32b2c06ee854487e06cd8d86781342fb3e5bb4634367034711e86802baa9c37bea906ce1929ad08d70884c97d801df13389a6",
|
||||
"author": "Andrey Antukh",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"@tokens-studio/tokenscript-interpreter": "^0.26.0"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
68
frontend/pnpm-lock.yaml
generated
68
frontend/pnpm-lock.yaml
generated
@ -210,9 +210,6 @@ importers:
|
||||
'@penpot/text-editor':
|
||||
specifier: link:text-editor
|
||||
version: link:text-editor
|
||||
'@penpot/tokenscript':
|
||||
specifier: link:packages/tokenscript
|
||||
version: link:packages/tokenscript
|
||||
'@penpot/ua-parser':
|
||||
specifier: penpot/ua-parser#1.0.0
|
||||
version: https://codeload.github.com/penpot/ua-parser/tar.gz/90b970f39f2dc08378b975a0f01045b4ec8e89a4
|
||||
@ -445,12 +442,6 @@ importers:
|
||||
|
||||
packages/mousetrap: {}
|
||||
|
||||
packages/tokenscript:
|
||||
dependencies:
|
||||
'@tokens-studio/tokenscript-interpreter':
|
||||
specifier: ^0.26.0
|
||||
version: 0.26.0
|
||||
|
||||
packages/ui:
|
||||
dependencies:
|
||||
react:
|
||||
@ -547,12 +538,6 @@ packages:
|
||||
'@adobe/css-tools@4.5.0':
|
||||
resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==}
|
||||
|
||||
'@ark/schema@0.56.6':
|
||||
resolution: {integrity: sha512-OmLv80BCF2ib+zjecrXDosip/CSnZSYBGjblhqWYZWFOzdLQka6QymHToE9u7Z/HZW2KA8Hwc8MCNXoI3uGSIA==}
|
||||
|
||||
'@ark/util@0.56.6':
|
||||
resolution: {integrity: sha512-mHqq0f0HyDd9YadpFl25w0QECAP7d8HGIUpEC85duD+L+xr4i/CwfOid/Z7zJFFF/47oLt6+pZB9RbqWpBYTYg==}
|
||||
|
||||
'@asamuzakjp/css-color@7.1.3':
|
||||
resolution: {integrity: sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==}
|
||||
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
|
||||
@ -1902,11 +1887,6 @@ packages:
|
||||
peerDependencies:
|
||||
style-dictionary: ^5.0.0
|
||||
|
||||
'@tokens-studio/tokenscript-interpreter@0.26.0':
|
||||
resolution: {integrity: sha512-dGjvUJnXRspWYp98FZw43l4cN+0ey/cF5sEJjL3coKc5C7DY7MsKgkmOONizmaZqf13GUIzklTEas3gt3jvrOQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@tokens-studio/types@0.5.2':
|
||||
resolution: {integrity: sha512-rzMcZP0bj2E5jaa7Fj0LGgYHysoCrbrxILVbT0ohsCUH5uCHY/u6J7Qw/TE0n6gR9Js/c9ZO9T8mOoz0HdLMbA==}
|
||||
|
||||
@ -2134,12 +2114,6 @@ packages:
|
||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
arkregex@0.0.12:
|
||||
resolution: {integrity: sha512-sOxL0eX50pnRYO4OyajKZChGuuj/iHwC5tysiAaGOPBu+zJwJQckWn7/DRH2++ol5uaKpyRn7N0hKT7nDqGmgg==}
|
||||
|
||||
arktype@2.2.7:
|
||||
resolution: {integrity: sha512-AbgM5SQvwCTURSR+ENfPtOG7cL5VZ0hqOVtoWyXKw2SviFddZhCBEUAjjVJYKCNFlPPR1VX5LGUKxsSqqc4Kdw==}
|
||||
|
||||
array-buffer-byte-length@1.0.2:
|
||||
resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3873,9 +3847,6 @@ packages:
|
||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||
engines: {node: '>= 14.16'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
picocolors@0.2.1:
|
||||
resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==}
|
||||
|
||||
@ -4174,10 +4145,6 @@ packages:
|
||||
resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
readline-sync@1.4.10:
|
||||
resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
recast@0.23.21:
|
||||
resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -5203,10 +5170,6 @@ packages:
|
||||
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
|
||||
yauzl@3.4.0:
|
||||
resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@1.2.2:
|
||||
resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
|
||||
engines: {node: '>=12.20'}
|
||||
@ -5215,12 +5178,6 @@ snapshots:
|
||||
|
||||
'@adobe/css-tools@4.5.0': {}
|
||||
|
||||
'@ark/schema@0.56.6':
|
||||
dependencies:
|
||||
'@ark/util': 0.56.6
|
||||
|
||||
'@ark/util@0.56.6': {}
|
||||
|
||||
'@asamuzakjp/css-color@7.1.3':
|
||||
dependencies:
|
||||
'@csstools/css-calc': 3.4.3(@csstools/css-parser-algorithms@4.0.2(@csstools/css-tokenizer@4.0.2))(@csstools/css-tokenizer@4.0.2)
|
||||
@ -6461,13 +6418,6 @@ snapshots:
|
||||
is-mergeable-object: 1.1.1
|
||||
style-dictionary: 5.5.5
|
||||
|
||||
'@tokens-studio/tokenscript-interpreter@0.26.0':
|
||||
dependencies:
|
||||
arktype: 2.2.7
|
||||
commander: 14.0.3
|
||||
readline-sync: 1.4.10
|
||||
yauzl: 3.4.0
|
||||
|
||||
'@tokens-studio/types@0.5.2': {}
|
||||
|
||||
'@tybys/wasm-util@0.10.4':
|
||||
@ -6717,16 +6667,6 @@ snapshots:
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
arkregex@0.0.12:
|
||||
dependencies:
|
||||
'@ark/util': 0.56.6
|
||||
|
||||
arktype@2.2.7:
|
||||
dependencies:
|
||||
'@ark/schema': 0.56.6
|
||||
'@ark/util': 0.56.6
|
||||
arkregex: 0.0.12
|
||||
|
||||
array-buffer-byte-length@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@ -8555,8 +8495,6 @@ snapshots:
|
||||
|
||||
pathval@2.0.1: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
picocolors@0.2.1: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@ -8897,8 +8835,6 @@ snapshots:
|
||||
|
||||
readdirp@5.1.1: {}
|
||||
|
||||
readline-sync@1.4.10: {}
|
||||
|
||||
recast@0.23.21:
|
||||
dependencies:
|
||||
ast-types: 0.16.3
|
||||
@ -10018,8 +9954,4 @@ snapshots:
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 22.0.0
|
||||
|
||||
yauzl@3.4.0:
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
|
||||
yocto-queue@1.2.2: {}
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
["style-dictionary$default" :as sd]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
@ -86,8 +85,7 @@
|
||||
(let [number? (or (number? value)
|
||||
(numeric-string? value))
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
|
||||
(<= (:value parsed-value) sm/min-safe-int))]
|
||||
out-of-bounds (cto/number-out-of-bounds? (:value parsed-value))]
|
||||
|
||||
(cond
|
||||
(and parsed-value (not out-of-bounds) number?)
|
||||
@ -112,8 +110,7 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(let [parsed-value (cfo/parse-token-value value)
|
||||
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
|
||||
(<= (:value parsed-value) sm/min-safe-int))]
|
||||
out-of-bounds (cto/number-out-of-bounds? (:value parsed-value))]
|
||||
(if (and parsed-value (not out-of-bounds))
|
||||
parsed-value
|
||||
(if out-of-bounds
|
||||
@ -185,7 +182,7 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(let [normalized-value (str/lower (str/trim value))
|
||||
valid? (contains? #{"none" "uppercase" "lowercase" "capitalize"} normalized-value)
|
||||
valid? (contains? cto/text-case-values normalized-value)
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
(cond
|
||||
valid?
|
||||
|
||||
@ -1,166 +1,255 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns app.main.data.tokenscript
|
||||
"Resolves the workspace tokens with the tokenscript interpreter
|
||||
(`tokenscript.core`, from penpot/tokenscript-interpreter).
|
||||
|
||||
`resolve-tokens` takes the usual map of token name to token and returns
|
||||
the same map with `:resolved-value` (a value ready for shape attributes:
|
||||
numbers in pixels, colors as hex strings, composites as maps),
|
||||
`:resolved-symbol` (the tokenscript value, see
|
||||
`tokenscript.symbols`) and `:errors` (Penpot error maps, see
|
||||
`app.main.data.workspace.tokens.errors`) added to each token."
|
||||
(:require
|
||||
["@penpot/tokenscript" :refer [BaseSymbolType
|
||||
ColorSymbol
|
||||
ListSymbol NumberSymbol
|
||||
NumberWithUnitSymbol
|
||||
ProcessorError
|
||||
processTokens
|
||||
TokenSymbol
|
||||
makeConfig]]
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.token :as cto]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[cuerdas.core :as str]))
|
||||
[cuerdas.core :as str]
|
||||
[tokenscript.core :as tsc]
|
||||
[tokenscript.errors :as tse]
|
||||
[tokenscript.symbols :as sym]))
|
||||
|
||||
(l/set-level! :debug)
|
||||
;; Value conversion -----------------------------------------------------------
|
||||
|
||||
;; Config ----------------------------------------------------------------------
|
||||
(defn- color->hex-string
|
||||
"Hex string of a color symbol (`#rrggbbaa` when it has alpha)."
|
||||
[color]
|
||||
(sym/to-string (tsc/color->hex color)))
|
||||
|
||||
(def config (makeConfig))
|
||||
(defn- symbol->value
|
||||
"Converts a resolved tokenscript value into the shape used by shape
|
||||
attributes and the token forms:
|
||||
|
||||
;; Class predicates ------------------------------------------------------------
|
||||
;; Predicates to get information about the tokenscript interpreter symbol type
|
||||
;; Or to determine the error
|
||||
- colors: hex strings (alpha as `#rrggbbaa`)
|
||||
- `rem` values: pixels (16px root font size), `%` values: fractions
|
||||
- other numbers with unit: the number
|
||||
- structured tokens (typography, shadows): maps with keyword keys and
|
||||
vectors
|
||||
- null: nil"
|
||||
[v]
|
||||
(let [entries->map (fn [entries]
|
||||
(into {} (map (fn [[k v]] [(keyword k) (symbol->value v)])) entries))]
|
||||
(cond
|
||||
(nil? v) nil
|
||||
(sym/null? v) nil
|
||||
(sym/token? v) (let [content (sym/value v)]
|
||||
(if (map? content)
|
||||
(entries->map content)
|
||||
(mapv symbol->value content)))
|
||||
(sym/list? v) (mapv symbol->value (sym/value v))
|
||||
(sym/dictionary? v) (entries->map (sym/value v))
|
||||
(sym/color? v) (color->hex-string v)
|
||||
(sym/number-with-unit? v) (let [n (sym/value v)]
|
||||
(case (:unit v)
|
||||
"rem" (* n 16)
|
||||
"%" (/ n 100)
|
||||
n))
|
||||
(sym/symbol? v) (sym/value v)
|
||||
:else v)))
|
||||
|
||||
(defn tokenscript-symbol? [v]
|
||||
(instance? BaseSymbolType v))
|
||||
;; Value validation -----------------------------------------------------------
|
||||
;;
|
||||
;; Tokenscript resolves any expression; these are the Penpot level checks
|
||||
;; on the resolved value of each token type (ranges, allowed keywords,
|
||||
;; composite shapes) that decide whether a token can be applied.
|
||||
|
||||
(defn structured-token? [v]
|
||||
(instance? TokenSymbol v))
|
||||
(defn- words->string
|
||||
"Values like `700 Italic` resolve to a list of words; font weights are
|
||||
strings."
|
||||
[v]
|
||||
(if (sequential? v) (str/join " " (map str v)) 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 font-family-symbols->penpot-unit
|
||||
"Converts a resolved font-family value into a vector of family names.
|
||||
Each family name is the string form of its whole entry, which is a list of
|
||||
word symbols when the family reached tokenscript unquoted."
|
||||
[^js v]
|
||||
(when (some? v)
|
||||
(let [entries (.-value v)]
|
||||
(if (instance? js/Array entries)
|
||||
(mapv str entries)
|
||||
[(str v)]))))
|
||||
|
||||
(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]]
|
||||
(let [k (keyword k)]
|
||||
;; The font-family member of a composite is a family list
|
||||
[k (if (= :font-family k)
|
||||
(font-family-symbols->penpot-unit v)
|
||||
(tokenscript-symbols->penpot-unit v))])))
|
||||
entries))))
|
||||
|
||||
(defn tokenscript-symbols->penpot-unit [^js v]
|
||||
(defn- font-family->vector
|
||||
"Font families are always a flat vector of family names, also when the
|
||||
value is a single name or references a font family token."
|
||||
[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)))
|
||||
(string? v) [v]
|
||||
(sequential? v) (vec (flatten v))
|
||||
:else v))
|
||||
|
||||
(defn resolved-value->penpot-unit
|
||||
"Converts the resolved value of a token of the given `type`.
|
||||
Font families need the type: a list of symbols is a list of families at the
|
||||
top level and the words of a single family name inside an entry."
|
||||
[type ^js v]
|
||||
(if (= :font-family type)
|
||||
(font-family-symbols->penpot-unit v)
|
||||
(tokenscript-symbols->penpot-unit v)))
|
||||
(defn- line-height->ratio
|
||||
"Line heights in pixels are stored relative to the font size. Returns
|
||||
the ratio or an error when the font size is not available."
|
||||
[line-height font-size]
|
||||
(if (and (sym/number-with-unit? line-height) (= "px" (:unit line-height)))
|
||||
(if (number? font-size)
|
||||
{:value (/ (sym/value line-height) font-size)}
|
||||
{:error (wte/error-with-value :error.style-dictionary/composite-line-height-needs-font-size font-size)})
|
||||
{:value (symbol->value line-height)}))
|
||||
|
||||
;; 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- typography->value
|
||||
"Converts a resolved typography composite, adjusting the fields the
|
||||
text attributes expect. With `line-height-ratio` a line height in
|
||||
pixels is converted relative to the font size."
|
||||
[s line-height-ratio]
|
||||
(let [value (symbol->value s)]
|
||||
(if-not (map? value)
|
||||
{:value value}
|
||||
(let [value (cond-> value
|
||||
(contains? value :font-family) (update :font-family font-family->vector)
|
||||
(contains? value :font-weight) (update :font-weight words->string))
|
||||
line-height (when (and line-height-ratio (sym/token? s))
|
||||
(get (sym/value s) "line-height"))]
|
||||
(if (some? line-height)
|
||||
(let [{ratio :value error :error} (line-height->ratio line-height (:font-size value))]
|
||||
(if error
|
||||
{:error error}
|
||||
{:value (assoc value :line-height ratio)}))
|
||||
{:value value})))))
|
||||
|
||||
(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)
|
||||
(defn- convert-value
|
||||
"Converts the resolved symbol of a token to the value shape expected by
|
||||
its type. Returns `{:value v}` or `{:error e}`. Options:
|
||||
|
||||
;; 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))
|
||||
:line-height-ratio convert typography line heights in pixels to a
|
||||
ratio of the font size (default true)"
|
||||
([type s] (convert-value type s nil))
|
||||
([type s {:keys [line-height-ratio] :or {line-height-ratio true}}]
|
||||
(case type
|
||||
:typography (typography->value s line-height-ratio)
|
||||
:font-family {:value (font-family->vector (symbol->value s))}
|
||||
:font-weight {:value (words->string (symbol->value s))}
|
||||
{:value (symbol->value s)})))
|
||||
|
||||
;; 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)))
|
||||
(defn- convert-token-value
|
||||
"`convert-value` for a token: a failing conversion (e.g. a color that
|
||||
cannot be written as hex) is an error of that token, not of the whole
|
||||
resolution."
|
||||
[{:keys [type value]} s]
|
||||
(try
|
||||
(convert-value type s)
|
||||
(catch :default _
|
||||
{:error (wte/error-with-value :error.style-dictionary/invalid-token-value value)})))
|
||||
|
||||
;; Extract the atom value
|
||||
get-result
|
||||
(fn [] @output)]
|
||||
#js {:onResolve on-resolve
|
||||
:onError on-error
|
||||
:getResult get-result}))
|
||||
(defn plugin-value
|
||||
"Resolved value of a token as exposed by the plugin API: like
|
||||
`:resolved-value` but with typography line heights left as resolved."
|
||||
[{:keys [type resolved-symbol]}]
|
||||
(when (some? resolved-symbol)
|
||||
(:value (convert-value type resolved-symbol {:line-height-ratio false}))))
|
||||
|
||||
(defn- validate-value
|
||||
"Returns a Penpot error for the resolved `value` of `token`, or nil when
|
||||
it is acceptable. The interpreter validates the kind of value of each
|
||||
type; these are the Penpot specific limits on top."
|
||||
[{:keys [type]} value]
|
||||
(if (and (number? value) (cto/number-out-of-bounds? value))
|
||||
(wte/error-with-value :error.token/number-too-large value)
|
||||
(case type
|
||||
:text-decoration
|
||||
(when-not (and (string? value) (cto/valid-text-decoration value))
|
||||
(wte/error-with-value :error.style-dictionary/invalid-token-value-text-decoration value))
|
||||
|
||||
:font-weight
|
||||
(when-not (cto/valid-font-weight-variant (str value))
|
||||
(wte/error-with-value :error.style-dictionary/invalid-token-value-font-weight value))
|
||||
|
||||
:font-family
|
||||
(when-not (or (string? value) (and (sequential? value) (every? string? value)))
|
||||
(wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value))
|
||||
|
||||
nil)))
|
||||
|
||||
;; Errors ---------------------------------------------------------------------
|
||||
|
||||
(defn- missing-reference-of
|
||||
"Missing token name behind an issue, if the root cause is a missing
|
||||
token."
|
||||
[{:keys [code data]}]
|
||||
(case code
|
||||
:processor/token-not-found (:token-name data)
|
||||
:interpreter/unknown-reference (:reference data)
|
||||
:processor/dependency-error (let [cause (:cause data)]
|
||||
(when (= :processor/token-not-found (:code cause))
|
||||
(get-in cause [:data :token-name])))
|
||||
nil))
|
||||
|
||||
(defn- circular?
|
||||
[{:keys [code data]}]
|
||||
(or (= :processor/circular-dependency code)
|
||||
(= :processor/circular-dependency (get-in data [:cause :code]))))
|
||||
|
||||
(defn- issue->error
|
||||
"Maps a tokenscript issue to a Penpot token error. The interpreter only
|
||||
reports codes and data; the messages are Penpot's. Codes without a
|
||||
message of their own are an invalid value."
|
||||
[{:keys [code data] :as issue} value]
|
||||
(cond
|
||||
(circular? issue)
|
||||
(wte/error-with-value :error.token/circular-reference nil)
|
||||
|
||||
(missing-reference-of issue)
|
||||
(wte/error-with-value :error.style-dictionary/missing-reference [(missing-reference-of issue)])
|
||||
|
||||
:else
|
||||
(case code
|
||||
(:operations/division-by-zero :functions/division-by-zero)
|
||||
(wte/error-with-value :error.token/division-by-zero nil)
|
||||
|
||||
:operations/cannot-mix-units
|
||||
(wte/error-with-value :error.token/incompatible-units (:units data))
|
||||
|
||||
:operations/cannot-compare-units
|
||||
(wte/error-with-value :error.token/incompatible-units [(:unit-a data) (:unit-b data)])
|
||||
|
||||
:interpreter/unknown-function
|
||||
(wte/error-with-value :error.token/unknown-function (:function-name data))
|
||||
|
||||
(:functions/argument-out-of-range :functions/invalid-base)
|
||||
(wte/error-with-value :error.token/function-argument-out-of-range (:function-name data))
|
||||
|
||||
(wte/error-with-value :error.style-dictionary/invalid-token-value value))))
|
||||
|
||||
(defn- validation-error
|
||||
"Maps a failed token type validation of the interpreter (an issue with
|
||||
an `INVALID_<TYPE>_<REASON>` code) to the Penpot error of the token's
|
||||
type."
|
||||
[{:keys [code]} {:keys [type value]}]
|
||||
(case type
|
||||
:opacity (wte/error-with-value :error.style-dictionary/invalid-token-value-opacity value)
|
||||
:stroke-width (wte/error-with-value :error.style-dictionary/invalid-token-value-stroke-width value)
|
||||
:text-case (wte/error-with-value :error.style-dictionary/invalid-token-value-text-case value)
|
||||
:text-decoration (wte/error-with-value :error.style-dictionary/invalid-token-value-text-decoration value)
|
||||
:font-weight (wte/error-with-value :error.style-dictionary/invalid-token-value-font-weight value)
|
||||
:font-family (wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value)
|
||||
:typography (wte/error-with-value :error.style-dictionary/invalid-token-value-typography value)
|
||||
:shadow (wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)
|
||||
(if (= "INVALID_NUMBER_UNIT" code)
|
||||
(wte/error-with-value :error.style-dictionary/value-with-units value)
|
||||
(wte/error-with-value :error.style-dictionary/invalid-token-value value))))
|
||||
|
||||
(defn- token-errors
|
||||
[issues token]
|
||||
(->> issues
|
||||
(keep (fn [issue]
|
||||
(cond
|
||||
(tse/error-map? issue) (issue->error issue (:value token))
|
||||
(= :tokenscript/issue (:type issue)) (validation-error issue token))))
|
||||
(seq)))
|
||||
|
||||
;; Resolution -----------------------------------------------------------------
|
||||
|
||||
(defn- quote-font-family
|
||||
"Quotes a font family name so tokenscript reads it as one string instead of
|
||||
parsing its words (`Red` would become a color, `2P` a number and a word).
|
||||
Entries with a token reference or already quoted are kept as they are."
|
||||
"Quotes a font family name so tokenscript reads it as one string instead
|
||||
of parsing its words (`Red` would become a color, `2P` a number and a
|
||||
word). Entries with a token reference or already quoted are kept."
|
||||
[family]
|
||||
(cond
|
||||
(or (not (string? family))
|
||||
@ -183,49 +272,86 @@
|
||||
(mapv quote-font-family value)
|
||||
value))
|
||||
|
||||
(defn- token-value->tokenscript-value
|
||||
[type value]
|
||||
(case type
|
||||
:font-family (quote-font-families value)
|
||||
:typography (cond-> value
|
||||
(map? value) (d/update-when :font-family quote-font-families))
|
||||
value))
|
||||
|
||||
(defn clj->token->tokenscript-token
|
||||
"Convert penpot token into a format that tokenscript can handle."
|
||||
(defn- token->input
|
||||
"Token data for the interpreter. Types are passed by their DTCG names,
|
||||
which the interpreter's bundled token types validate."
|
||||
[{:keys [type value]}]
|
||||
#js {"$type" (name type)
|
||||
"$value" (clj->js (token-value->tokenscript-value type value))})
|
||||
(cond-> {:value (case type
|
||||
:font-family (quote-font-families value)
|
||||
:typography (cond-> value
|
||||
(map? value) (d/update-when :font-family quote-font-families))
|
||||
value)}
|
||||
(some? type) (assoc :type (get cto/token-type->dtcg-token-type type (name type)))))
|
||||
|
||||
(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}]"
|
||||
(defn- empty-value?
|
||||
[token]
|
||||
(nil? (:value token)))
|
||||
|
||||
(defn- resolved-token
|
||||
"Adds the result of the interpreter to `token`: `:resolved-value` and
|
||||
`:resolved-symbol` for the resolved symbol `s`, or `:errors` for its
|
||||
`issues`, a failing conversion or a value Penpot doesn't accept."
|
||||
[token issues s]
|
||||
(if-let [errors (token-errors issues token)]
|
||||
(assoc token :errors (vec errors))
|
||||
(let [{:keys [value error]} (convert-token-value token s)]
|
||||
(if-let [error (or error (validate-value token value))]
|
||||
(assoc token :errors [error])
|
||||
(assoc token
|
||||
:resolved-value value
|
||||
:resolved-symbol s)))))
|
||||
|
||||
(defn- resolve-tokens*
|
||||
[tokens]
|
||||
(let [token-map (js/Map.)]
|
||||
(doseq [[k token] tokens]
|
||||
(.set token-map k (clj->token->tokenscript-token token)))
|
||||
token-map))
|
||||
(let [tpoint (ct/tpoint-ms)
|
||||
inputs (into {}
|
||||
(comp (remove (fn [[_ token]] (empty-value? token)))
|
||||
(map (fn [[name token]] [name (token->input token)])))
|
||||
tokens)
|
||||
result (tsc/build inputs)
|
||||
resolved (:tokens result)
|
||||
issues (:issues result)
|
||||
output (reduce-kv
|
||||
(fn [acc name token]
|
||||
(assoc acc name
|
||||
(if (empty-value? token)
|
||||
(assoc token :errors [(wte/get-error-code :error.token/empty-input)])
|
||||
(resolved-token token (get issues name) (get resolved name)))))
|
||||
{}
|
||||
tokens)]
|
||||
(l/dbg :hint "tokenscript/resolve-tokens" :elapsed (tpoint))
|
||||
output))
|
||||
|
||||
(defn process-tokens
|
||||
"Builds tokens using `tokenscript`."
|
||||
;; Several components resolve the same token maps on every change (the
|
||||
;; sidebar, the forms and the propagation each compute the active tokens
|
||||
;; separately), so the results of the last few maps are kept. Entries are
|
||||
;; matched by identity first and by equality second: comparing two maps
|
||||
;; of a few hundred tokens is far cheaper than resolving them again.
|
||||
(def ^:private cache-size 8)
|
||||
|
||||
(defonce ^:private results-cache (volatile! []))
|
||||
|
||||
(defn- cached-result
|
||||
[tokens]
|
||||
(let [input (clj-tokens->tokenscript-tokens tokens)
|
||||
result (processTokens input #js {:config config
|
||||
:builder (create-token-builder tokens)})]
|
||||
result))
|
||||
(let [entries @results-cache]
|
||||
(or (some (fn [[t result]] (when (identical? t tokens) result)) entries)
|
||||
(some (fn [[t result]] (when (= t tokens) result)) entries))))
|
||||
|
||||
(defn update-token
|
||||
(defn- remember-result
|
||||
[tokens result]
|
||||
(vswap! results-cache (fn [entries] (vec (take cache-size (cons [tokens result] entries))))))
|
||||
|
||||
(defn resolve-tokens
|
||||
"Resolves `tokens` (a map of token name to token). Returns the tokens
|
||||
with `:resolved-value` or `:errors` added."
|
||||
[tokens]
|
||||
(or (cached-result tokens)
|
||||
(let [result (resolve-tokens* tokens)]
|
||||
(remember-result tokens result)
|
||||
result)))
|
||||
|
||||
(defn resolve-token
|
||||
"Resolves `token` against `tokens` (the token replaces any token with
|
||||
the same name). Returns the resolved 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)))
|
||||
(get (resolve-tokens (assoc tokens (:name token) token)) (:name token)))
|
||||
|
||||
@ -735,9 +735,6 @@
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
resolved-value (get-in resolved-tokens [(:name token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/resolved-value->penpot-unit (:type token) resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
|
||||
@ -7,9 +7,7 @@
|
||||
(ns app.main.data.workspace.tokens.color
|
||||
(:require
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.config :as cf]
|
||||
[app.main.data.tinycolor :as tinycolor]
|
||||
[app.main.data.tokenscript :as ts]))
|
||||
[app.main.data.tinycolor :as tinycolor]))
|
||||
|
||||
(defn color-bullet-color [token-color-value]
|
||||
(when-let [tc (tinycolor/valid-color token-color-value)]
|
||||
@ -19,8 +17,5 @@
|
||||
(tinycolor/->hex-string tc))))
|
||||
|
||||
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
|
||||
(if (contains? cf/flags :tokenscript)
|
||||
(when (and resolved-value (ts/color-symbol? resolved-value))
|
||||
(ts/color-symbol->penpot-color resolved-value))
|
||||
(when (and resolved-value (cfo/color-token? token))
|
||||
(color-bullet-color resolved-value))))
|
||||
(when (and resolved-value (cfo/color-token? token))
|
||||
(color-bullet-color resolved-value)))
|
||||
|
||||
@ -57,6 +57,22 @@
|
||||
{:error/code :error.token/number-too-large
|
||||
:error/fn #(str (tr "errors.tokens.number-too-large" %))}
|
||||
|
||||
:error.token/division-by-zero
|
||||
{:error/code :error.token/division-by-zero
|
||||
:error/fn #(tr "errors.tokens.division-by-zero")}
|
||||
|
||||
:error.token/incompatible-units
|
||||
{:error/code :error.token/incompatible-units
|
||||
:error/fn #(tr "errors.tokens.incompatible-units" (str/join ", " %))}
|
||||
|
||||
:error.token/unknown-function
|
||||
{:error/code :error.token/unknown-function
|
||||
:error/fn #(tr "errors.tokens.unknown-function" %)}
|
||||
|
||||
:error.token/function-argument-out-of-range
|
||||
{:error/code :error.token/function-argument-out-of-range
|
||||
:error/fn #(tr "errors.tokens.function-argument-out-of-range" %)}
|
||||
|
||||
;; Surfaced when a token name conflicts with a token-group prefix
|
||||
;; across the active sets (e.g. one set defines `a`, another set
|
||||
;; defines `a.b`). DTCG/StyleDictionary cannot represent both as
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns app.main.data.workspace.tokens.format
|
||||
(:require
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def category-dictionary
|
||||
@ -35,39 +40,10 @@
|
||||
(str/join "\n")
|
||||
(str "\n")))
|
||||
|
||||
(defn- format-structured-token
|
||||
"Formats tokenscript Token"
|
||||
[token-symbol]
|
||||
(->> (.-value token-symbol)
|
||||
(.entries)
|
||||
(es6-iterator-seq)
|
||||
(format-map-entries)))
|
||||
|
||||
(defn format-tokenscript-symbol
|
||||
[^js tokenscript-symbol]
|
||||
(cond
|
||||
(ts/rem-number-with-unit? tokenscript-symbol)
|
||||
(str (ts/rem->px tokenscript-symbol) "px")
|
||||
|
||||
(ts/color-symbol? tokenscript-symbol)
|
||||
(ts/color-symbol->hex-string tokenscript-symbol)
|
||||
|
||||
(ts/structured-record-token? tokenscript-symbol)
|
||||
(format-structured-token tokenscript-symbol)
|
||||
|
||||
(ts/structured-array-token? tokenscript-symbol)
|
||||
(str/join "\n" (map format-tokenscript-symbol (.-value tokenscript-symbol)))
|
||||
|
||||
:else
|
||||
(.toString tokenscript-symbol)))
|
||||
|
||||
(defn format-token-value
|
||||
"Converts token value of any shape to a string."
|
||||
[token-value]
|
||||
(cond
|
||||
(ts/tokenscript-symbol? token-value)
|
||||
(format-tokenscript-symbol token-value)
|
||||
|
||||
(map? token-value)
|
||||
(format-map-entries token-value)
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main.data.workspace.tokens.propagation
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.changes-builder :as pcb]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.files.tokens :as cfo]
|
||||
@ -217,9 +216,7 @@
|
||||
(when (and tokens-lib tokens-status)
|
||||
(when-let [tokens-tree (cfo/get-tokens-in-active-sets tokens-status tokens-lib)]
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (-> (ts/resolve-tokens tokens-tree)
|
||||
(d/update-vals #(update % :resolved-value
|
||||
(partial ts/resolved-value->penpot-unit (:type %))))))
|
||||
(rx/of (ts/resolve-tokens tokens-tree))
|
||||
(sd/resolve-tokens tokens-tree))
|
||||
(rx/mapcat
|
||||
(fn [sd-tokens]
|
||||
|
||||
@ -81,10 +81,7 @@
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(let [{:keys [errors resolved-value]} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)})))))))))
|
||||
|
||||
@ -58,10 +58,7 @@
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(let [{:keys [errors resolved-value]} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
|
||||
@ -76,10 +76,7 @@
|
||||
(sd/resolve-tokens-interactive tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/resolved-value->penpot-unit (:type token) resolved-value)
|
||||
resolved-value)]
|
||||
(let [{:keys [errors resolved-value]} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
|
||||
@ -144,16 +144,25 @@
|
||||
|
||||
|
||||
(defn- resolve-value-tokenscript
|
||||
[tokens prev-token value]
|
||||
(let [result (ts/update-token tokens (assoc prev-token :value value))
|
||||
token-result (.-resolved result)]
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
(assoc prev-token
|
||||
:value value
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name))
|
||||
|
||||
{:keys [errors resolved-value]}
|
||||
;; Remove previous token when renaming a token
|
||||
(ts/resolve-token (dissoc tokens (:name prev-token)) token)]
|
||||
(rx/of
|
||||
(cond
|
||||
(ts/processor-error? token-result) {:error (wte/error-with-value :error.style-dictionary/missing-reference (some->> (.-dependencyChain token-result)
|
||||
(seq)
|
||||
(rest)))}
|
||||
(instance? js/Error token-result) {:error (wte/error-with-value :error.style-dictionary/invalid-token-value value)}
|
||||
:else {:value token-result}))))
|
||||
(if (seq errors)
|
||||
{:error (first errors)}
|
||||
{:value resolved-value}))))
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
@ -178,10 +187,7 @@
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)]
|
||||
(let [{:keys [errors resolved-value]} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (if errors
|
||||
|
||||
@ -44,10 +44,7 @@
|
||||
(sd/resolve-tokens-interactive tokens'))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [resolved-token (cond-> (get resolved-tokens (:name token))
|
||||
(contains? cf/flags :tokenscript)
|
||||
(update :resolved-value
|
||||
(partial ts/resolved-value->penpot-unit (:type token))))]
|
||||
(let [resolved-token (get resolved-tokens (:name token))]
|
||||
(cond
|
||||
(:resolved-value resolved-token)
|
||||
(rx/of resolved-token)
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
|
||||
(defn- generate-tooltip
|
||||
"Generates a tooltip for a given token"
|
||||
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set is-name-collision errors]
|
||||
[is-viewer shape theme-token token half-applied no-valid-value ref-not-in-active-set error-message]
|
||||
(let [{:keys [name type resolved-value value]} token
|
||||
resolved-value-theme (:resolved-value theme-token)
|
||||
resolved-value (or resolved-value-theme resolved-value)
|
||||
@ -141,8 +141,8 @@
|
||||
ref-not-in-active-set
|
||||
(tr "workspace.tokens.ref-not-valid" name)
|
||||
|
||||
is-name-collision
|
||||
(wte/resolve-error-message (first errors))
|
||||
error-message
|
||||
error-message
|
||||
|
||||
no-valid-value
|
||||
(tr "workspace.tokens.value-not-valid")
|
||||
@ -237,9 +237,11 @@
|
||||
|
||||
is-viewer? (not can-edit)
|
||||
|
||||
tokenscript? (contains? cf/flags :tokenscript)
|
||||
|
||||
ref-not-in-active-set
|
||||
(if (contains? cf/flags :tokenscript)
|
||||
(seq (:errors resolved-token))
|
||||
(if tokenscript?
|
||||
(wte/has-error-code? :error.style-dictionary/missing-reference errors)
|
||||
(and is-reference?
|
||||
(not (contains-reference-value? value active-theme-tokens))))
|
||||
|
||||
@ -250,6 +252,11 @@
|
||||
|
||||
no-valid-value (seq errors)
|
||||
|
||||
;; Tokenscript errors carry their own message, as in the edit form.
|
||||
error-message
|
||||
(when (and no-valid-value (or tokenscript? name-collision))
|
||||
(wte/resolve-error-message (first errors)))
|
||||
|
||||
errors?
|
||||
(or ref-not-in-active-set
|
||||
no-valid-value
|
||||
@ -297,11 +304,11 @@
|
||||
|
||||
on-hover
|
||||
(mf/use-fn
|
||||
(mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set name-collision errors)
|
||||
(mf/deps selected-shapes is-viewer? active-theme-tokens token half-applied? no-valid-value ref-not-in-active-set error-message)
|
||||
(fn [_]
|
||||
(let [theme-token (get active-theme-tokens name)
|
||||
title (generate-tooltip is-viewer? (first selected-shapes) theme-token token
|
||||
half-applied? no-valid-value ref-not-in-active-set name-collision errors)]
|
||||
half-applied? no-valid-value ref-not-in-active-set error-message)]
|
||||
(reset! tooltip-content* title))))
|
||||
|
||||
;; `tooltip-content` may hold rich (hiccup) content, so it's wrapped
|
||||
@ -346,9 +353,8 @@
|
||||
[:> icon*
|
||||
{:icon-id i/broken-link
|
||||
:class (stl/css :token-pill-icon)
|
||||
:aria-label (if name-collision
|
||||
(wte/resolve-error-message (first errors))
|
||||
(tr "workspace.tokens.missing-reference"))}]
|
||||
:aria-label (or error-message
|
||||
(tr "workspace.tokens.missing-reference"))}]
|
||||
|
||||
color
|
||||
[:> swatch* {:background color
|
||||
|
||||
@ -232,14 +232,12 @@
|
||||
k))
|
||||
|
||||
(defn- shadow-entry->js
|
||||
"Converts one resolved shadow entry (a JS Map of field name -> tokenscript
|
||||
symbol) into a plain JS object using the public member names and the
|
||||
unit-converted values."
|
||||
[^js m]
|
||||
"Converts one resolved shadow entry (a map of field name -> value) into
|
||||
a plain JS object using the public member names."
|
||||
[m]
|
||||
(let [out #js {}]
|
||||
(.forEach m (fn [sym k]
|
||||
(obj/set! out (shadow-key->camel k)
|
||||
(ts/tokenscript-symbols->penpot-unit sym))))
|
||||
(doseq [[k v] m]
|
||||
(obj/set! out (shadow-key->camel (name k)) (clj->js v)))
|
||||
out))
|
||||
|
||||
(defn- shadow-resolved-value->js
|
||||
@ -249,38 +247,29 @@
|
||||
(when (some? entries)
|
||||
(into-array (map shadow-entry->js entries))))
|
||||
|
||||
(defn- font-families-resolved-value->js
|
||||
"Converts a resolved fontFamilies value (a tokenscript list symbol) into the
|
||||
documented `string[]` shape."
|
||||
[resolved-value]
|
||||
(some-> (ts/font-family-symbols->penpot-unit resolved-value)
|
||||
(clj->js)))
|
||||
|
||||
(defn- get-resolved-value
|
||||
[token tokens-tree]
|
||||
(let [resolved-tokens (ts/resolve-tokens tokens-tree)
|
||||
resolved-value (dm/get-in resolved-tokens [(:name token) :resolved-value])]
|
||||
resolved-token (get resolved-tokens (:name token))
|
||||
resolved-value (:resolved-value resolved-token)]
|
||||
(cond
|
||||
(= :font-family (:type token))
|
||||
;; A fontFamilies token resolves to a list of families; expose it as the
|
||||
;; documented `string[]` rather than the raw tokenscript list symbol.
|
||||
(font-families-resolved-value->js resolved-value)
|
||||
;; A fontFamilies token resolves to a vector of families; expose it as
|
||||
;; the documented `string[]`.
|
||||
(some-> resolved-value clj->js)
|
||||
|
||||
(= :typography (:type token))
|
||||
;; A typography token resolves to a composite; expose it as the documented
|
||||
;; `TokenTypographyValue[]` rather than the raw tokenscript structure.
|
||||
(typography-resolved-value->js
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
||||
;; `TokenTypographyValue[]` with the line height as resolved.
|
||||
(typography-resolved-value->js (ts/plugin-value resolved-token))
|
||||
|
||||
(= :shadow (:type token))
|
||||
;; A shadow token resolves to a list of composites whose entries the
|
||||
;; tokenscript unit conversion leaves as raw symbols; expose them as the
|
||||
;; A shadow token resolves to a list of composites; expose them as the
|
||||
;; documented `TokenShadowValue[]`.
|
||||
(shadow-resolved-value->js
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))
|
||||
(shadow-resolved-value->js resolved-value)
|
||||
|
||||
:else
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value))))
|
||||
resolved-value)))
|
||||
|
||||
(defn token-proxy? [p]
|
||||
(obj/type-of? p "TokenProxy"))
|
||||
|
||||
@ -110,6 +110,7 @@
|
||||
[frontend-tests.tokens.style-dictionary-test]
|
||||
[frontend-tests.tokens.token-errors-test]
|
||||
[frontend-tests.tokens.token-path-test]
|
||||
[frontend-tests.tokens.tokenscript-test]
|
||||
[frontend-tests.tokens.workspace-tokens-remap-test]
|
||||
[frontend-tests.ui.check-updates-test]
|
||||
[frontend-tests.ui.color-token-position-test]
|
||||
@ -269,6 +270,7 @@
|
||||
'frontend-tests.tokens.style-dictionary-test
|
||||
'frontend-tests.tokens.token-errors-test
|
||||
'frontend-tests.tokens.token-path-test
|
||||
'frontend-tests.tokens.tokenscript-test
|
||||
'frontend-tests.tokens.logic.tokens-status-test
|
||||
'frontend-tests.tokens.workspace-tokens-remap-test
|
||||
'frontend-tests.ui.check-updates-test
|
||||
|
||||
184
frontend/test/frontend_tests/tokens/tokenscript_test.cljs
Normal file
184
frontend/test/frontend_tests/tokens/tokenscript_test.cljs
Normal file
@ -0,0 +1,184 @@
|
||||
;; 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 SUBSIDIARY SL
|
||||
|
||||
(ns frontend-tests.tokens.tokenscript-test
|
||||
(:require
|
||||
[app.common.test-helpers.ids-map :as cthi]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.tokenscript :as ts]
|
||||
[cljs.test :as t :include-macros true]))
|
||||
|
||||
(defn- tokens-map
|
||||
"Tokens map of a library with one set holding `tokens`."
|
||||
[& tokens]
|
||||
(let [lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set) :name "core")))]
|
||||
(-> (reduce (fn [lib token] (ctob/add-token lib (cthi/id :core-set) (ctob/make-token token))) lib tokens)
|
||||
(ctob/get-all-tokens-map))))
|
||||
|
||||
(t/deftest resolve-tokens-test
|
||||
(t/testing "resolves references and converts units to pixels"
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "borderRadius.sm" :value "12px" :type :border-radius}
|
||||
{:name "borderRadius.md-with-dashes" :value "{borderRadius.sm} * 2" :type :border-radius}
|
||||
{:name "borderRadius.rem" :value "2rem" :type :border-radius}
|
||||
{:name "opacity.half" :value "50%" :type :opacity}
|
||||
{:name "borderRadius.large" :value "123456789012345" :type :border-radius}
|
||||
{:name "borderRadius.largePx" :value "123456789012345px" :type :border-radius}
|
||||
{:name "borderRadius.largeFn" :value "{borderRadius.sm} * 200000000" :type :border-radius}))]
|
||||
(t/is (= 12 (get-in resolved ["borderRadius.sm" :resolved-value])))
|
||||
(t/is (= 24 (get-in resolved ["borderRadius.md-with-dashes" :resolved-value])))
|
||||
(t/is (= 32 (get-in resolved ["borderRadius.rem" :resolved-value])))
|
||||
(t/is (= 0.5 (get-in resolved ["opacity.half" :resolved-value])))
|
||||
(doseq [name ["borderRadius.large" "borderRadius.largePx" "borderRadius.largeFn"]]
|
||||
(t/is (nil? (get-in resolved [name :resolved-value])))
|
||||
(t/is (= :error.token/number-too-large (get-in resolved [name :errors 0 :error/code])))))))
|
||||
|
||||
(t/deftest resolve-tokens-colors-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "color.red" :value "#ff0000" :type :color}
|
||||
{:name "color.alias" :value "{color.red}" :type :color}
|
||||
{:name "color.alpha" :value "#ff000080" :type :color}
|
||||
{:name "color.named" :value "red" :type :color}
|
||||
{:name "color.light" :value "lighten({color.red}, 0.2)" :type :color}))]
|
||||
(t/is (= "#ff0000" (get-in resolved ["color.alias" :resolved-value])))
|
||||
(t/is (= "#ff000080" (get-in resolved ["color.alpha" :resolved-value])) "alpha is kept as hex8")
|
||||
(t/is (= "#FF0000" (get-in resolved ["color.named" :resolved-value])) "css names resolve")
|
||||
(t/is (string? (get-in resolved ["color.light" :resolved-value])) "color functions resolve to hex")))
|
||||
|
||||
(t/deftest resolve-tokens-errors-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "a" :value "{missing}" :type :border-radius}
|
||||
{:name "b" :value "{a} + 1" :type :border-radius}
|
||||
{:name "x" :value "{y}" :type :border-radius}
|
||||
{:name "y" :value "{x}" :type :border-radius}
|
||||
{:name "z" :value "{x} + 1" :type :border-radius}
|
||||
{:name "bad" :value "1 ´ 2" :type :border-radius}))]
|
||||
(t/is (= :error.style-dictionary/missing-reference (get-in resolved ["a" :errors 0 :error/code])))
|
||||
(t/is (= ["missing"] (get-in resolved ["a" :errors 0 :error/value])))
|
||||
(t/is (= :error.style-dictionary/missing-reference (get-in resolved ["b" :errors 0 :error/code]))
|
||||
"dependents report the missing token")
|
||||
(t/is (= ["missing"] (get-in resolved ["b" :errors 0 :error/value])))
|
||||
(t/is (= :error.token/circular-reference (get-in resolved ["x" :errors 0 :error/code])))
|
||||
(t/is (= :error.token/circular-reference (get-in resolved ["y" :errors 0 :error/code])))
|
||||
(t/is (= :error.token/circular-reference (get-in resolved ["z" :errors 0 :error/code]))
|
||||
"dependents of a cycle report the cycle")
|
||||
(t/is (= :error.style-dictionary/invalid-token-value (get-in resolved ["bad" :errors 0 :error/code])))))
|
||||
|
||||
(t/deftest resolve-tokens-error-codes-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "zero" :value "1 / 0" :type :number}
|
||||
{:name "mod-zero" :value "mod(4, 0)" :type :number}
|
||||
{:name "units" :value "1px * 2em" :type :sizing}
|
||||
{:name "fn" :value "foo(1)" :type :number}
|
||||
{:name "ln" :value "ln(0)" :type :number}
|
||||
{:name "ref" :value "{zero} + 1" :type :number}))
|
||||
error (fn [name] (select-keys (get-in resolved [name :errors 0]) [:error/code :error/value]))]
|
||||
(t/is (= {:error/code :error.token/division-by-zero :error/value nil} (error "zero")))
|
||||
(t/is (= {:error/code :error.token/division-by-zero :error/value nil} (error "mod-zero")))
|
||||
(t/is (= {:error/code :error.token/incompatible-units :error/value ["px" "em"]} (error "units")))
|
||||
(t/is (= {:error/code :error.token/unknown-function :error/value "foo"} (error "fn")))
|
||||
(t/is (= {:error/code :error.token/function-argument-out-of-range :error/value "ln"} (error "ln")))
|
||||
(t/is (= {:error/code :error.style-dictionary/invalid-token-value :error/value "{zero} + 1"} (error "ref"))
|
||||
"a token failing through its reference is an invalid value")))
|
||||
|
||||
;; A token named "a" and a token named "a.b" can coexist: `{a}` resolves
|
||||
;; to the token, `{a.b}` to the nested one (#9584).
|
||||
(t/deftest resolve-tokens-name-collision-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "a" :value "8px" :type :border-radius}
|
||||
{:name "a.b" :value "12px" :type :border-radius}
|
||||
{:name "c" :value "{a} + {a.b}" :type :border-radius}))]
|
||||
(t/is (= 8 (get-in resolved ["a" :resolved-value])))
|
||||
(t/is (= 12 (get-in resolved ["a.b" :resolved-value])))
|
||||
(t/is (= 20 (get-in resolved ["c" :resolved-value])))))
|
||||
|
||||
(t/deftest resolve-tokens-composites-test
|
||||
(t/testing "typography and shadow tokens resolve field by field"
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "size" :value "16" :type :font-size}
|
||||
{:name "typo" :type :typography
|
||||
:value {:font-family ["Roboto" "Arial"]
|
||||
:font-size "{size} * 2"
|
||||
:font-weight "700 Italic"
|
||||
:line-height "48px"}}
|
||||
{:name "shadow" :type :shadow
|
||||
:value [{:offset-x "2" :offset-y "{size}" :blur "4" :spread "0"
|
||||
:color "#000" :inset false}]}))]
|
||||
(t/is (= {:font-family ["Roboto" "Arial"] :font-size 32 :font-weight "700 Italic" :line-height 1.5}
|
||||
(get-in resolved ["typo" :resolved-value]))
|
||||
"line heights in pixels become a ratio of the font size")
|
||||
(t/is (= [{:offset-x 2 :offset-y 16 :blur 4 :spread 0 :color "#000" :inset false}]
|
||||
(get-in resolved ["shadow" :resolved-value])))))
|
||||
(t/testing "font families referencing a font family token are flattened"
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "families" :value ["Arial" "sans-serif"] :type :font-family}
|
||||
{:name "single" :value "Roboto" :type :font-family}
|
||||
{:name "typo" :value {:font-family ["{families}"]} :type :typography}))]
|
||||
(t/is (= ["Arial" "sans-serif"] (get-in resolved ["families" :resolved-value])))
|
||||
(t/is (= ["Roboto"] (get-in resolved ["single" :resolved-value])))
|
||||
(t/is (= ["Arial" "sans-serif"] (get-in resolved ["typo" :resolved-value :font-family])))))
|
||||
(t/testing "a typography token with an array value is invalid"
|
||||
(let [resolved (ts/resolve-tokens (tokens-map {:name "typography.bad" :value ["Roboto"] :type :typography}))]
|
||||
(t/is (nil? (get-in resolved ["typography.bad" :resolved-value])))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-typography
|
||||
(get-in resolved ["typography.bad" :errors 0 :error/code]))))))
|
||||
|
||||
(t/deftest resolve-tokens-value-validation-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "opacity.bad" :value "2" :type :opacity}
|
||||
{:name "stroke.bad" :value "-1" :type :stroke-width}
|
||||
{:name "case.ok" :value "uppercase" :type :text-case}
|
||||
{:name "case.bad" :value "wat" :type :text-case}
|
||||
{:name "weight.ok" :value "bold" :type :font-weight}
|
||||
{:name "weight.bad" :value "heavyish" :type :font-weight}))]
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-opacity (get-in resolved ["opacity.bad" :errors 0 :error/code])))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-stroke-width (get-in resolved ["stroke.bad" :errors 0 :error/code])))
|
||||
(t/is (= "uppercase" (get-in resolved ["case.ok" :resolved-value])))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-text-case (get-in resolved ["case.bad" :errors 0 :error/code])))
|
||||
(t/is (= "bold" (get-in resolved ["weight.ok" :resolved-value])))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-font-weight (get-in resolved ["weight.bad" :errors 0 :error/code])))))
|
||||
|
||||
;; The interpreter validates the kind of value of each token type: `1foo`
|
||||
;; and `1 2` are lists, not numbers.
|
||||
(t/deftest resolve-tokens-type-validation-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "size.glued" :value "1foo" :type :dimensions}
|
||||
{:name "size.list" :value "1 2" :type :sizing}
|
||||
{:name "size.ok" :value "1rem" :type :sizing}
|
||||
{:name "number.glued" :value "1foo" :type :number}
|
||||
{:name "number.unit" :value "10px" :type :number}
|
||||
{:name "number.ok" :value "1.5" :type :number}
|
||||
{:name "opacity.percent" :value "150%" :type :opacity}
|
||||
{:name "typography.bad" :value {:font-size "1foo" :line-height "1.5"} :type :typography}
|
||||
{:name "shadow.bad" :value "8" :type :shadow}))
|
||||
code (fn [name] (get-in resolved [name :errors 0 :error/code]))]
|
||||
(doseq [name ["size.glued" "size.list" "number.glued"]]
|
||||
(t/is (nil? (get-in resolved [name :resolved-value])))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value (code name))))
|
||||
(t/is (= "1foo" (get-in resolved ["size.glued" :errors 0 :error/value])))
|
||||
(t/is (= :error.style-dictionary/value-with-units (code "number.unit")))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-opacity (code "opacity.percent")))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-typography (code "typography.bad")))
|
||||
(t/is (= :error.style-dictionary/invalid-token-value-shadow (code "shadow.bad")))
|
||||
(t/is (= 16 (get-in resolved ["size.ok" :resolved-value])))
|
||||
(t/is (= 1.5 (get-in resolved ["number.ok" :resolved-value])))))
|
||||
|
||||
;; A token with a `nil` value (e.g. a composite typography token saved via
|
||||
;; the workspace form with no fields filled in) is tagged with an empty
|
||||
;; input error and does not affect the other tokens.
|
||||
(t/deftest resolve-tokens-nil-value-test
|
||||
(let [resolved (ts/resolve-tokens
|
||||
(tokens-map {:name "typography.empty" :value nil :type :typography}
|
||||
{:name "borderRadius.sm" :value "12px" :type :border-radius}))]
|
||||
(t/is (nil? (get-in resolved ["typography.empty" :resolved-value])))
|
||||
(t/is (= :error.token/empty-input (get-in resolved ["typography.empty" :errors 0 :error/code])))
|
||||
(t/is (= 12 (get-in resolved ["borderRadius.sm" :resolved-value])))))
|
||||
|
||||
(t/deftest resolve-token-test
|
||||
(let [tokens (tokens-map {:name "a" :value "8px" :type :border-radius})
|
||||
resolved (ts/resolve-token tokens {:name "b" :value "{a} * 2" :type :border-radius})]
|
||||
(t/is (= 16 (:resolved-value resolved)))))
|
||||
@ -1805,6 +1805,10 @@ msgstr "Token has circular reference"
|
||||
msgid "errors.tokens.composite-line-height-needs-font-size"
|
||||
msgstr "Line Height depends on Font Size. Add a Font Size to get the resolved value."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:62
|
||||
msgid "errors.tokens.division-by-zero"
|
||||
msgstr "Invalid token value: division by zero"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:350, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:436
|
||||
msgid "errors.tokens.empty-field"
|
||||
msgstr "This field cannot be empty"
|
||||
@ -1817,10 +1821,18 @@ msgstr "Token value cannot be empty"
|
||||
msgid "errors.tokens.error-parse"
|
||||
msgstr "Import Error: Could not parse JSON."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:74
|
||||
msgid "errors.tokens.function-argument-out-of-range"
|
||||
msgstr "Invalid token value: the argument of %s() is out of range"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:33, src/app/main/data/workspace/tokens/errors.cljs:38
|
||||
msgid "errors.tokens.import-error"
|
||||
msgstr "Import Error:"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:66
|
||||
msgid "errors.tokens.incompatible-units"
|
||||
msgstr "Invalid token value: the units %s cannot be combined"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:54
|
||||
msgid "errors.tokens.invalid-color"
|
||||
msgstr "Invalid color value: %s"
|
||||
@ -1914,6 +1926,10 @@ msgstr "Shadow spread must be greater than or equal to 0."
|
||||
msgid "errors.tokens.stroke-width-range"
|
||||
msgstr "Stroke width must be greater than or equal to 0."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:70
|
||||
msgid "errors.tokens.unknown-function"
|
||||
msgstr "Invalid token value: unknown function %s"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:84, ../common/src/app/common/files/tokens.cljc:52
|
||||
msgid "errors.tokens.value-with-percent"
|
||||
msgstr "Invalid value: % is not allowed."
|
||||
|
||||
@ -1837,6 +1837,10 @@ msgstr ""
|
||||
"El Line Height depende del Font Size. Añade un Font Size para obtener el "
|
||||
"valor computado."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:62
|
||||
msgid "errors.tokens.division-by-zero"
|
||||
msgstr "Valor de token no válido: división por cero"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs:350, src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs:436
|
||||
msgid "errors.tokens.empty-field"
|
||||
msgstr "Este campo no puede estar vacío"
|
||||
@ -1849,10 +1853,18 @@ msgstr "El valor del token no puede estar vacío"
|
||||
msgid "errors.tokens.error-parse"
|
||||
msgstr "Error al importar: No se pudo procesar el JSON."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:74
|
||||
msgid "errors.tokens.function-argument-out-of-range"
|
||||
msgstr "Valor de token no válido: el argumento de %s() está fuera de rango"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:33, src/app/main/data/workspace/tokens/errors.cljs:38
|
||||
msgid "errors.tokens.import-error"
|
||||
msgstr "Error al importar:"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:66
|
||||
msgid "errors.tokens.incompatible-units"
|
||||
msgstr "Valor de token no válido: las unidades %s no se pueden combinar"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:54
|
||||
msgid "errors.tokens.invalid-color"
|
||||
msgstr "Valor de color inválido: %s"
|
||||
@ -1947,6 +1959,10 @@ msgstr "La extensión (spread) de la sombra debe ser mayor o igual a 0."
|
||||
msgid "errors.tokens.stroke-width-range"
|
||||
msgstr "Stroke width debe ser mayor o igual a 0."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:70
|
||||
msgid "errors.tokens.unknown-function"
|
||||
msgstr "Valor de token no válido: función desconocida %s"
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:84, ../common/src/app/common/files/tokens.cljc:52
|
||||
msgid "errors.tokens.value-with-percent"
|
||||
msgstr "Valor no válido: no se permite %."
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user