Compare commits

..

12 Commits

Author SHA1 Message Date
Andrés Moya
c6ad58db85 🐛 Add resolved value to tokens in plugins API 2026-02-16 16:09:04 +01:00
Andrés Moya
619e2387dc 🐛 Fix applied tokens property names 2026-02-16 15:16:14 +01:00
Andrés Moya
813c804d45 🔧 Enhance schema validation of token application 2026-02-16 15:16:14 +01:00
Andrey Antukh
63f0c68977 Merge remote-tracking branch 'origin/main' into staging 2026-02-16 14:35:28 +01:00
Andrey Antukh
1f2a234458 📚 Update changelog 2026-02-16 14:32:28 +01:00
Andrey Antukh
b281870c50 📚 Update changelog 2026-02-16 14:27:33 +01:00
Andrey Antukh
3909bc0fc1 Merge remote-tracking branch 'origin/main' into staging 2026-02-16 14:17:46 +01:00
Andrey Antukh
b6427ecaac 🐛 Revert yetti upgrade
Because of regression introduced on undertow-core 2.3.19
2026-02-16 14:16:29 +01:00
Andrey Antukh
e82319c49e Merge tag '2.13.2' 2026-02-16 14:13:51 +01:00
Andrey Antukh
ce63bae92d Add better approach for error handling to obj/reify 2026-02-16 11:07:40 +01:00
David Barragán Merino
1349789a7b 🔧 Fix the plugin style documentation build command 2026-02-13 14:20:34 +01:00
David Barragán Merino
d7203ef24c 🔧 Fix the plugin bundle build command 2026-02-13 09:39:12 +01:00
11 changed files with 187 additions and 105 deletions

View File

@@ -35,12 +35,18 @@
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
## 2.13.3
### :bug: Bugs fixed
- Revert yetti (http server) update, because that caused a regression on multipart uploads
## 2.13.2
### :bug: Bugs fixed
- Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317)
- Fix security issue (Path Traversal Vulnerability) on fonts related RPC method
- Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)
## 2.13.1

View File

@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.9"
:git/sha "5fad7a9"
{:git/tag "v11.8"
:git/sha "1d1b33f"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}

View File

