mirror of
https://github.com/penpot/penpot.git
synced 2026-05-30 04:08:08 +00:00
🐛 Fix DTCG token import discriminator and group-level $type inheritance (#9912)
* 🐛 Fix DTCG token import discriminator and group-level $type inheritance Closes #8342. The DTCG Community Group Final Report (W3C, 2025-10-28) specifies: "The presence of a $value property definitively identifies an object as a token." "A token's type can be specified by the optional $type property [...] Furthermore, the $type property on a group applies to all tokens nested within that group." Two bugs in `common/src/app/common/types/tokens_lib.cljc` violate the spec and silently break import of any third-party DTCG file that uses group-level type inheritance: 1. `flatten-nested-tokens-json` used `(not (contains? v "$type"))` as the group-vs-token discriminator. A group node carrying only a `$type` (to set a default for child tokens) was misidentified as a token, then immediately discarded because it had no `$value`. 2. `schema:dtcg-node` declared both `$type` and `$value` as required, so even after the discriminator was fixed any leaf token that relied on group-level type inheritance failed `dtcg-node?` validation and never reached the parser. The combined effect: importing a spec-compliant DTCG file that expressed types at the group level produced a TokensLib with no tokens at all, because every leaf was discarded as "unknown type". Penpot-exported files were unaffected because Penpot always emits both `$type` and `$value` on every token and never attaches `$type` to a group, so the existing tests covered only the inline-type shape. - `schema:dtcg-node`: mark `$type` optional. - `flatten-nested-tokens-json`: use `$value` as the discriminator (anything without `$value` is a group), accept an optional `inherited-type` accumulator that carries the nearest enclosing group `$type` down through recursion, and resolve a token's type from its own `$type` first, falling back to the inherited type. A token's own `$type` always wins over the inherited one (per spec). Added `parse-dtcg-group-type-inheritance` covering both cases: - group `$type` is inherited by tokens that don't declare their own (`colors.red`, `colors.blue`, `space.small`) - token `$type` overrides the inherited group `$type` (`colors.danger`, `space.large`) Existing DTCG round-trip tests continue to pass because they all declare `$type` at the token level, which the new code still honours. CHANGES.md entry added under the 2.17.0 Bugs-fixed section. * 📚 Do not update CHANGES.md We are changing the procedures to not update the changelog on each PR. Instead, we use github tracking to check what issues come in a release, and update the changelog automatically in a batch. Signed-off-by: Andrés Moya <hirunatan@hammo.org> --------- Signed-off-by: Andrés Moya <hirunatan@hammo.org> Co-authored-by: MilosM348 <milos.milic001@outlook.com>
This commit is contained in:
parent
4c8b33691a
commit
1f35f57258
63
CHANGES.md
63
CHANGES.md
@ -26,32 +26,43 @@
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix plugin API `LibraryTypography.remove()` failing with a UUID assertion error (by @leonaIee) [#8223](https://github.com/penpot/penpot/issues/8223) (PR: [#9279](https://github.com/penpot/penpot/pull/9279))
|
||||
- Fix MCP SSE sessions leaking on zombie connections by adding inactivity timeout (by @bitloi) [#9432](https://github.com/penpot/penpot/issues/9432) (PR: [#9464](https://github.com/penpot/penpot/pull/9464))
|
||||
- Use "copia" instead of "copiar" as Spanish duplicate-suffix for design tokens (by @Rene0422) [#9623](https://github.com/penpot/penpot/issues/9623) (PR: [#9671](https://github.com/penpot/penpot/pull/9671))
|
||||
- Expose Source Sans Pro semibold (weight 600) variants in the builtin fonts list (by @dhgoal) [#7378](https://github.com/penpot/penpot/issues/7378) (PR: [#9247](https://github.com/penpot/penpot/pull/9247))
|
||||
- Fix plugin API `shape.fills` and `shape.strokes` arrays being read-only (by @RenzoMXD) [#8357](https://github.com/penpot/penpot/issues/8357) (PR: [#9161](https://github.com/penpot/penpot/pull/9161))
|
||||
- Fix `get-profile` masking transient DB errors as anonymous user (by @jack-stormentswe) [#9253](https://github.com/penpot/penpot/issues/9253) (PR: [#9254](https://github.com/penpot/penpot/pull/9254))
|
||||
- Fix `Ctrl+'` "Show guides" shortcut on non-US keyboard layouts (by @RenzoMXD) [#8423](https://github.com/penpot/penpot/issues/8423) (PR: [#9209](https://github.com/penpot/penpot/pull/9209))
|
||||
- Fix lost-update race on `team.features` during concurrent file creation (by @web-dev0521) [#9197](https://github.com/penpot/penpot/issues/9197) (PR: [#9198](https://github.com/penpot/penpot/pull/9198))
|
||||
- Fix clipboard crash on insecure origins (plain HTTP / non-localhost) (by @MilosM348) [#6514](https://github.com/penpot/penpot/issues/6514) (PR: [#9188](https://github.com/penpot/penpot/pull/9188))
|
||||
- Fix blend-mode hover preview not reverting when dropdown is dismissed (by @jack-stormentswe) [#9235](https://github.com/penpot/penpot/issues/9235) (PR: [#9237](https://github.com/penpot/penpot/pull/9237))
|
||||
- Scoll to new created tokens on the tokens tree [#9711](https://github.com/penpot/penpot/issues/9711) (PR: [#9803](https://github.com/penpot/penpot/pull/9803))
|
||||
- Fix text style name input appending font name instead of replacing when edited [#9785](https://github.com/penpot/penpot/issues/9785) (PR: [#9784](https://github.com/penpot/penpot/pull/9784))
|
||||
- Fix crash when creating or editing a token named "white" or "black" [#9256](https://github.com/penpot/penpot/issues/9256) (PR: [#9034](https://github.com/penpot/penpot/pull/9034))
|
||||
- Fix MCP media upload errors and SVG import losing embedded images (by @claytonlin1110) [#9164](https://github.com/penpot/penpot/issues/9164) (PR: [#9201](https://github.com/penpot/penpot/pull/9201))
|
||||
- Fix conditional hook violation in shape-wrapper component (by @Dexterity104) [#9280](https://github.com/penpot/penpot/issues/9280) (PR: [#9281](https://github.com/penpot/penpot/pull/9281))
|
||||
- Fix panic paths in ShapeImageIds byte conversion (by @Dexterity104) [#9282](https://github.com/penpot/penpot/issues/9282) (PR: [#9283](https://github.com/penpot/penpot/pull/9283))
|
||||
- Fix viewers being able to overwrite file thumbnails (by @jony376) [#9284](https://github.com/penpot/penpot/issues/9284) (PR: [#9285](https://github.com/penpot/penpot/pull/9285))
|
||||
- Fix different behavior on switch for two identical copies (by @MischaPanch) [#9498](https://github.com/penpot/penpot/issues/9498) (PR: [#9434](https://github.com/penpot/penpot/pull/9434))
|
||||
- Populate is-indirect flag on file libraries from relation graph (by @Dexterity104) [#9506](https://github.com/penpot/penpot/issues/9506) (PR: [#9289](https://github.com/penpot/penpot/pull/9289))
|
||||
- Fix incorrect error message in plugins API (by @bitcompass) [#9417](https://github.com/penpot/penpot/issues/9417) (PR: [#9486](https://github.com/penpot/penpot/pull/9486))
|
||||
- Fix library update dialog triggered by component position changes [#9629](https://github.com/penpot/penpot/issues/9629) (PR: [#9616](https://github.com/penpot/penpot/pull/9616))
|
||||
- Fix missing error message for invalid shadow token [#9583](https://github.com/penpot/penpot/issues/9583) (PR: [#9809](https://github.com/penpot/penpot/pull/9809))
|
||||
- Fix misaligned B(V) input label in HSB color picker [#9731](https://github.com/penpot/penpot/issues/9731) (PR: [#9793](https://github.com/penpot/penpot/pull/9793))
|
||||
- Fix shadow token creation not allowing empty blur or spread value [#9808](https://github.com/penpot/penpot/issues/9808) (PR: [#9809](https://github.com/penpot/penpot/pull/9809))
|
||||
- Fix settings form visual layout broken after recent contribution [#9882](https://github.com/penpot/penpot/issues/9882) (PR: [#9883](https://github.com/penpot/penpot/pull/9883))
|
||||
- Fix token validation failure caused by group nodes in token tree [#9010](https://github.com/penpot/penpot/issues/9010) (PR: [#9025](https://github.com/penpot/penpot/pull/9025), [#9825](https://github.com/penpot/penpot/pull/9825))
|
||||
- Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092)
|
||||
- Fix LDAP provider params schema typo (`bind-passwor` → `bind-password`) introduced during the `clojure.spec` → `malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated
|
||||
- Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide` → `:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key
|
||||
- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108)
|
||||
- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD
|
||||
- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877)
|
||||
- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838)
|
||||
- Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947)
|
||||
- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582)
|
||||
- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526)
|
||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
||||
- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
|
||||
- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639)
|
||||
- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786)
|
||||
- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841)
|
||||
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
|
||||
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
|
||||
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
|
||||
- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
|
||||
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
|
||||
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
|
||||
- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris099) [Github #8577](https://github.com/penpot/penpot/issues/8577)
|
||||
- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628)
|
||||
- Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901)
|
||||
- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959)
|
||||
- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960)
|
||||
- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984)
|
||||
- Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977)
|
||||
- Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979)
|
||||
- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990)
|
||||
- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067)
|
||||
- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516)
|
||||
- Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090)
|
||||
- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137)
|
||||
- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409)
|
||||
- Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479)
|
||||
- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057)
|
||||
|
||||
## 2.16.0 (Unreleased)
|
||||
|
||||
|
||||
@ -1598,6 +1598,13 @@ Will return a value that matches this schema:
|
||||
["type" :string]]]))
|
||||
|
||||
(def ^:private schema:dtcg-node
|
||||
;; Per the DTCG Community Group Final Report (W3C, 2025-10-28):
|
||||
;; "The presence of a $value property definitively identifies an object as a token."
|
||||
;; "A token's type can be specified by the optional $type property [...]
|
||||
;; Furthermore, the $type property on a group applies to all tokens nested
|
||||
;; within that group."
|
||||
;; So $value is the only required property; $type may be inherited from a
|
||||
;; parent group and is therefore optional on the token itself.
|
||||
[:schema {:registry
|
||||
{::simple-value
|
||||
[:or :string :int :double ::sm/boolean]
|
||||
@ -1610,7 +1617,7 @@ Will return a value that matches this schema:
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]]]]}}
|
||||
[:map
|
||||
["$type" :string]
|
||||
["$type" {:optional true} :string]
|
||||
["$value" [:ref ::value]]]])
|
||||
|
||||
(def ^:private dtcg-node?
|
||||
@ -1749,33 +1756,51 @@ Will return a value that matches this schema:
|
||||
|
||||
(defn- flatten-nested-tokens-json
|
||||
"Convert a tokens tree in the decoded json fragment into a flat map,
|
||||
being the keys the token paths after joining the keys with '.'."
|
||||
[decoded-json-tokens parent-path]
|
||||
(reduce-kv
|
||||
(fn [tokens k v]
|
||||
(let [child-path (if (empty? parent-path)
|
||||
(name k)
|
||||
(str parent-path "." k))]
|
||||
(if (and (map? v)
|
||||
(not (contains? v "$type")))
|
||||
(merge tokens (flatten-nested-tokens-json v child-path))
|
||||
(let [token-type (cto/dtcg-token-type->token-type (get v "$type"))]
|
||||
(if token-type
|
||||
(assoc tokens child-path (make-token
|
||||
:name child-path
|
||||
:type token-type
|
||||
:value
|
||||
(let [token-value (get v "$value")]
|
||||
(case token-type
|
||||
:font-family (convert-dtcg-font-family token-value)
|
||||
:typography (convert-dtcg-typography-composite token-value)
|
||||
:shadow (convert-dtcg-shadow-composite token-value)
|
||||
token-value))
|
||||
:description (get v "$description")))
|
||||
;; Discard unknown type tokens
|
||||
tokens)))))
|
||||
{}
|
||||
decoded-json-tokens))
|
||||
being the keys the token paths after joining the keys with '.'.
|
||||
|
||||
Per the DTCG spec, a node is a token iff it carries a `$value`
|
||||
property; every other map is a group. A group may declare a `$type`
|
||||
that is inherited by every nested token that does not declare its
|
||||
own `$type`, so this walker propagates the nearest enclosing group
|
||||
`$type` down through the recursion."
|
||||
([decoded-json-tokens parent-path]
|
||||
(flatten-nested-tokens-json decoded-json-tokens parent-path nil))
|
||||
([decoded-json-tokens parent-path inherited-type]
|
||||
(reduce-kv
|
||||
(fn [tokens k v]
|
||||
(let [child-path (if (empty? parent-path)
|
||||
(name k)
|
||||
(str parent-path "." k))]
|
||||
(if (and (map? v)
|
||||
(not (contains? v "$value")))
|
||||
;; Group: recurse, letting the group's `$type` (if any) replace
|
||||
;; the inherited type for descendants that don't declare their
|
||||
;; own `$type`.
|
||||
(merge tokens
|
||||
(flatten-nested-tokens-json
|
||||
v
|
||||
child-path
|
||||
(or (get v "$type") inherited-type)))
|
||||
;; Token: resolve the type from the token's own `$type`,
|
||||
;; falling back to the inherited group `$type`.
|
||||
(let [type-key (or (get v "$type") inherited-type)
|
||||
token-type (cto/dtcg-token-type->token-type type-key)]
|
||||
(if token-type
|
||||
(assoc tokens child-path (make-token
|
||||
:name child-path
|
||||
:type token-type
|
||||
:value
|
||||
(let [token-value (get v "$value")]
|
||||
(case token-type
|
||||
:font-family (convert-dtcg-font-family token-value)
|
||||
:typography (convert-dtcg-typography-composite token-value)
|
||||
:shadow (convert-dtcg-shadow-composite token-value)
|
||||
token-value))
|
||||
:description (get v "$description")))
|
||||
;; Discard unknown / un-typeable tokens
|
||||
tokens)))))
|
||||
{}
|
||||
decoded-json-tokens)))
|
||||
|
||||
(defn- parse-single-set-dtcg-json
|
||||
"Parse a decoded json file with a single set of tokens in DTCG format into a TokensLib."
|
||||
|
||||
@ -1348,6 +1348,50 @@
|
||||
(t/testing "token added"
|
||||
(t/is (some? (ctob/get-token-by-name lib "single_set" "color.red.100")))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest parse-dtcg-group-type-inheritance
|
||||
;; Per DTCG spec: $type on a group is inherited by every nested token
|
||||
;; that does not declare its own $type. Tokens are identified by the
|
||||
;; presence of $value, not by the presence of $type.
|
||||
(let [json {"colors" {"$type" "color"
|
||||
"red" {"$value" "#ff0000"}
|
||||
"blue" {"$value" "#0000ff"
|
||||
"$description" "Brand blue"}
|
||||
"danger" {"$type" "color"
|
||||
"$value" "#cc0000"}}
|
||||
"space" {"$type" "dimension"
|
||||
"small" {"$value" "4px"}
|
||||
"large" {"$value" "16px"
|
||||
"$type" "dimension"}}}
|
||||
lib (ctob/parse-decoded-json json "set")]
|
||||
(t/testing "group `$type` is inherited by tokens without their own `$type`"
|
||||
(t/is (tht/token-data-eq? (ctob/get-token-by-name lib "set" "colors.red")
|
||||
{:name "colors.red"
|
||||
:type :color
|
||||
:value "#ff0000"
|
||||
:description ""}))
|
||||
(t/is (tht/token-data-eq? (ctob/get-token-by-name lib "set" "colors.blue")
|
||||
{:name "colors.blue"
|
||||
:type :color
|
||||
:value "#0000ff"
|
||||
:description "Brand blue"}))
|
||||
(t/is (tht/token-data-eq? (ctob/get-token-by-name lib "set" "space.small")
|
||||
{:name "space.small"
|
||||
:type :dimensions
|
||||
:value "4px"
|
||||
:description ""})))
|
||||
(t/testing "token `$type` overrides the inherited group `$type`"
|
||||
(t/is (tht/token-data-eq? (ctob/get-token-by-name lib "set" "colors.danger")
|
||||
{:name "colors.danger"
|
||||
:type :color
|
||||
:value "#cc0000"
|
||||
:description ""}))
|
||||
(t/is (tht/token-data-eq? (ctob/get-token-by-name lib "set" "space.large")
|
||||
{:name "space.large"
|
||||
:type :dimensions
|
||||
:value "16px"
|
||||
:description ""}))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest parse-multi-set-legacy-json
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-legacy-example.json")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user