🎉 Add addTokensLib method to the library

This commit is contained in:
Andrey Antukh
2025-10-24 14:21:56 +02:00
parent ec5e814a72
commit ef271db879
8 changed files with 219 additions and 20 deletions

View File

@@ -485,6 +485,13 @@
(commit-change change1) (commit-change change1)
(commit-change change2)))) (commit-change change2))))
(defn add-tokens-lib
[state tokens-lib]
(-> state
(commit-change
{:type :set-tokens-lib
:tokens-lib tokens-lib})))
(defn delete-shape (defn delete-shape
[file id] [file id]
(commit-change (commit-change

View File

@@ -371,7 +371,7 @@
[:set-tokens-lib [:set-tokens-lib
[:map {:title "SetTokensLib"} [:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]] [:type [:= :set-tokens-lib]]
[:tokens-lib ::sm/any]]] ;; TODO: we should define a plain object schema for tokens-lib [:tokens-lib ctob/schema:tokens-lib]]]
[:set-token [:set-token
[:map {:title "SetTokenChange"} [:map {:title "SetTokenChange"}

View File

@@ -7,10 +7,11 @@
(ns app.common.types.tokens-lib (ns app.common.types.tokens-lib
(:require (:require
#?(:clj [app.common.fressian :as fres]) #?(:clj [app.common.fressian :as fres])
#?(:clj [clojure.data.json :as json]) #?(:clj [clojure.data.json :as c.json])
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.json :as json]
[app.common.path-names :as cpn] [app.common.path-names :as cpn]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.schema.generators :as sg] [app.common.schema.generators :as sg]
@@ -198,8 +199,8 @@
:tokens tokens}) :tokens tokens})
#?@(:clj #?@(:clj
[json/JSONWriter [c.json/JSONWriter
(-write [this writter options] (json/-write (datafy this) writter options))]) (-write [this writter options] (c.json/-write (datafy this) writter options))])
INamedItem INamedItem
(get-id [_] (get-id [_]
@@ -912,6 +913,7 @@ Will return a value that matches this schema:
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name")) (get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
(declare parse-multi-set-dtcg-json) (declare parse-multi-set-dtcg-json)
(declare read-multi-set-dtcg)
(declare export-dtcg-json) (declare export-dtcg-json)
(deftype TokensLib [sets themes active-themes] (deftype TokensLib [sets themes active-themes]
@@ -923,8 +925,8 @@ Will return a value that matches this schema:
:active-themes active-themes}) :active-themes active-themes})
#?@(:clj #?@(:clj
[json/JSONWriter [c.json/JSONWriter
(-write [this writter options] (json/-write (export-dtcg-json this) writter options))]) (-write [this writter options] (c.json/-write (export-dtcg-json this) writter options))])
ITokenSets ITokenSets
;; Naming conventions: ;; Naming conventions:
@@ -1409,7 +1411,11 @@ Will return a value that matches this schema:
;; function that is declared but not defined; so we need to pass ;; function that is declared but not defined; so we need to pass
;; an anonymous function and delegate the resolution to runtime ;; an anonymous function and delegate the resolution to runtime
{:encode/json #(export-dtcg-json %) {:encode/json #(export-dtcg-json %)
:decode/json #(parse-multi-set-dtcg-json %)}})) :decode/json #(read-multi-set-dtcg %)
;; FIXME: add better, more reallistic generator
:gen/gen (->> (sg/small-int)
(sg/fmap (fn [_]
(make-tokens-lib))))}}))
(defn duplicate-set (defn duplicate-set
"Make a new set with a unique name, copying data from the given set in the lib." "Make a new set with a unique name, copying data from the given set in the lib."
@@ -1453,18 +1459,20 @@ Will return a value that matches this schema:
["value" :map] ["value" :map]
["type" :string]]])) ["type" :string]]]))
(def ^:private schema:dtcg-node
[:or
[:map
["$value" :string]
["$type" :string]]
[:map
["$value" [:sequential [:map ["$type" :string]]]]
["$type" :string]]
[:map
["$value" :map]
["$type" :string]]])
(def ^:private dtcg-node? (def ^:private dtcg-node?
(sm/validator (sm/validator schema:dtcg-node))
[:or
[:map
["$value" :string]
["$type" :string]]
[:map
["$value" [:sequential [:map ["$type" :string]]]]
["$type" :string]]
[:map
["$value" :map]
["$type" :string]]]))
(defn- get-json-format (defn- get-json-format
"Searches through decoded token file and returns: "Searches through decoded token file and returns:
@@ -1651,6 +1659,43 @@ Will return a value that matches this schema:
(assert (= (get-json-format decoded-json-tokens) :json-format/legacy) "expected a legacy format for `decoded-json-tokens`") (assert (= (get-json-format decoded-json-tokens) :json-format/legacy) "expected a legacy format for `decoded-json-tokens`")
(parse-single-set-dtcg-json set-name (legacy-json->dtcg-json decoded-json-tokens))) (parse-single-set-dtcg-json set-name (legacy-json->dtcg-json decoded-json-tokens)))
(def ^:private schema:multi-set-dtcg
"Schema for penpot multi-set dtcg json decoded data/
Mainly used for validate the structure of the incoming data before
proceed to parse it to our internal data structures."
[:schema {:registry
{::node
[:or
[:map-of :string [:ref ::node]]
schema:dtcg-node]}}
[:map
["$themes" {:optional true}
[:vector
[:map {:title "Theme"}
["id" {:optional true} :string]
["name" :string]
["description" :string]
["isSource" :boolean]
["selectedTokenSets"
[:map-of :string [:enum "enabled" "disabled"]]]]]]
["$metadata" {:optional true}
[:map {:title "Metadata"}
["tokenSetOrder" {:optional true} [:vector :string]]
["activeThemes" {:optional true} [:vector :string]]
["activeSets" {:optional true} [:vector :string]]]]
[:malli.core/default
[:map-of :string [:ref ::node]]]]])
(def ^:private check-multi-set-dtcg-data
(sm/check-fn schema:multi-set-dtcg))
(def ^:private decode-multi-set-dtcg-data
(sm/decoder schema:multi-set-dtcg
sm/json-transformer))
;; FIXME: remove `-json` suffix
(defn parse-multi-set-dtcg-json (defn parse-multi-set-dtcg-json
"Parse a decoded json file with multi sets in DTCG format into a TokensLib." "Parse a decoded json file with multi sets in DTCG format into a TokensLib."
[decoded-json] [decoded-json]
@@ -1741,6 +1786,23 @@ Will return a value that matches this schema:
library)) library))
(defn read-multi-set-dtcg
"Read penpot multi-set dctg tokens. Accepts string or JSON decoded
data (without any case transformation). Used as schema decoder and
in the SDK."
[data]
(let [data (if (string? data)
(json/decode data :key-fn identity)
data)
data #?(:cljs (if (object? data)
(json/->clj data :key-fn identity)
data)
:clj data)
data (decode-multi-set-dtcg-data data)]
(-> (check-multi-set-dtcg-data data)
(parse-multi-set-dtcg-json))))
(defn- parse-multi-set-legacy-json (defn- parse-multi-set-legacy-json
"Parse a decoded json file with multi sets in legacy format into a TokensLib." "Parse a decoded json file with multi sets in legacy format into a TokensLib."
[decoded-json] [decoded-json]
@@ -1753,6 +1815,7 @@ Will return a value that matches this schema:
(parse-multi-set-dtcg-json (merge other-data (parse-multi-set-dtcg-json (merge other-data
dtcg-sets-data)))) dtcg-sets-data))))
;; FIXME: remove `-json` suffix
(defn parse-decoded-json (defn parse-decoded-json
"Guess the format and content type of the decoded json file and parse it into a TokensLib. "Guess the format and content type of the decoded json file and parse it into a TokensLib.
The `file-name` is used to determine the set name when the json file contains a single set." The `file-name` is used to determine the set name when the json file contains a single set."

View File

@@ -1,5 +1,10 @@
# CHANGELOG # CHANGELOG
## 1.1.0-RC1
- Add experimental addTokensLib method
## 1.0.11 ## 1.0.11
- Set correct path if it is not provided on addComponent - Set correct path if it is not provided on addComponent

View File

@@ -1,13 +1,13 @@
{ {
"name": "@penpot/library", "name": "@penpot/library",
"version": "1.0.11", "version": "1.1.0-RC1",
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f", "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f",
"type": "module", "type": "module",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "git+https://github.com/penpot/penpot.git"
}, },
"resolutions": { "resolutions": {
"@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch" "@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch"

View File

@@ -11,6 +11,7 @@
[app.common.files.builder :as fb] [app.common.files.builder :as fb]
[app.common.json :as json] [app.common.json :as json]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.tokens-lib :refer [read-multi-set-dtcg]]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.util.object :as obj])) [app.util.object :as obj]))
@@ -263,6 +264,15 @@
:mtype (get fmedia :mtype)}] :mtype (get fmedia :mtype)}]
(json/->js (d/without-nils image)))))) (json/->js (d/without-nils image))))))
:addTokensLib
(fn [data]
(try
(let [tlib (read-multi-set-dtcg data)]
(swap! state fb/add-tokens-lib tlib)
nil)
(catch :default cause
(handle-exception cause))))
:genId :genId
(fn [] (fn []
(dm/str (uuid/next))) (dm/str (uuid/next)))