@@ -175,6 +175,10 @@
(defn shape-proxy? [p]
(obj/type-of? p "ShapeProxy"))
;; Cannot use token/token-proxy? here because of circular dependency in applyToShapes in token proxy
(defn token-proxy? [t]
(obj/type-of? t "TokenProxy"))
(defn shape-proxy
([plugin-id id]
(shape-proxy plugin-id (:current-file-id @st/state) (:current-page-id @st/state) id))
@@ -1301,16 +1305,19 @@
tokens)))}
:applyToken
(fn [token attrs]
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
kw-attrs (into #{} (map keyword attrs))]
(if (some #(not (cto/token-attr? %)) kw-attrs)
(u/display-not-valid :applyToken attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs kw-attrs
:shape-ids [id]
:expand-with-children false})))))
{:schema [:tuple
[:fn token-proxy?]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [token attrs]
(let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id"))
kw-attrs (into #{} (map keyword attrs))]
(if (some #(not (cto/token-attr? %)) kw-attrs)
(u/display-not-valid :applyToken attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs kw-attrs
:shape-ids [id]
:expand-with-children false})))))}
:isVariantHead
(fn []

View File

@@ -13,10 +13,10 @@
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
;; [app.plugins.shape :as shape]
[app.plugins.utils :as u]
[app.util.object :as obj]
[beicon.v2.core :as rx]
@@ -35,13 +35,25 @@
:shape-ids shape-ids
:expand-with-children false})))))
(defn- get-resolved-value
[token tokens-tree]
(let [resolved-tokens (ts/resolve-tokens tokens-tree)
resolved-value (-> resolved-tokens
(dm/get-in [(:name token) :resolved-value])
(ts/tokenscript-symbols->penpot-unit))]
resolved-value))
(defn token-proxy? [p]
(obj/type-of? p "TokenProxy"))
;; Cannot use shape/shape-proxy? here because of circular dependency in applyToken in shape proxy
(defn shape-proxy? [s]
(obj/type-of? s "ShapeProxy"))
(defn token-proxy
[plugin-id file-id set-id id]
(obj/reify {:name "TokenProxy"
:wrap u/wrap-errors}
:on-error u/handle-error}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$set-id {:enumerable false :get (constantly set-id)}
@@ -82,6 +94,26 @@
(fn [_ value]
(st/emit! (dwtl/update-token set-id id {:value value})))}
:resolvedValue
{:this true
:enumerable false
:get
(fn [_]
(let [token (u/locate-token file-id set-id id)
tokens-lib (u/locate-tokens-lib file-id)
tokens-tree (ctob/get-tokens-in-active-sets tokens-lib)]
(get-resolved-value token tokens-tree)))}
:resolvedValueString
{:this true
:enumerable false
:get
(fn [_]
(let [token (u/locate-token file-id set-id id)
tokens-lib (u/locate-tokens-lib file-id)
tokens-tree (ctob/get-tokens-in-active-sets tokens-lib)]
(str (get-resolved-value token tokens-tree))))}
:description
{:this true
:get
@@ -113,14 +145,10 @@
:applyToShapes
{:schema [:tuple
;; FIXME: the schema decoder is interpreting the array of shape-proxys and converting
;; them to plain maps. For now we adapt the schema to accept it, but the decoder
;; should be fixed to keep the original proxy objects coming from the plugin.
;; [:vector [:fn shape/shape-proxy?]]
[:vector [:map [:id ::sm/uuid]]]
[:maybe [:set [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]]
[:vector [:fn shape-proxy?]]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map :id shapes) attrs))}
(apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
:applyToSelected
{:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
@@ -136,7 +164,7 @@
(defn token-set-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenSetProxy"
:wrap u/wrap-errors}
:on-error u/handle-error}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -257,7 +285,7 @@
(defn token-theme-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenThemeProxy"
:wrap u/wrap-errors}
:on-error u/handle-error}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -352,7 +380,7 @@
(defn tokens-catalog
[plugin-id file-id]
(obj/reify {:name "TokensCatalog"
:wrap u/wrap-errors}
:on-error u/handle-error}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$id {:enumerable false :get (constantly file-id)}

View File

@@ -223,7 +223,9 @@
(defn display-not-valid
[code value]
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
(if (some? value)
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))
nil)
(defn reject-not-valid
@@ -248,19 +250,12 @@
(let [s (set values)]
(if (= (count s) 1) (first s) "mixed")))
(defn wrap-errors
"Function wrapper to be used in plugin proxies methods to handle errors.
When an exception is thrown, a readable error message is output to the console
and the exception is captured."
[f]
(fn []
(let [args (js-arguments)]
(try
(.apply f nil args)
(catch :default cause
(display-not-valid (ex-message cause) (obj/stringify args))
(if-let [explain (-> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.log (ex-data cause)))
(js/console.log (.-stack cause))
nil)))))
(defn handle-error
"Function to be used in plugin proxies methods to handle errors and print a readable
message to the console."
[cause]
(display-not-valid (ex-message cause) nil)
(if-let [explain (-> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.log (ex-data cause)))
(js/console.log (.-stack cause)))

View File

@@ -10,6 +10,7 @@
(:refer-clojure :exclude [set! new get merge clone contains? array? into-array reify class])
#?(:cljs (:require-macros [app.util.object]))
(:require
[app.common.data :as d]
[app.common.json :as json]
[app.common.schema :as sm]
[clojure.core :as c]
@@ -156,6 +157,7 @@
this-sym (with-meta (gensym (str rsym "-this-")) {:tag 'js})
target-sym (with-meta (gensym (str rsym "-target-")) {:tag 'js})
cause-sym (gensym "cause-")
make-sym
(fn [pname prefix]
@@ -176,6 +178,7 @@
wrap (c/get params :wrap)
schema-1 (c/get params :schema-1)
this? (c/get params :this false)
on-error (c/get params :on-error)
decode-expr
(c/get params :decode/fn)
@@ -214,7 +217,16 @@
(with-meta {:tag 'function}))
val-sym
(gensym (str "val-" (str/slug pname) "-"))]
(gensym (str "val-" (str/slug pname) "-"))
wrap-error-handling
(if on-error
(fn [expr]
`(try
~expr
(catch :default ~cause-sym
(~on-error ~cause-sym))))
identity)]
(concat
(when wrap
@@ -226,8 +238,13 @@
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
(.call ~fn-sym ~this-sym ~this-sym)))
get-expr)])
~(wrap-error-handling
`(.call ~fn-sym ~this-sym ~this-sym))))
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~get-expr]
~(wrap-error-handling
`(.call ~fn-sym ~this-sym)))))])
(when set-expr
[schema-sym schema-n
@@ -241,28 +258,35 @@
(make-sym pname "set-fn")
`(fn [~val-sym]
(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
~(wrap-error-handling
`(let [~this-sym (~'js* "this")
~fn-sym ~set-expr
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym `(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
;; We only emit schema and coercer bindings if
;; schema-n is provided
~@(if (some? schema-n)
[schema-sym
`(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
coercer-sym `(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
val-sym (if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym `(~coercer-sym ~val-sym)]
[])]
coercer-sym
`(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym))))])
val-sym
(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym
`(~coercer-sym ~val-sym)]
[])]
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)
`(.call ~fn-sym ~this-sym ~val-sym)))))])
(when fn-expr
[schema-sym (or schema-n schema-1)
@@ -275,7 +299,12 @@
(make-sym pname "get-fn")
`(fn []
(let [~this-sym (~'js* "this")
~fn-sym ~fn-expr
~fn-sym ~(if (and (list? fn-expr)
(= 'fn (first fn-expr)))
(let [[sa sb & sother] fn-expr]
`(~sa ~sb ~(wrap-error-handling `(do ~@sother))))
fn-expr)
~fn-sym ~(if this?
`(.bind ~fn-sym ~this-sym ~this-sym)
`(.bind ~fn-sym ~this-sym))
@@ -284,25 +313,31 @@
;; schema-n or schema-1 is provided
~@(if (or schema-n schema-1)
[fn-sym `(fn* [~@(if schema-1 [val-sym] [])]
(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym ~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~(wrap-error-handling
`(let [~@(if schema-n
[val-sym `(into-array (cljs.core/js-arguments))]
[])
~val-sym
~(if (not= decode-expr 'app.common.json/->clj)
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
~schema-sym (if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
~schema-sym
(if (fn? ~schema-sym)
(~schema-sym ~val-sym)
~schema-sym)
~coercer-sym (if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~coercer-sym
(if (nil? ~coercer-sym)
(sm/coercer ~schema-sym)
~coercer-sym)
~val-sym (~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym))))]
~val-sym
(~coercer-sym ~val-sym)]
~(if schema-1
`(~fn-sym ~val-sym)
`(apply ~fn-sym ~val-sym)))))]
[])]
~(if wrap
`(~wrap-sym ~fn-sym)
@@ -375,12 +410,16 @@
(let [definition (first params)]
(if (some? definition)
(let [definition (if (map? definition)
(c/merge {:wrap (:wrap tmeta)} definition)
(c/merge {:wrap (:wrap tmeta)
:on-error (:on-error tmeta)}
definition)
(-> {:enumerable false}
(c/merge (meta definition))
(assoc :wrap (:wrap tmeta))
(assoc :on-error (:on-error tmeta))
(assoc :fn definition)
(dissoc :get :set)))
(dissoc :get :set :line :column)
(d/without-nils)))
definition (assoc definition :name (name ckey))]
(recur (rest params)

View File

@@ -113,7 +113,9 @@
class="body-m panel-item token-item"
(click)="applyToken(token.id)"
>
<span>{{ token.name }}</span>
<span title="{{ token.resolvedValueString }}">
{{ token.name }}
</span>
<button
type="button"
data-appearance="secondary"

View File

@@ -23,6 +23,7 @@ type Token = {
id: string;
name: string;
description: string;
resolvedValueString: string;
};
type TokensGroup = [string, Token[]];

View File

@@ -109,6 +109,7 @@ function loadTokens(setId: string) {
id: token.id,
name: token.name,
description: token.description,
resolvedValueString: token.resolvedValueString,
};
}),
]);

View File

@@ -4345,6 +4345,15 @@ export interface TokenBase {
*/
remove(): void;
/**
* The value calculated by finding all tokens with the same name in active sets
* and resolving the references.
*
* It's converted to string, regardless of the data type of the value depending
* on the token type. It can be undefined if no value has been found in active sets.
*/
readonly resolvedValueString: string | undefined;
/**
* Applies this token to one or more properties of the given shapes.
* @param shapes is an array of shapes to apply it.
@@ -5226,27 +5235,27 @@ type TokenDimensionProps =
/**
* The properties that a FontFamilies token can be applied to.
*/
type TokenFontFamiliesProps = 'font-families';
type TokenFontFamiliesProps = 'fontFamilies';
/**
* The properties that a FontSizes token can be applied to.
*/
type TokenFontSizesProps = 'font-size';
type TokenFontSizesProps = 'fontSize';
/**
* The properties that a FontWeight token can be applied to.
*/
type TokenFontWeightProps = 'font-weight';
type TokenFontWeightProps = 'fontWeight';
/**
* The properties that a LetterSpacing token can be applied to.
*/
type TokenLetterSpacingProps = 'letter-spacing';
type TokenLetterSpacingProps = 'letterSpacing';
/**
* The properties that a Number token can be applied to.
*/
type TokenNumberProps = 'rotation' | 'line-height';
type TokenNumberProps = 'rotation';
/**
* The properties that an Opacity token can be applied to.
@@ -5262,18 +5271,18 @@ type TokenSizingProps =
| 'height'
// Layout
| 'layout-item-min-w'
| 'layout-item-max-w'
| 'layout-item-min-h'
| 'layout-item-max-h';
| 'layoutItemMinW'
| 'layoutItemMaxW'
| 'layoutItemMinH'
| 'layoutItemMaxH';
/**
* The properties that a Spacing token can be applied to.
*/
type TokenSpacingProps =
// Spacing / Gap
| 'row-gap'
| 'column-gap'
| 'rowGap'
| 'columnGap'
// Spacing / Padding
| 'p1'
@@ -5290,17 +5299,17 @@ type TokenSpacingProps =
/**
* The properties that a BorderWidth token can be applied to.
*/
type TokenBorderWidthProps = 'stroke-width';
type TokenBorderWidthProps = 'strokeWidth';
/**
* The properties that a TextCase token can be applied to.
*/
type TokenTextCaseProps = 'text-case';
type TokenTextCaseProps = 'textCase';
/**
* The properties that a TextDecoration token can be applied to.
*/
type TokenTextDecorationProps = 'text-decoration';
type TokenTextDecorationProps = 'textDecoration';
/**
* The properties that a Typography token can be applied to.

View File

@@ -441,18 +441,12 @@ pub fn propagate_modifiers(
db.cmp(&da)
});
// This temporary bounds is necesary so the layouts can be calculated
// correctly but will be discarded before the next iteration for the
// bounds to be calculated properly with the modifiers.
let mut bounds_temp = bounds.clone();
for id in &layout_reflows_vec {
if reflown.contains(id) {
continue;
}
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp);
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds);
}
layout_reflows = HashSet::new();
}
#[allow(dead_code)]