mirror of
https://github.com/penpot/penpot.git
synced 2026-10-28 21:40:34 +00:00
🐛 Fix font family space in tokens (#11750)
* 🐛 Fix font family space in tokens * 🐛 Resolve token merge conflicts Keep the font-family token fix and the newer background-plugin change together in the unreleased changelog and plugin API test suite. AI-assisted-by: gpt-6-luna --------- Co-authored-by: niwinz <843689+niwinz@users.noreply.github.com>
This commit is contained in:
parent
9dc61b1c18
commit
e3b8b006db
@ -8,9 +8,12 @@
|
||||
processTokens
|
||||
TokenSymbol
|
||||
makeConfig]]
|
||||
[app.common.data :as d]
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.workspace.tokens.errors :as wte]))
|
||||
[app.common.types.token :as cto]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(l/set-level! :debug)
|
||||
|
||||
@ -78,6 +81,17 @@
|
||||
|
||||
(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`."
|
||||
@ -85,9 +99,13 @@
|
||||
(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)))))
|
||||
(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]
|
||||
(cond
|
||||
@ -99,6 +117,15 @@
|
||||
(percent-number-with-unit? v) (/ (.-value v) 100)
|
||||
:else (.-value 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)))
|
||||
|
||||
;; Processors ------------------------------------------------------------------
|
||||
;; The processor resolves tokens
|
||||
;; resolved/error tokens get put back into a clojure structure directly during build time
|
||||
@ -130,11 +157,45 @@
|
||||
:onError on-error
|
||||
:getResult get-result}))
|
||||
|
||||
(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."
|
||||
[family]
|
||||
(cond
|
||||
(or (not (string? family))
|
||||
(seq (cto/find-token-value-references family))
|
||||
(re-matches #"^(['\"]).*\1$" family))
|
||||
family
|
||||
|
||||
(not (str/includes? family "\""))
|
||||
(str "\"" family "\"")
|
||||
|
||||
(not (str/includes? family "'"))
|
||||
(str "'" family "'")
|
||||
|
||||
:else
|
||||
family))
|
||||
|
||||
(defn- quote-font-families
|
||||
[value]
|
||||
(if (sequential? value)
|
||||
(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."
|
||||
[{:keys [type value]}]
|
||||
#js {"$type" (name type)
|
||||
"$value" (clj->js value)})
|
||||
"$value" (clj->js (token-value->tokenscript-value type value))})
|
||||
|
||||
(defn clj-tokens->tokenscript-tokens
|
||||
"Convert penpot map of tokens into tokenscript map structure.
|
||||
|
||||
@ -736,7 +736,7 @@
|
||||
|
||||
resolved-value (get-in resolved-tokens [(:name token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
(ts/resolved-value->penpot-unit (:type token) resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
|
||||
@ -218,7 +218,8 @@
|
||||
(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 ts/tokenscript-symbols->penpot-unit))))
|
||||
(d/update-vals #(update % :resolved-value
|
||||
(partial ts/resolved-value->penpot-unit (:type %))))))
|
||||
(sd/resolve-tokens tokens-tree))
|
||||
(rx/mapcat
|
||||
(fn [sd-tokens]
|
||||
|
||||
@ -78,7 +78,7 @@
|
||||
(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)
|
||||
(ts/resolved-value->penpot-unit (:type token) resolved-value)
|
||||
resolved-value)]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
|
||||
@ -46,7 +46,8 @@
|
||||
(fn [resolved-tokens]
|
||||
(let [resolved-token (cond-> (get resolved-tokens (:name token))
|
||||
(contains? cf/flags :tokenscript)
|
||||
(update :resolved-value ts/tokenscript-symbols->penpot-unit))]
|
||||
(update :resolved-value
|
||||
(partial ts/resolved-value->penpot-unit (:type token))))]
|
||||
(cond
|
||||
(:resolved-value resolved-token)
|
||||
(rx/of resolved-token)
|
||||
|
||||
@ -252,13 +252,10 @@
|
||||
|
||||
(defn- font-families-resolved-value->js
|
||||
"Converts a resolved fontFamilies value (a tokenscript list symbol) into the
|
||||
documented `string[]` shape rather than leaking the raw tokenscript structure."
|
||||
documented `string[]` shape."
|
||||
[resolved-value]
|
||||
(let [v (ts/tokenscript-symbols->penpot-unit resolved-value)]
|
||||
(cond
|
||||
(nil? v) nil
|
||||
(sequential? v) (clj->js v)
|
||||
:else #js [v])))
|
||||
(some-> (ts/font-family-symbols->penpot-unit resolved-value)
|
||||
(clj->js)))
|
||||
|
||||
(defn- get-resolved-value
|
||||
[token tokens-tree]
|
||||
|
||||
@ -525,6 +525,71 @@
|
||||
(t/is (array? result))
|
||||
(t/is (= ["Inter" "Arial"] (vec result)))))
|
||||
|
||||
(t/deftest font-family-token-resolved-value-keeps-multi-word-families
|
||||
;; An unquoted family whose name has several words resolves to a list of word
|
||||
;; symbols, so the family name is the string form of each entry.
|
||||
(let [token (ctob/make-token
|
||||
{:name "font.body"
|
||||
:type :font-family
|
||||
:value ["Hanken Grotesk" "IBM Plex Mono"]})
|
||||
result (get-resolved-value token {(:name token) token})]
|
||||
(t/is (array? result))
|
||||
(t/is (= ["Hanken Grotesk" "IBM Plex Mono"] (vec result)))))
|
||||
|
||||
(t/deftest typography-token-resolved-value-keeps-multi-word-families
|
||||
(let [token (ctob/make-token
|
||||
{:name "type.body"
|
||||
:type :typography
|
||||
:value {:font-family ["Hanken Grotesk" "Arial"]
|
||||
:font-size "16px"}})
|
||||
result (get-resolved-value token {(:name token) token})
|
||||
entry (aget result 0)]
|
||||
(t/is (= ["Hanken Grotesk" "Arial"] (vec (aget entry "fontFamilies"))))))
|
||||
|
||||
(t/deftest font-family-token-resolved-value-keeps-names-with-expression-words
|
||||
;; Words like `Red` or `Black` read as colors and `2P` as a number and a word
|
||||
;; when tokenscript parses the family name.
|
||||
(let [families ["Red Hat Display" "Crimson Pro" "Archivo Black"
|
||||
"Press Start 2P" "M PLUS 1p" "Crimson"]
|
||||
token (ctob/make-token
|
||||
{:name "font.display"
|
||||
:type :font-family
|
||||
:value families})
|
||||
result (get-resolved-value token {(:name token) token})]
|
||||
(t/is (= families (vec result)))))
|
||||
|
||||
(t/deftest font-family-token-resolved-value-mixes-references-and-names
|
||||
(let [base (ctob/make-token
|
||||
{:name "font.base"
|
||||
:type :font-family
|
||||
:value ["Red Hat Text"]})
|
||||
token (ctob/make-token
|
||||
{:name "font.stack"
|
||||
:type :font-family
|
||||
:value ["{font.base}" "Rock 3D"]})
|
||||
result (get-resolved-value token {(:name base) base
|
||||
(:name token) token})]
|
||||
(t/is (= ["Red Hat Text" "Rock 3D"] (vec result)))))
|
||||
|
||||
(t/deftest font-family-token-resolved-value-keeps-quoted-names
|
||||
(let [token (ctob/make-token
|
||||
{:name "font.quoted"
|
||||
:type :font-family
|
||||
:value ["\"Red Hat Mono\"" "'Black Ops One'"]})
|
||||
result (get-resolved-value token {(:name token) token})]
|
||||
(t/is (= ["Red Hat Mono" "Black Ops One"] (vec result)))))
|
||||
|
||||
(t/deftest typography-token-resolved-value-keeps-names-with-expression-words
|
||||
(let [token (ctob/make-token
|
||||
{:name "type.display"
|
||||
:type :typography
|
||||
:value {:font-family ["Red Hat Display" "Press Start 2P"]
|
||||
:font-size "16px"}})
|
||||
result (get-resolved-value token {(:name token) token})
|
||||
entry (aget result 0)]
|
||||
(t/is (= ["Red Hat Display" "Press Start 2P"]
|
||||
(vec (aget entry "fontFamilies"))))))
|
||||
|
||||
(t/deftest token-theme-add-set-accepts-token-set-id
|
||||
(let [plugin-id "plugin-id"
|
||||
file-id (uuid/next)
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
- **plugins-runtime**: Removed the premature deep-hardening of the host plugin context, which froze shared host functions (including `Function.prototype`) before SES override taming, causing `TypeError: Cannot assign to read only property 'toString'` on later host-side function extension. Related to #11001.
|
||||
- **plugins-runtime**: The plugin modal no longer shows two resize grips in Firefox. Firefox now shows only its native grip. Closes #11795.
|
||||
- **plugins-runtime**: Fixed the `fontFamilies` token property mapping so `Shape.applyToken(token, ["fontFamilies"])` resolves to the canonical `:font-family` attribute and applied-token readback exposes the documented `fontFamilies` key instead of the undocumented singular `fontFamily`. Closes #11405.
|
||||
- **plugins-runtime**: `TokenFontFamilies.resolvedValue` and the `fontFamilies` member of `TokenTypographyValue` now keep a family whose name has several words (`Hanken Grotesk`) as one entry, instead of splitting it into an array of its words, and keep names with words the resolver would otherwise read as colors or numbers (`Red Hat Display`, `Press Start 2P`) unchanged.
|
||||
- **plugins-runtime**: Opening a plugin no longer drops running background plugins (`allowBackground`, such as the MCP plugin) from the runtime registry. They kept running, but their UI messages were no longer delivered to the plugin and they could no longer be unloaded, so MCP tasks timed out after another plugin was opened.
|
||||
|
||||
## 1.5.0 (2026-07-08)
|
||||
|
||||
@ -418,6 +418,31 @@ describe('Tokens', () => {
|
||||
expect(Object.keys(b.tokens)).toContain('paddingLeft');
|
||||
});
|
||||
|
||||
// The target is a text shape because the attribute set of a board excludes
|
||||
// the font-family property, so the application is filtered out there and
|
||||
// the assertions pass vacuously.
|
||||
// The family comes from the bundled Google fonts catalog, available in every
|
||||
// run. `Red` in its name reads as a color if the resolver parses the words.
|
||||
test('applyToken binds a fontFamilies token to a text shape', async (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const font = ctx.penpot.fonts.findByName('Red Hat Display');
|
||||
if (!font) throw new Error('Red Hat Display font not found');
|
||||
const token = set.addToken({
|
||||
type: 'fontFamilies',
|
||||
name: unique('fontFamilies.'),
|
||||
value: [font.fontFamily],
|
||||
});
|
||||
|
||||
const t = ctx.penpot.createText('Hello Penpot');
|
||||
if (!t) throw new Error('createText returned null');
|
||||
ctx.board.appendChild(t);
|
||||
|
||||
t.applyToken(token, ['fontFamilies']);
|
||||
await waitFor(() => t.fontFamily === font.fontFamily);
|
||||
expect(Object.keys(t.tokens)).toContain('fontFamilies');
|
||||
expect(t.fontFamily).toBe(font.fontFamily);
|
||||
});
|
||||
|
||||
test('applyToken accepts the uniform strokeWidth property', async (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const token = set.addToken({
|
||||
@ -730,6 +755,39 @@ describe('Token types', () => {
|
||||
expect(resolved as unknown as string[]).toContain('Arial');
|
||||
});
|
||||
|
||||
// The resolver parses a family whose name has several words as a list of
|
||||
// word symbols, so each family name must survive as a single entry. A
|
||||
// single-word family takes the simpler path the test above covers.
|
||||
test('fontFamilies token resolvedValue keeps multi-word families whole', (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const token = set.addToken({
|
||||
type: 'fontFamilies',
|
||||
name: unique('fontFamilies.'),
|
||||
value: ['Hanken Grotesk'],
|
||||
});
|
||||
// The stored value keeps the family whole, isolating resolution below.
|
||||
expect(token.value).toEqual(['Hanken Grotesk']);
|
||||
|
||||
const resolved = token.resolvedValue as unknown as string[];
|
||||
expect(Array.isArray(resolved)).toBe(true);
|
||||
expect(resolved).toHaveLength(1);
|
||||
expect(resolved[0]).toBe('Hanken Grotesk');
|
||||
});
|
||||
|
||||
// Words like `Red` or `Black` read as colors and `2P` as a number and a word
|
||||
// when the resolver parses the family name.
|
||||
test('fontFamilies token resolvedValue keeps names with expression words', (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const families = ['Red Hat Display', 'Archivo Black', 'Press Start 2P'];
|
||||
const token = set.addToken({
|
||||
type: 'fontFamilies',
|
||||
name: unique('fontFamilies.'),
|
||||
value: families,
|
||||
});
|
||||
|
||||
expect(token.resolvedValue as unknown as string[]).toEqual(families);
|
||||
});
|
||||
|
||||
test('shadow token exposes its composite value', (ctx) => {
|
||||
const set = activeSet(ctx, unique('set'));
|
||||
const token = set.addToken({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user