View File

@@ -0,0 +1,62 @@
{
"a": {},
"b": {
"aaa": {
"$value": "red",
"$type": "color",
"$description": ""
},
"bbb": {
"$value": "blue",
"$type": "color",
"$description": ""
},
"ccc": {
"eee": {
"$value": "green",
"$type": "color",
"$description": ""
}
},
"fff": {
"ttt": {
"$value": {
"fontFamilies": [
"Aboreto"
],
"fontSizes": "12",
"fontWeights": "300"
},
"$type": "typography",
"$description": ""
}
}
},
"b/c": {},
"$themes": [
{
"id": "48af6582-f247-8060-8006-ff4dd1d761a8",
"name": "tes1",
"description": "",
"isSource": false,
"selectedTokenSets": {
"a": "enabled",
"b": "enabled"
}
}
],
"$metadata": {
"tokenSetOrder": [
"a",
"b",
"b/c"
],
"activeThemes": [
"/tes1"
],
"activeSets": [
"a",
"b"
]
}
}

View File

@@ -1,8 +1,15 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import * as fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import * as penpot from "#self"; import * as penpot from "#self";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
test("create empty context", () => { test("create empty context", () => {
const context = penpot.createBuildContext(); const context = penpot.createBuildContext();
assert.ok(context); assert.ok(context);
@@ -118,3 +125,48 @@ test("create context with color", () => {
assert.equal(color.opacity, params.opacity); assert.equal(color.opacity, params.opacity);
assert.equal(color.name, params.name); assert.equal(color.name, params.name);
}); });
test("create context with tokens lib as json", () => {
const context = penpot.createBuildContext();
const fileId = context.addFile({name: "file 1"});
const pageId = context.addPage({name: "page 1"});
const tokensFilePath = path.join(__dirname, "_tokens-1.json");
const tokens = fs.readFileSync(tokensFilePath, "utf8");
context.addTokensLib(tokens);
const internalState = context.getInternalState();
const file = internalState.files[fileId];
assert.ok(file, "file should exist");
assert.ok(file.data);
assert.ok(file.data.tokensLib)
});
test("create context with tokens lib as obj", () => {
const context = penpot.createBuildContext();
const fileId = context.addFile({name: "file 1"});
const pageId = context.addPage({name: "page 1"});
const tokensFilePath = path.join(__dirname, "_tokens-1.json");
const tokens = fs.readFileSync(tokensFilePath, "utf8");
context.addTokensLib(JSON.parse(tokens))
const internalState = context.getInternalState();
const file = internalState.files[fileId];
assert.ok(file, "file should exist");
assert.ok(file.data);
assert.ok(file.data.tokensLib)
});