mirror of
https://github.com/penpot/penpot.git
synced 2026-02-12 22:53:02 +00:00
✨ Typography import-export
This commit is contained in:
committed by
Andrés Moya
parent
8bd0edca46
commit
b3763dec3f
@@ -52,7 +52,11 @@
|
||||
:typography "typography"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
(set/map-invert token-type->dtcg-token-type))
|
||||
(-> (set/map-invert token-type->dtcg-token-type)
|
||||
;; Allow these properties to be imported with singular key names for backwards compability
|
||||
(assoc "fontWeight" :font-weight
|
||||
"fontSize" :font-size
|
||||
"fontFamily" :font-family)))
|
||||
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
@@ -1501,6 +1501,29 @@ Will return a value that matches this schema:
|
||||
(and (not (contains? decoded-json "$metadata"))
|
||||
(not (contains? decoded-json "$themes"))))
|
||||
|
||||
(defn- convert-dtcg-font-family
|
||||
"Convert font-family token value from DTCG format to internal format.
|
||||
- If value is a string, split it into a collection of font families
|
||||
- If value is already an array, keep it as is
|
||||
- Otherwise keep as is"
|
||||
[value]
|
||||
(cond
|
||||
(string? value) (cto/split-font-family value)
|
||||
(sequential? value) value
|
||||
:else value))
|
||||
|
||||
(defn- convert-dtcg-typography-composite
|
||||
"Convert typography token value keys from DTCG format to internal format."
|
||||
[value]
|
||||
(if (map? value)
|
||||
(-> value
|
||||
(set/rename-keys cto/dtcg-token-type->token-type)
|
||||
(select-keys cto/typography-keys)
|
||||
;; Convert font-family values within typography composite tokens
|
||||
(d/update-when :font-family convert-dtcg-font-family))
|
||||
;; Reference value
|
||||
value))
|
||||
|
||||
(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 '.'."
|
||||
@@ -1518,16 +1541,12 @@ Will return a value that matches this schema:
|
||||
(assoc tokens child-path (make-token
|
||||
:name child-path
|
||||
:type token-type
|
||||
:value (cond-> (get v "$value")
|
||||
;; Split string of font-families
|
||||
(and (= :font-family token-type)
|
||||
(string? (get v "$value")))
|
||||
cto/split-font-family
|
||||
|
||||
;; Keep array of font-families
|
||||
(and (= :font-family token-type)
|
||||
(sequential? (get v "$value")))
|
||||
identity)
|
||||
: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)
|
||||
token-value))
|
||||
:description (get v "$description")))
|
||||
;; Discard unknown type tokens
|
||||
tokens)))))
|
||||
@@ -1680,8 +1699,22 @@ Will return a value that matches this schema:
|
||||
:else
|
||||
(parse-multi-set-dtcg-json decoded-json))))
|
||||
|
||||
(defn- typography-token->dtcg-token
|
||||
[value]
|
||||
(if (map? value)
|
||||
(reduce-kv
|
||||
(fn [acc k v]
|
||||
(if (contains? cto/typography-keys k)
|
||||
(assoc acc (cto/token-type->dtcg-token-type k) v)
|
||||
acc))
|
||||
{} value)
|
||||
value))
|
||||
|
||||
(defn- token->dtcg-token [token]
|
||||
(cond-> {"$value" (:value token)
|
||||
(cond-> {"$value" (cond-> (:value token)
|
||||
;; Transform typography token values
|
||||
(= :typography (:type token))
|
||||
typography-token->dtcg-token)
|
||||
"$type" (cto/token-type->dtcg-token-type (:type token))}
|
||||
(:description token) (assoc "$description" (:description token))))
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"fonts": {
|
||||
"string-font-family": {
|
||||
"$value": "Arial, Helvetica, sans-serif",
|
||||
"$type": "fontFamilies",
|
||||
"$description": "A font family defined as a string"
|
||||
},
|
||||
"array-font-family": {
|
||||
"$value": ["Inter", "system-ui", "sans-serif"],
|
||||
"$type": "fontFamilies",
|
||||
"$description": "A font family defined as an array"
|
||||
},
|
||||
"single-font-family": {
|
||||
"$value": "Georgia",
|
||||
"$type": "fontFamilies"
|
||||
},
|
||||
"complex-font-family": {
|
||||
"$value": "Times New Roman, serif",
|
||||
"$type": "fontFamilies"
|
||||
},
|
||||
"font-with-spaces": {
|
||||
"$value": "Source Sans Pro, Arial, sans-serif",
|
||||
"$type": "fontFamilies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"test": {
|
||||
"typo": {
|
||||
"$value": {
|
||||
"fontWeight": "100",
|
||||
"fontSize": "16px",
|
||||
"letterSpacing": "0.1em"
|
||||
},
|
||||
"$type": "typography"
|
||||
},
|
||||
"typo2": {
|
||||
"$value": "{typo}",
|
||||
"$type": "typography"
|
||||
},
|
||||
"font-weight": {
|
||||
"$value": "200",
|
||||
"$type": "fontWeights"
|
||||
},
|
||||
"typo-to-single": {
|
||||
"$value": "{font-weight}",
|
||||
"$type": "typography"
|
||||
},
|
||||
"test-empty": {
|
||||
"$value": {},
|
||||
"$type": "typography"
|
||||
},
|
||||
"font-size": {
|
||||
"$value": "18px",
|
||||
"$type": "fontSizes"
|
||||
},
|
||||
"typo-complex": {
|
||||
"$value": {
|
||||
"fontWeight": "bold",
|
||||
"fontSize": "24px",
|
||||
"letterSpacing": "0.05em",
|
||||
"fontFamilies": ["Arial", "sans-serif"],
|
||||
"textCase": "uppercase"
|
||||
},
|
||||
"$type": "typography",
|
||||
"$description": "A complex typography token"
|
||||
},
|
||||
"typo-with-string-font-family": {
|
||||
"$value": {
|
||||
"fontWeight": "600",
|
||||
"fontSize": "20px",
|
||||
"fontFamilies": "Roboto, Helvetica, sans-serif"
|
||||
},
|
||||
"$type": "typography",
|
||||
"$description": "Typography token with string font family"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1580,3 +1580,254 @@
|
||||
"$type" "color"
|
||||
"$description" ""}}}}}]
|
||||
(t/is (= expected result)))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest parse-typography-tokens
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-typography-example.json")
|
||||
(json/decode {:key-fn identity}))
|
||||
lib (ctob/parse-decoded-json json "typography-test")
|
||||
set (ctob/get-set lib "typography-test")]
|
||||
|
||||
(t/testing "typography token with composite value"
|
||||
(let [token (ctob/get-token-by-name lib "typography-test" "test.typo")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :typography))
|
||||
(t/is (= (:value token) {:font-weight "100"
|
||||
:font-size "16px"
|
||||
:letter-spacing "0.1em"}))
|
||||
(t/is (= (:description token) ""))))
|
||||
|
||||
(t/testing "typography token with string reference"
|
||||
(let [token (ctob/get-token-by-name lib "typography-test" "test.typo2")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :typography))
|
||||
(t/is (= (:value token) "{typo}"))
|
||||
(t/is (= (:description token) ""))))
|
||||
|
||||
(t/testing "typography token referencing single token"
|
||||
(let [token (ctob/get-token-by-name lib "typography-test" "test.typo-to-single")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :typography))
|
||||
(t/is (= (:value token) "{font-weight}"))
|
||||
(t/is (= (:description token) ""))))
|
||||
|
||||
(t/testing "typography token with empty value"
|
||||
(let [token (ctob/get-token-by-name lib "typography-test" "test.test-empty")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :typography))
|
||||
(t/is (= (:value token) {}))
|
||||
(t/is (= (:description token) ""))))
|
||||
|
||||
(t/testing "typography token with complex value and description"
|
||||
(let [token (ctob/get-token-by-name lib "typography-test" "test.typo-complex")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :typography))
|
||||
(t/is (= (:value token) {:font-weight "bold"
|
||||
:font-size "24px"
|
||||
:letter-spacing "0.05em"
|
||||
:font-family ["Arial", "sans-serif"]
|
||||
:text-case "uppercase"}))
|
||||
(t/is (= (:description token) "A complex typography token"))))
|
||||
|
||||
(t/testing "individual font tokens still work"
|
||||
(let [font-weight-token (ctob/get-token-by-name lib "typography-test" "test.font-weight")
|
||||
font-size-token (ctob/get-token-by-name lib "typography-test" "test.font-size")]
|
||||
(t/is (some? font-weight-token))
|
||||
(t/is (= (:type font-weight-token) :font-weight))
|
||||
(t/is (= (:value font-weight-token) "200"))
|
||||
|
||||
(t/is (some? font-size-token))
|
||||
(t/is (= (:type font-size-token) :font-size))
|
||||
(t/is (= (:value font-size-token) "18px"))))
|
||||
|
||||
(t/testing "typography token with string font family gets transformed to array"
|
||||
(let [token (ctob/get-token-by-name lib "typography-test" "test.typo-with-string-font-family")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :typography))
|
||||
(t/is (= (:value token) {:font-weight "600"
|
||||
:font-size "20px"
|
||||
:font-family ["Roboto" "Helvetica" "sans-serif"]}))
|
||||
(t/is (= (:description token) "Typography token with string font family")))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest export-typography-tokens
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set
|
||||
:name "typography-set"
|
||||
:tokens {"typo.composite"
|
||||
(ctob/make-token
|
||||
{:name "typo.composite"
|
||||
:type :typography
|
||||
:value {:font-weight "bold"
|
||||
:font-size "16px"
|
||||
:letter-spacing "0.1em"}
|
||||
:description "A composite typography token"})
|
||||
"typo.reference"
|
||||
(ctob/make-token
|
||||
{:name "typo.reference"
|
||||
:type :typography
|
||||
:value "{other-token}"})
|
||||
"typo.empty"
|
||||
(ctob/make-token
|
||||
{:name "typo.empty"
|
||||
:type :typography
|
||||
:value {}})})))
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
typography-set (get result "typography-set")]
|
||||
|
||||
(t/testing "composite typography token export"
|
||||
(let [composite-token (get-in typography-set ["typo" "composite"])]
|
||||
(t/is (= (get composite-token "$type") "typography"))
|
||||
(t/is (= (get composite-token "$value") {"fontWeights" "bold"
|
||||
"fontSizes" "16px"
|
||||
"letterSpacing" "0.1em"}))
|
||||
(t/is (= (get composite-token "$description") "A composite typography token"))))
|
||||
|
||||
(t/testing "reference typography token export"
|
||||
(let [reference-token (get-in typography-set ["typo" "reference"])]
|
||||
(t/is (= (get reference-token "$type") "typography"))
|
||||
(t/is (= (get reference-token "$value") "{other-token}"))
|
||||
(t/is (= (get reference-token "$description") ""))))
|
||||
|
||||
(t/testing "empty typography token export"
|
||||
(let [empty-token (get-in typography-set ["typo" "empty"])]
|
||||
(t/is (= (get empty-token "$type") "typography"))
|
||||
(t/is (= (get empty-token "$value") {}))
|
||||
(t/is (= (get empty-token "$description") "")))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest typography-token-round-trip
|
||||
(let [original-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set
|
||||
:name "test-set"
|
||||
:tokens {"typo.test"
|
||||
(ctob/make-token
|
||||
{:name "typo.test"
|
||||
:type :typography
|
||||
:value {:font-weight "700"
|
||||
:font-size "20px"
|
||||
:letter-spacing "0.05em"
|
||||
:font-family ["Helvetica", "sans-serif"]}
|
||||
:description "Round trip test"})
|
||||
"typo.ref"
|
||||
(ctob/make-token
|
||||
{:name "typo.ref"
|
||||
:type :typography
|
||||
:value "{typo.test}"})})))
|
||||
;; Export to JSON format
|
||||
exported (ctob/export-dtcg-json original-lib)
|
||||
;; Import back
|
||||
imported-lib (ctob/parse-decoded-json exported "")]
|
||||
|
||||
(t/testing "round trip preserves typography tokens"
|
||||
(let [original-token (ctob/get-token-by-name original-lib "test-set" "typo.test")
|
||||
imported-token (ctob/get-token-by-name imported-lib "test-set" "typo.test")]
|
||||
(t/is (some? imported-token))
|
||||
(t/is (= (:type imported-token) (:type original-token)))
|
||||
(t/is (= (:value imported-token) (:value original-token)))
|
||||
(t/is (= (:description imported-token) (:description original-token))))
|
||||
|
||||
(let [original-ref (ctob/get-token-by-name original-lib "test-set" "typo.ref")
|
||||
imported-ref (ctob/get-token-by-name imported-lib "test-set" "typo.ref")]
|
||||
(t/is (some? imported-ref))
|
||||
(t/is (= (:type imported-ref) (:type original-ref)))
|
||||
(t/is (= (:value imported-ref) (:value original-ref))))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest parse-font-family-tokens
|
||||
(let [json (-> (slurp "test/common_tests/types/data/tokens-font-family-example.json")
|
||||
(json/decode {:key-fn identity}))
|
||||
lib (ctob/parse-decoded-json json "font-family-test")]
|
||||
|
||||
(t/testing "string font family token gets split into array"
|
||||
(let [token (ctob/get-token-by-name lib "font-family-test" "fonts.string-font-family")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :font-family))
|
||||
(t/is (= (:value token) ["Arial" "Helvetica" "sans-serif"]))
|
||||
(t/is (= (:description token) "A font family defined as a string"))))
|
||||
|
||||
(t/testing "array font family token stays as array"
|
||||
(let [token (ctob/get-token-by-name lib "font-family-test" "fonts.array-font-family")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :font-family))
|
||||
(t/is (= (:value token) ["Inter" "system-ui" "sans-serif"]))
|
||||
(t/is (= (:description token) "A font family defined as an array"))))
|
||||
|
||||
(t/testing "single font family string gets converted to array"
|
||||
(let [token (ctob/get-token-by-name lib "font-family-test" "fonts.single-font-family")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :font-family))
|
||||
(t/is (= (:value token) ["Georgia"]))
|
||||
(t/is (= (:description token) ""))))
|
||||
|
||||
(t/testing "complex font names with spaces handled correctly"
|
||||
(let [token (ctob/get-token-by-name lib "font-family-test" "fonts.font-with-spaces")]
|
||||
(t/is (some? token))
|
||||
(t/is (= (:type token) :font-family))
|
||||
(t/is (= (:value token) ["Source Sans Pro" "Arial" "sans-serif"])))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest export-font-family-tokens
|
||||
(let [tokens-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set
|
||||
:name "font-family-set"
|
||||
:tokens {"fonts.array-family"
|
||||
(ctob/make-token
|
||||
{:name "fonts.array-family"
|
||||
:type :font-family
|
||||
:value ["Roboto" "sans-serif"]
|
||||
:description "An array font family token"})
|
||||
"fonts.single-family"
|
||||
(ctob/make-token
|
||||
{:name "fonts.single-family"
|
||||
:type :font-family
|
||||
:value ["Georgia"]})})))
|
||||
result (ctob/export-dtcg-json tokens-lib)
|
||||
font-family-set (get result "font-family-set")]
|
||||
|
||||
(t/testing "array font family token export"
|
||||
(let [array-token (get-in font-family-set ["fonts" "array-family"])]
|
||||
(t/is (= (get array-token "$type") "fontFamilies"))
|
||||
(t/is (= (get array-token "$value") ["Roboto" "sans-serif"]))
|
||||
(t/is (= (get array-token "$description") "An array font family token"))))
|
||||
|
||||
(t/testing "single font family token export"
|
||||
(let [single-token (get-in font-family-set ["fonts" "single-family"])]
|
||||
(t/is (= (get single-token "$type") "fontFamilies"))
|
||||
(t/is (= (get single-token "$value") ["Georgia"]))
|
||||
(t/is (= (get single-token "$description") "")))))))
|
||||
|
||||
#?(:clj
|
||||
(t/deftest font-family-token-round-trip
|
||||
(let [original-lib (-> (ctob/make-tokens-lib)
|
||||
(ctob/add-set (ctob/make-token-set
|
||||
:name "test-set"
|
||||
:tokens {"fonts.test-array"
|
||||
(ctob/make-token
|
||||
{:name "fonts.test-array"
|
||||
:type :font-family
|
||||
:value ["Arial" "Helvetica" "sans-serif"]
|
||||
:description "Round trip test"})
|
||||
"fonts.test-single"
|
||||
(ctob/make-token
|
||||
{:name "fonts.test-single"
|
||||
:type :font-family
|
||||
:value ["Times New Roman"]})})))
|
||||
;; Export to JSON format
|
||||
exported (ctob/export-dtcg-json original-lib)
|
||||
;; Import back
|
||||
imported-lib (ctob/parse-decoded-json exported "")]
|
||||
|
||||
(t/testing "round trip preserves font family tokens"
|
||||
(let [original-token (ctob/get-token-by-name original-lib "test-set" "fonts.test-array")
|
||||
imported-token (ctob/get-token-by-name imported-lib "test-set" "fonts.test-array")]
|
||||
(t/is (some? imported-token))
|
||||
(t/is (= (:type imported-token) (:type original-token)))
|
||||
(t/is (= (:value imported-token) (:value original-token)))
|
||||
(t/is (= (:description imported-token) (:description original-token))))
|
||||
|
||||
(let [original-single (ctob/get-token-by-name original-lib "test-set" "fonts.test-single")
|
||||
imported-single (ctob/get-token-by-name imported-lib "test-set" "fonts.test-single")]
|
||||
(t/is (some? imported-single))
|
||||
(t/is (= (:type imported-single) (:type original-single)))
|
||||
(t/is (= (:value imported-single) (:value original-single))))))))
|
||||
|
||||
Reference in New Issue
Block a user