diff --git a/dequal/alts.ts b/dequal/alts.ts new file mode 100644 index 0000000..8c6c054 --- /dev/null +++ b/dequal/alts.ts @@ -0,0 +1,78 @@ +// 227B – 128k op/s +export function v227(foo, bar) { + var keys, ctor; + return foo === bar || ( + foo && bar && (ctor=foo.constructor) === bar.constructor ? + ctor === RegExp ? foo.toString() == bar.toString() + : ctor === Date ? foo.getTime() == bar.getTime() + : ctor === Array ? + foo.length === bar.length && foo.every(function (val, idx) { + return v227(val, bar[idx]); + }) + : ctor === Object + && (keys=Object.keys(foo)).length === Object.keys(bar).length + && keys.every(function (k) { + return k in bar && v227(foo[k], bar[k]); + }) + : (foo !== foo && bar !== bar) + ); +} + +// 255B – 155k op/s +export function v255(foo, bar) { + var ctor, len, k; + if (foo === bar) return true; + if (foo && bar && (ctor=foo.constructor) === bar.constructor) { + if (ctor === Date) return foo.getTime() === bar.getTime(); + if (ctor === RegExp) return foo.toString() === bar.toString(); + if (ctor === Array && (len=foo.length) === bar.length) { + while (len-- > 0 && v255(foo[len], bar[len])); + return len === -1; + } + if (ctor === Object) { + if (Object.keys(foo).length !== Object.keys(bar).length) return false; + for (k in foo) { + if (!(k in bar) || !v255(foo[k], bar[k])) return false; + } + return true; + } + } + return foo !== foo && bar !== bar; +} + +// 246B – 157k op/s +export function v246(foo, bar) { + var ctor, i; + if (foo === bar) return true; + if (foo && bar && (ctor=foo.constructor) === bar.constructor) { + if (ctor === Date) return foo.getTime() === bar.getTime(); + if (ctor === RegExp) return foo.toString() === bar.toString(); + if (ctor === Array) { + if (foo.length !== bar.length) return false; + for (i=0; i < foo.length; i++) if (!v246(foo[i], bar[i])) return false; + return true + } + if (ctor === Object) { + if (Object.keys(foo).length !== Object.keys(bar).length) return false; + for (i in foo) if (!(i in bar) || !v246(foo[i], bar[i])) return false; + return true; + } + } + return foo !== foo && bar !== bar; +} + +// 225B - 97k op/s +export function v225(foo, bar) { + var ctor, keys, len; + if (foo === bar) return true; + if (foo && bar && (ctor=foo.constructor) === bar.constructor) { + if (ctor === Date) return foo.getTime() === bar.getTime(); + if (ctor === RegExp) return foo.toString() === bar.toString(); + if (typeof foo === 'object') { + if (Object.keys(foo).length !== Object.keys(bar).length) return false; + for (len in foo) if (!(len in bar) || !v225(foo[len], bar[len])) return false; + return true; + } + } + return foo !== foo && bar !== bar; +} diff --git a/dequal/index.ts b/dequal/index.ts new file mode 100644 index 0000000..d0b1e2d --- /dev/null +++ b/dequal/index.ts @@ -0,0 +1,84 @@ +var has = Object.prototype.hasOwnProperty; + +function find(iter, tar, key) { + for (key of iter.keys()) { + if (dequal(key, tar)) return key; + } +} + +export function dequal(foo, bar) { + var ctor, len, tmp; + if (foo === bar) return true; + + if (foo && bar && (ctor=foo.constructor) === bar.constructor) { + if (ctor === Date) return foo.getTime() === bar.getTime(); + if (ctor === RegExp) return foo.toString() === bar.toString(); + + if (ctor === Array) { + if ((len=foo.length) === bar.length) { + while (len-- && dequal(foo[len], bar[len])); + } + return len === -1; + } + + if (ctor === Set) { + if (foo.size !== bar.size) { + return false; + } + for (len of foo) { + tmp = len; + if (tmp && typeof tmp === 'object') { + tmp = find(bar, tmp); + if (!tmp) return false; + } + if (!bar.has(tmp)) return false; + } + return true; + } + + if (ctor === Map) { + if (foo.size !== bar.size) { + return false; + } + for (len of foo) { + tmp = len[0]; + if (tmp && typeof tmp === 'object') { + tmp = find(bar, tmp); + if (!tmp) return false; + } + if (!dequal(len[1], bar.get(tmp))) { + return false; + } + } + return true; + } + + if (ctor === ArrayBuffer) { + foo = new Uint8Array(foo); + bar = new Uint8Array(bar); + } else if (ctor === DataView) { + if ((len=foo.byteLength) === bar.byteLength) { + while (len-- && foo.getInt8(len) === bar.getInt8(len)); + } + return len === -1; + } + + if (ArrayBuffer.isView(foo)) { + if ((len=foo.byteLength) === bar.byteLength) { + while (len-- && foo[len] === bar[len]); + } + return len === -1; + } + + if (!ctor || typeof foo === 'object') { + len = 0; + for (ctor in foo) { + if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false; + if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false; + } + return Object.keys(bar).length === len; + } + } + + return foo !== foo && bar !== bar; +} diff --git a/dequal/lite.ts b/dequal/lite.ts new file mode 100644 index 0000000..5820d67 --- /dev/null +++ b/dequal/lite.ts @@ -0,0 +1,29 @@ +var has = Object.prototype.hasOwnProperty; + +export function dequal(foo, bar) { + var ctor, len; + if (foo === bar) return true; + + if (foo && bar && (ctor=foo.constructor) === bar.constructor) { + if (ctor === Date) return foo.getTime() === bar.getTime(); + if (ctor === RegExp) return foo.toString() === bar.toString(); + + if (ctor === Array) { + if ((len=foo.length) === bar.length) { + while (len-- && dequal(foo[len], bar[len])); + } + return len === -1; + } + + if (!ctor || typeof foo === 'object') { + len = 0; + for (ctor in foo) { + if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false; + if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false; + } + return Object.keys(bar).length === len; + } + } + + return foo !== foo && bar !== bar; +} diff --git a/design-tokens-cli/chooseTransform.ts b/design-tokens-cli/chooseTransform.ts new file mode 100644 index 0000000..221084a --- /dev/null +++ b/design-tokens-cli/chooseTransform.ts @@ -0,0 +1,27 @@ +import { toCustomProps } from './transformers/toCustomProps.js'; +import { toScssVars } from './transformers/toScssVars.js'; +import { toESM } from './transformers/toESM.js'; +import { toJSON } from './transformers/toJSON.js'; + +/** + * Convert an object of design token name/value pairs into Scss (Sass) variables + * @param {Object} pairs The flattened token key/value pairs + * @param {String} as What the tokens should be transformed into + * @returns {String} + */ + const chooseTransform = (pairs, as, groupName, config) => { + switch (as) { + case 'css': + return toCustomProps(pairs, config); + case 'scss': + return toScssVars(pairs, config); + case 'mjs' || 'js': + return toESM(pairs, groupName, config); + case 'json': + return toJSON(pairs, config); + default: + throw new Error(`The 'as' value ${as} is not recognized.`); + } +} + +export { chooseTransform } \ No newline at end of file diff --git a/design-tokens-cli/example/tokens-layout/inset.tokens.json b/design-tokens-cli/example/tokens-layout/inset.tokens.json new file mode 100644 index 0000000..f0159c7 --- /dev/null +++ b/design-tokens-cli/example/tokens-layout/inset.tokens.json @@ -0,0 +1,20 @@ +{ + "inset": { + "a": { + "$value": { + "left": "0", + "top": "0", + "bottom": "0", + "right": "0" + } + }, + "b": { + "$value": { + "left": "50%", + "top": "50%", + "bottom": "50%", + "right": "50%" + } + } + } +} \ No newline at end of file diff --git a/design-tokens-cli/example/tokens-layout/size.tokens.json b/design-tokens-cli/example/tokens-layout/size.tokens.json new file mode 100644 index 0000000..9ab9fb6 --- /dev/null +++ b/design-tokens-cli/example/tokens-layout/size.tokens.json @@ -0,0 +1,88 @@ +{ + "sizing": { + "relative": { + "0": { + "$value": "0", + "comment": "no spacing, zero." + }, + "25": { + "$value": ".0625", + "comment": ".0625rem, 1px" + }, + "50": { + "$value": ".125rem", + "comment": ".125rem, 2px" + }, + "100": { + "$value": ".25rem", + "comment": ".25rem, 4px." + }, + "200": { + "$value": ".5rem", + "comment": ".5rem, 8px." + }, + "400": { + "$value": "1rem", + "comment": "1rem, 16px." + }, + "600": { + "$value": "1.5rem", + "comment": "1.5rem, 24px." + }, + "800": { + "$value": "2rem", + "comment": "2rem, 32px." + }, + "1200": { + "$value": "3rem", + "comment": "3rem, 48px." + }, + "1600": { + "$value": "4rem", + "comment": "4rem, 64px." + } + }, + "absolute": { + "0": { + "$value": "0", + "comment": "no spacing, zero." + }, + "25": { + "$value": "1px", + "comment": ".0625rem, 1px" + }, + "50": { + "$value": "2px", + "comment": ".125rem, 2px" + }, + "100": { + "$value": "4px", + "comment": ".25rem, 4px." + }, + "200": { + "$value": "8px", + "comment": ".5rem, 8px." + }, + "400": { + "$value": "16px", + "comment": "1rem, 16px." + }, + "600": { + "$value": "24px", + "comment": "1.5rem, 24px." + }, + "800": { + "$value": "32px", + "comment": "2rem, 32px." + }, + "1200": { + "$value": "48px", + "comment": "3rem, 48px." + }, + "1600": { + "$value": "64px", + "comment": "4rem, 64px." + } + } + } +} \ No newline at end of file diff --git a/design-tokens-cli/example/tokens-type/color.tokens b/design-tokens-cli/example/tokens-type/color.tokens new file mode 100644 index 0000000..95b697c --- /dev/null +++ b/design-tokens-cli/example/tokens-type/color.tokens @@ -0,0 +1,50 @@ +{ + "color": { + "white": { + "name": "white", + "$value": "#ffffff" + }, + "blanche": { + "name": "white", + "$value": "{color.white}" + }, + "weiss": { + "name": "white", + "$value": "{color.blanche}" + }, + "black": { + "name": "black", + "$value": "#000000" + }, + "grayscale": { + "200": { + "name": "grayscale-200", + "$value": "#f8f8f8" + }, + "300": { + "name": "grayscale-300", + "$value": "#f3f3f3" + }, + "400": { + "name": "grayscale-400", + "$value": "#dadada" + }, + "500": { + "name": "grayscale-500", + "$value": "#999999" + }, + "600": { + "name": "grayscale-600", + "$value": "#666666" + }, + "700": { + "name": "grayscale-700", + "$value": "#555555" + }, + "800": { + "name": "grayscale-800", + "$value": "#222222" + } + } + } +} \ No newline at end of file diff --git a/design-tokens-cli/example/tokens-type/font-size.tokens.json b/design-tokens-cli/example/tokens-type/font-size.tokens.json new file mode 100644 index 0000000..ebdf973 --- /dev/null +++ b/design-tokens-cli/example/tokens-type/font-size.tokens.json @@ -0,0 +1,40 @@ +{ + "font-size": { + "350": { + "$value": ".875rem", + "comment": "14px" + }, + "400": { + "$value": "1rem", + "comment": "16px" + }, + "450": { + "$value": "1.125rem", + "comment": "18px" + }, + "500": { + "$value": "1.25rem", + "comment": "20px" + }, + "600": { + "$value": "1.5rem", + "comment": "24px" + }, + "650": { + "$value": "1.875rem", + "comment": "28px" + }, + "800": { + "$value": "2rem", + "comment": "32px" + }, + "875": { + "$value": "2.1875rem", + "comment": "35px" + }, + "900": { + "$value": "2.5rem", + "comment": "36px" + } + } +} \ No newline at end of file diff --git a/design-tokens-cli/example/tokens.config.json b/design-tokens-cli/example/tokens.config.json new file mode 100644 index 0000000..4833c2d --- /dev/null +++ b/design-tokens-cli/example/tokens.config.json @@ -0,0 +1,48 @@ +{ + "globalPrefix": "token", + "transforms": [ + { + "name": "layout", + "from": "example/tokens-layout", + "to": [ + { + "as": "scss", + "to": "example/output/scss" + }, + { + "as": "css", + "to": "example/output/css" + }, + { + "as": "mjs", + "to": "example/output/js" + }, + { + "as": "json", + "to": "example/output/json" + } + ] + }, + { + "from": "example/tokens-type", + "to": [ + { + "as": "scss", + "to": "example/output/scss" + }, + { + "as": "css", + "to": "example/output/css" + }, + { + "as": "mjs", + "to": "example/output/js" + }, + { + "as": "json", + "to": "example/output/json" + } + ] + } + ] +} \ No newline at end of file diff --git a/design-tokens-cli/filterByGroup.ts b/design-tokens-cli/filterByGroup.ts new file mode 100644 index 0000000..70c13f7 --- /dev/null +++ b/design-tokens-cli/filterByGroup.ts @@ -0,0 +1,17 @@ +/** + * Filter an object of token key/values by the group name + * @param {Object} tokens The starting object (one level of key/value pairs only) + * @returns {Object} + */ +const filterByGroup = (tokens, groupName) => { + let filtered = Object.keys(tokens) + .filter(key => key.startsWith(groupName)) + .reduce((obj, key) => { + return Object.assign(obj, { + [key]: tokens[key] + }); + }, {}); + return filtered; +} + +export { filterByGroup } \ No newline at end of file diff --git a/design-tokens-cli/findDuplicates.ts b/design-tokens-cli/findDuplicates.ts new file mode 100644 index 0000000..28771b0 --- /dev/null +++ b/design-tokens-cli/findDuplicates.ts @@ -0,0 +1,10 @@ +/** + * Finds duplicates in an array + * @param {Array} arr The starting array + * @returns {Array} + */ +const findDuplicates = arr => { + return arr.filter((item, index) => arr.indexOf(item) !== index); +} + +export { findDuplicates } \ No newline at end of file diff --git a/design-tokens-cli/findTrueValues.ts b/design-tokens-cli/findTrueValues.ts new file mode 100644 index 0000000..13bca97 --- /dev/null +++ b/design-tokens-cli/findTrueValues.ts @@ -0,0 +1,28 @@ +import { refToName } from "./refToName.js"; + +/** + * Searches through chained references to replace reference with originating value + * @param {Object} pairs The flattened token key/value pairs + * @returns {Object} + */ +const findTrueValues = groups => { + const newGroups = JSON.parse(JSON.stringify(groups)); + let justPairs = {}; + Object.keys(newGroups).forEach(group => { + Object.assign(justPairs, newGroups[group]); + }); + for (const pair in justPairs) { + let val = justPairs[pair]; + while (val.startsWith('{')) { + let name = refToName(justPairs[pair]); + if (!justPairs[name]) { + throw new Error(`The token reference name '${name}' does not exist.`); + } + val = justPairs[name]; + } + justPairs[pair] = val; + } + return justPairs; +} + +export { findTrueValues } diff --git a/design-tokens-cli/flattenJSON.ts b/design-tokens-cli/flattenJSON.ts new file mode 100644 index 0000000..5d52788 --- /dev/null +++ b/design-tokens-cli/flattenJSON.ts @@ -0,0 +1,54 @@ +import { sortKeys } from './sortKeys.js'; + +/** + * Create a simple object of design token name/value pairs from sped-adhering design tokens JSON + * @param {Array} tokens A standard design tokens object (JSON)) + * @returns {Object} of token names and values + */ +const flattenJSON = tokens => { + const existingObjects = []; + const path = []; + const tokensArrays = []; + (function find(tokens) { + for (const key of Object.keys(tokens)) { + if (key === '$value') { + if (typeof tokens[key] === 'string') { + path.push(tokens[key]); + tokensArrays.push([...path]); + path.pop(); + } else if (typeof tokens[key] === 'object') { + let $values = tokens[key]; + for (const key in $values) { + let pathCopy = [...path]; + pathCopy.push(key); + pathCopy.push($values[key]); + tokensArrays.push([...pathCopy]); + } + } else { + throw new Error(`$value properties must be strings or objects.`); + } + } + const o = tokens[key]; + if (o && typeof o === "object" && !Array.isArray(o)) { + if (!existingObjects.find(tokens => tokens === o)) { + path.push(key); + existingObjects.push(o); + find(o); + path.pop(); + } + } + } + }(tokens)); + const newObject = {}; + tokensArrays.forEach(arr => { + const keys = arr.slice(0, -1).map(k => { + return k.split(' ').join('-'); + }); + const key = keys.join('-'); + const value = arr.at(-1); + newObject[key] = value; + }); + return sortKeys(newObject); +} + +export { flattenJSON } \ No newline at end of file diff --git a/design-tokens-cli/index.ts b/design-tokens-cli/index.ts new file mode 100755 index 0000000..365ee6d --- /dev/null +++ b/design-tokens-cli/index.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +import { program as cli } from 'commander'; +import { transform } from './utils/transform.js'; + +cli.description('Process spec-conforming design tokens JSON'); +cli.name('designTokens'); +cli.usage(""); +cli.addHelpCommand(false); +cli.helpOption(false); + +cli + .command('transform') + .argument('[configPath]', 'The config file path') + .action(transform); + +cli.parse(process.argv); \ No newline at end of file diff --git a/design-tokens-cli/refToName.ts b/design-tokens-cli/refToName.ts new file mode 100644 index 0000000..d80db4c --- /dev/null +++ b/design-tokens-cli/refToName.ts @@ -0,0 +1,12 @@ +/** + * Convert design token references to design token names + * @param {String} refString The design tokens reference $value + * @returns {String} A design token name (kebab case) + */ +const refToName = refString => { + const cropped = refString.slice(1,-1).trim(); + const kebabbed = cropped.split('.').join('-').split(' ').join('-'); + return kebabbed; +} + +export { refToName } \ No newline at end of file diff --git a/design-tokens-cli/sortKeys.ts b/design-tokens-cli/sortKeys.ts new file mode 100644 index 0000000..8509446 --- /dev/null +++ b/design-tokens-cli/sortKeys.ts @@ -0,0 +1,14 @@ +/** + * Sort a shallow object alphabetically by key + * @param {Object} object A shallow object to be sorted alphabetically, by key + * @returns {Object} + */ +const sortKeys = object => { + return Object.keys(object) + .sort() + .reduce((acc, key) => ({ + ...acc, [key]: object[key] + }), {}); +} + +export { sortKeys } \ No newline at end of file diff --git a/design-tokens-cli/tests/filterByGroup.test.ts b/design-tokens-cli/tests/filterByGroup.test.ts new file mode 100644 index 0000000..aea9ac2 --- /dev/null +++ b/design-tokens-cli/tests/filterByGroup.test.ts @@ -0,0 +1,14 @@ +'use strict'; +import { filterByGroup } from '../utils/filterByGroup.js'; + +test('Filters tokens key/value pairs by group name', () => { + const tokens = { + "color-token-1": "#000", + "color-token-2": "#fff", + "size-token-1": "1em", + "size-token-2": "2em" + } + + const filtered = filterByGroup(tokens, 'color'); + expect(Object.keys(filtered).length).toEqual(2); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/findTrueValues.test.ts b/design-tokens-cli/tests/findTrueValues.test.ts new file mode 100644 index 0000000..af405d8 --- /dev/null +++ b/design-tokens-cli/tests/findTrueValues.test.ts @@ -0,0 +1,15 @@ +'use strict'; +import { findTrueValues } from '../utils/findTrueValues.js'; + +test('Searches through references to find true value and apply it', () => { + const tokens = { + 'color': { + 'color-white': '#ffffff', + 'color-blanche': '{color.white}', + 'color-weiss': '{color.blanche}' + } + } + + const resolved = findTrueValues(tokens); + expect(resolved['color-weiss']).toBe('#ffffff'); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/refToName.test.ts b/design-tokens-cli/tests/refToName.test.ts new file mode 100644 index 0000000..fa76071 --- /dev/null +++ b/design-tokens-cli/tests/refToName.test.ts @@ -0,0 +1,9 @@ +'use strict'; +import { refToName } from "../utils/refToName.js"; + +test('Converts a reference string to a real token name.', () => { + const ref = '{ color.gray scale.0 }'; + + const converted = refToName(ref); + expect(converted).toBe('color-gray-scale-0'); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/sortKeys.test.ts b/design-tokens-cli/tests/sortKeys.test.ts new file mode 100644 index 0000000..9a7f4bc --- /dev/null +++ b/design-tokens-cli/tests/sortKeys.test.ts @@ -0,0 +1,14 @@ +'use strict'; +import { sortKeys } from "../utils/sortKeys.js"; + +test('Sorts an object\'s keys alphabetically', () => { + const object = { + b: true, + c: true, + z: true, + a: true + } + + const sorted = sortKeys(object); + expect(Object.keys(sorted)[0]).toBe('a'); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/toCustomProps.test.ts b/design-tokens-cli/tests/toCustomProps.test.ts new file mode 100644 index 0000000..0c8a27a --- /dev/null +++ b/design-tokens-cli/tests/toCustomProps.test.ts @@ -0,0 +1,19 @@ +'use strict'; +import { toCustomProps } from "../utils/transformers/toCustomProps.js"; + +test('Converts tokens to CSS custom properties', () => { + const tokens = { + 'token-color-1': '#000', + 'token-color-2': '#fff' + } + + const expectedString = ` + :root { + --token-color-1: #000; + --token-color-2: #fff; + } + `; + + const returnedString = toCustomProps(tokens); + expect(expectedString.replace(/\s/g, '')).toEqual(returnedString.replace(/\s/g, '')); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/toESM.test.ts b/design-tokens-cli/tests/toESM.test.ts new file mode 100644 index 0000000..7318e7e --- /dev/null +++ b/design-tokens-cli/tests/toESM.test.ts @@ -0,0 +1,21 @@ +'use strict'; +import { toESM } from "../utils/transformers/toESM.js"; + +test('Converts tokens to ES modules', () => { + const tokens = { + 'token-color-1': '#000', + 'token-color-2': '#fff' + } + + const groupName = 'my-colors'; + + const expectedString = ` + export const myColors = { + 'token-color-1': '#000', + 'token-color-2': '#fff' + } + `; + + const returnedString = toESM(tokens, groupName); + expect(expectedString.replace(/\s/g, '')).toEqual(returnedString.replace(/\s/g, '')); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/toJSON.test.ts b/design-tokens-cli/tests/toJSON.test.ts new file mode 100644 index 0000000..0d1bc5c --- /dev/null +++ b/design-tokens-cli/tests/toJSON.test.ts @@ -0,0 +1,19 @@ +'use strict'; +import { toJSON } from "../utils/transformers/toJSON.js"; + +test('Converts tokens to (flat) JSON', () => { + const tokens = { + "token-color-1": "#000", + "token-color-2": "#fff" + } + + const expectedString = ` + { + "token-color-1": "#000", + "token-color-2": "#fff" + } + `; + + const returnedString = toJSON(tokens); + expect(expectedString.replace(/\s/g, '')).toEqual(returnedString.replace(/\s/g, '')); +}); \ No newline at end of file diff --git a/design-tokens-cli/tests/toScssVars.test.ts b/design-tokens-cli/tests/toScssVars.test.ts new file mode 100644 index 0000000..6a18f03 --- /dev/null +++ b/design-tokens-cli/tests/toScssVars.test.ts @@ -0,0 +1,17 @@ +'use strict'; +import { toScssVars } from "../utils/transformers/toScssVars.js"; + +test('Converts tokens to CSS Sass (scss) variables', () => { + const tokens = { + 'token-color-1': '#000', + 'token-color-2': '#fff' + } + + const expectedString = ` + $token-color-1: #000; + $token-color-2: #fff; + `; + + const returnedString = toScssVars(tokens); + expect(expectedString.replace(/\s/g, '')).toEqual(returnedString.replace(/\s/g, '')); +}); \ No newline at end of file diff --git a/design-tokens-cli/transform.ts b/design-tokens-cli/transform.ts new file mode 100644 index 0000000..f4f1a08 --- /dev/null +++ b/design-tokens-cli/transform.ts @@ -0,0 +1,67 @@ +import jetpack from 'fs-jetpack'; +import { findDuplicates } from "./findDuplicates.js"; +import { findTrueValues } from "./findTrueValues.js"; +import { flattenJSON } from './flattenJSON.js'; +import { chooseTransform } from './chooseTransform.js'; + +const transform = (configPath, options) => { + // If no config path argument, look for config file + if (!configPath) { + configPath = jetpack.find('./', { matching: 'tokens.config.json' })[0]; + } + if (!configPath) { + throw new Error('No config file found in current working directory.'); + } + + // Read the config file as JSON + const config = jetpack.read(configPath, 'json'); + + config.transforms.forEach(transform => { + let from = jetpack.cwd(transform.from); + // Keep track of tokens in one object + let allTokens = {}; + from.find({ matching: ['*.tokens.json', '*.tokens'] }).forEach(path => { + const json = from.read(path, 'json'); + let pairs = flattenJSON(json); + allTokens[path.split('.')[0]] = pairs; + }); + + // Resolve token references + const resolvedPairs = findTrueValues(allTokens); + // Exit if there are duplicate token names + const duplicates = findDuplicates(Object.keys(resolvedPairs)); + if (duplicates.length) { + throw new Error(`You have duplicate token names: ${duplicates.join(', ')}`); + } + + // Place true values back into categorized object + for (let group in allTokens) { + Object.keys(allTokens[group]).forEach(token => { + allTokens[group][token] = resolvedPairs[token]; + }); + } + + // If the transform has a name, concatenate under name + if (transform.name) { + transform.to.forEach(format => { + let code = chooseTransform(resolvedPairs, format.as, transform.name, config); + let formatTo = jetpack.cwd(format.to); + let newPath = `${transform.name}.tokens.${format.as}`; + formatTo.write(newPath, code); + }); + return; + // Otherwise, create separate files after file names + } else { + for (let group in allTokens) { + transform.to.forEach(format => { + let code = chooseTransform(allTokens[group], format.as, group, config); + let formatTo = jetpack.cwd(format.to); + let newPath = `${group}.tokens.${format.as}`; + formatTo.write(newPath, code); + }); + } + } + }); +} + +export { transform } \ No newline at end of file diff --git a/design-tokens-cli/transformers/toCustomProps.ts b/design-tokens-cli/transformers/toCustomProps.ts new file mode 100644 index 0000000..b91eafe --- /dev/null +++ b/design-tokens-cli/transformers/toCustomProps.ts @@ -0,0 +1,18 @@ +/** + * Convert an object of design token name/value pairs into CSS custom Properties + * @param {Object} tokensObject + * @returns {String} + */ +const toCustomProps = (tokensObject, config, includeRoot = true) => { + const prefix = config.globalPrefix ? `${config.globalPrefix}-` : ''; + let string = ''; + if (includeRoot) string += ':root {\n'; + Object.keys(tokensObject).forEach(key => { + if (includeRoot) string += ' '; + string += `\t--${prefix}${key}: ${tokensObject[key]};\n`; + }); + if (includeRoot) string += '}\n'; + return string; +} + +export { toCustomProps } \ No newline at end of file diff --git a/design-tokens-cli/transformers/toESM.ts b/design-tokens-cli/transformers/toESM.ts new file mode 100644 index 0000000..215c7c2 --- /dev/null +++ b/design-tokens-cli/transformers/toESM.ts @@ -0,0 +1,20 @@ +/** + * Convert an object of design token name/value pairs into an ES module + * @param {Object} tokensObject + * @returns {String} + */ +const toESM = (tokensObject, groupName, config) => { + const prefix = config.globalPrefix ? `${config.globalPrefix}-` : ''; + groupName = groupName.replace(/-./g, x=>x[1].toUpperCase()); + const keys = Object.keys(tokensObject); + let string = ''; + string += `export const ${groupName} = {\n`; + keys.forEach(key => { + let comma = (keys.indexOf(key) + 1) === keys.length ? '' : ','; + string += `\t'${prefix}${key}': '${tokensObject[key]}'${comma}\n`; + }); + string += `}`; + return string; +} + +export { toESM } \ No newline at end of file diff --git a/design-tokens-cli/transformers/toJSON.ts b/design-tokens-cli/transformers/toJSON.ts new file mode 100644 index 0000000..f66909c --- /dev/null +++ b/design-tokens-cli/transformers/toJSON.ts @@ -0,0 +1,15 @@ +/** + * Convert an object of design token name/value pairs into JSON (pass through, basically) + * @param {Object} tokensObject + * @returns {String} + */ + const toJSON = (tokensObject, config) => { + if (config.globalPrefix) { + tokensObject = Object.fromEntries( + Object.entries(tokensObject).map(([k, v]) => [`${config.globalPrefix}-${k}`, v]) + ) + } + return JSON.stringify(tokensObject, undefined, '\t'); +} + +export { toJSON } \ No newline at end of file diff --git a/design-tokens-cli/transformers/toScssVars.ts b/design-tokens-cli/transformers/toScssVars.ts new file mode 100644 index 0000000..4d3e7ea --- /dev/null +++ b/design-tokens-cli/transformers/toScssVars.ts @@ -0,0 +1,15 @@ +/** + * Convert an object of design token name/value pairs into Scss (Sass) variables + * @param {Object} tokensObject + * @returns {String} + */ +const toScssVars = (tokensObject, config) => { + const prefix = config.globalPrefix ? `${config.globalPrefix}-` : ''; + let string = ''; + Object.keys(tokensObject).forEach(key => { + string += `$${prefix}${key}: ${tokensObject[key]};\n`; + }); + return string; +} + +export { toScssVars } \ No newline at end of file diff --git a/dom-helpers/activeElement.ts b/dom-helpers/activeElement.ts new file mode 100644 index 0000000..6a90b1a --- /dev/null +++ b/dom-helpers/activeElement.ts @@ -0,0 +1,21 @@ +import ownerDocument from './ownerDocument' + +/** + * Returns the actively focused element safely. + * + * @param doc the document to check + */ +export default function activeElement(doc = ownerDocument()) { + // Support: IE 9 only + // IE9 throws an "Unspecified error" accessing document.activeElement from an