diff --git a/__tests__/utils/convertToDTCG.test.js b/__tests__/utils/convertToDTCG.test.js new file mode 100644 index 000000000..7a7be5ea3 --- /dev/null +++ b/__tests__/utils/convertToDTCG.test.js @@ -0,0 +1,350 @@ +/* + * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +import { expect } from 'chai'; +import { convertToDTCG } from '../../lib/utils/convertToDTCG.js'; + +describe('utils', () => { + describe('convertToDTCG', () => { + it('should swap value, type and description to use $ property prefix', () => { + const result = convertToDTCG( + { + colors: { + red: { + value: '#ff0000', + type: 'color', + description: 'A red color', + }, + green: { + value: '#00ff00', + type: 'color', + description: 'A green color', + }, + blue: { + value: '#0000ff', + type: 'color', + description: 'A blue color', + }, + }, + }, + { applyTypesToGroup: false }, + ); + expect(result).to.eql({ + colors: { + red: { + $value: '#ff0000', + $type: 'color', + $description: 'A red color', + }, + green: { + $value: '#00ff00', + $type: 'color', + $description: 'A green color', + }, + blue: { + $value: '#0000ff', + $type: 'color', + $description: 'A blue color', + }, + }, + }); + }); + + it('should apply type to the upper most common ancestor', () => { + const result = convertToDTCG({ + colors: { + red: { + value: '#ff0000', + type: 'color', + }, + green: { + value: '#00ff00', + type: 'color', + }, + blue: { + value: '#0000ff', + type: 'color', + }, + }, + dimensions: { + sm: { + value: '2px', + type: 'dimension', + }, + md: { + value: '8px', + type: 'dimension', + }, + lg: { + value: '16px', + type: 'dimension', + }, + }, + }); + expect(result).to.eql({ + colors: { + $type: 'color', + red: { + $value: '#ff0000', + }, + green: { + $value: '#00ff00', + }, + blue: { + $value: '#0000ff', + }, + }, + dimensions: { + $type: 'dimension', + sm: { + $value: '2px', + }, + md: { + $value: '8px', + }, + lg: { + $value: '16px', + }, + }, + }); + }); + + it('should keep types as is when not shared with all siblings', () => { + const result = convertToDTCG({ + colors: { + red: { + value: '#ff0000', + type: 'color', + }, + green: { + value: '#00ff00', + type: 'color', + }, + blue: { + value: '#0000ff', + type: 'different-type', + }, + }, + dimensions: { + sm: { + value: '2px', + type: 'dimension', + }, + md: { + value: '8px', + type: 'dimension', + }, + lg: { + value: '16px', + type: 'dimension', + }, + }, + }); + expect(result).to.eql({ + colors: { + red: { + $value: '#ff0000', + $type: 'color', + }, + green: { + $value: '#00ff00', + $type: 'color', + }, + blue: { + $value: '#0000ff', + $type: 'different-type', + }, + }, + dimensions: { + $type: 'dimension', + sm: { + $value: '2px', + }, + md: { + $value: '8px', + }, + lg: { + $value: '16px', + }, + }, + }); + }); + + it('should work with any number of nestings', () => { + const result = convertToDTCG({ + colors: { + red: { + value: '#ff0000', + type: 'color', + }, + grey: { + 100: { + value: '#aaaaaa', + type: 'color', + }, + 200: { + deeper: { + value: '#cccccc', + type: 'color', + }, + }, + 400: { + value: '#dddddd', + type: 'color', + }, + 500: { + foo: { + bar: { + qux: { + value: '#eeeeee', + type: 'color', + }, + }, + }, + }, + }, + green: { + value: '#00ff00', + type: 'color', + }, + blue: { + value: '#0000ff', + type: 'color', + }, + }, + }); + expect(result).to.eql({ + $type: 'color', + colors: { + red: { + $value: '#ff0000', + }, + grey: { + 100: { + $value: '#aaaaaa', + }, + 200: { + deeper: { + $value: '#cccccc', + }, + }, + 400: { + $value: '#dddddd', + }, + 500: { + foo: { + bar: { + qux: { + $value: '#eeeeee', + }, + }, + }, + }, + }, + green: { + $value: '#00ff00', + }, + blue: { + $value: '#0000ff', + }, + }, + }); + }); + + it('should handle scenarios where not all types are the same', () => { + const result = convertToDTCG({ + colors: { + red: { + value: '#ff0000', + type: 'color', + }, + grey: { + 100: { + value: '#aaaaaa', + type: 'color', + }, + 200: { + deeper: { + value: '#cccccc', + type: 'color', + }, + }, + 400: { + value: '#dddddd', + type: 'color', + }, + 500: { + foo: { + bar: { + qux: { + value: '#eeeeee', + type: 'different-type', + }, + }, + }, + }, + }, + green: { + value: '#00ff00', + type: 'color', + }, + blue: { + value: '#0000ff', + type: 'color', + }, + }, + }); + expect(result).to.eql({ + colors: { + red: { + $value: '#ff0000', + $type: 'color', + }, + grey: { + 100: { + $value: '#aaaaaa', + $type: 'color', + }, + 200: { + $type: 'color', + deeper: { + $value: '#cccccc', + }, + }, + 400: { + $value: '#dddddd', + $type: 'color', + }, + 500: { + $type: 'different-type', + foo: { + bar: { + qux: { + $value: '#eeeeee', + }, + }, + }, + }, + }, + green: { + $value: '#00ff00', + $type: 'color', + }, + blue: { + $value: '#0000ff', + $type: 'color', + }, + }, + }); + }); + }); +}); diff --git a/lib/utils/convertToDTCG.js b/lib/utils/convertToDTCG.js new file mode 100644 index 000000000..aac5cf657 --- /dev/null +++ b/lib/utils/convertToDTCG.js @@ -0,0 +1,55 @@ +import isPlainObject from 'is-plain-obj'; + +/** + * @typedef {import('../../types/DesignToken.d.ts').DesignToken} DesignToken + * @typedef {import('../../types/DesignToken.d.ts').DesignTokens} DesignTokens + */ + +/** + * @param {DesignTokens} slice + * @param {DesignTokens} full + * @param {{applyTypesToGroup?: boolean}} [opts] + */ +function recurse(slice, full, opts) { + /** @type {Set} */ + let types = new Set(); + if (Object.hasOwn(slice, 'value')) { + const token = /** @type {DesignToken} */ (slice); + token.$value = token.value; + delete token.value; + if (token.type) { + token.$type = token.type; + delete token.type; + types.add(token.$type); + } + if (token.description) { + token.$description = token.description; + delete token.description; + } + return types; + } else { + Object.keys(slice).forEach((key) => { + if (isPlainObject(slice[key])) { + types = new Set([...types, ...recurse(slice[key], full, opts)]); + } + }); + if (types.size === 1 && opts?.applyTypesToGroup !== false) { + slice.$type = [...types][0]; + Object.keys(slice).forEach((key) => { + delete slice[key].$type; + }); + } + } + return types; +} + +/** + * @param {DesignTokens} dictionary + * @param {{applyTypesToGroup?: boolean}} [opts] + */ +export function convertToDTCG(dictionary, opts) { + const copy = structuredClone(dictionary); + const full = structuredClone(dictionary); + recurse(copy, full, opts); + return copy; +} diff --git a/types/DesignToken.d.ts b/types/DesignToken.d.ts index 4a5b8c323..91d524e24 100644 --- a/types/DesignToken.d.ts +++ b/types/DesignToken.d.ts @@ -29,6 +29,7 @@ export interface DesignToken { } export interface DesignTokens { + $type?: string; [key: string]: DesignTokens | DesignToken; }