diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index f23e9c5a70..c8b6b6f9f5 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -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))) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 7e098e5cab..fa9d634082 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -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)))) diff --git a/common/test/common_tests/types/data/tokens-font-family-example.json b/common/test/common_tests/types/data/tokens-font-family-example.json new file mode 100644 index 0000000000..b4d23dec2c --- /dev/null +++ b/common/test/common_tests/types/data/tokens-font-family-example.json @@ -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" + } + } +} \ No newline at end of file diff --git a/common/test/common_tests/types/data/tokens-typography-example.json b/common/test/common_tests/types/data/tokens-typography-example.json new file mode 100644 index 0000000000..614c7cf1e4 --- /dev/null +++ b/common/test/common_tests/types/data/tokens-typography-example.json @@ -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" + } + } +} \ No newline at end of file diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index cd7fab3a65..d155ba1089 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -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))))))))