From 31195ace70d480303f5c80024e12ee791b551569 Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Mon, 25 Mar 2024 10:51:14 -0600 Subject: [PATCH] Improve Tokens Studio inline aliasing --- .changeset/cuddly-cooks-kiss.md | 5 ++ packages/core/src/parse/tokens-studio.ts | 78 +++++++++++++++++-- packages/core/src/parse/tokens/color.ts | 2 +- .../core/test/fixtures/tokens-studio.json | 4 + packages/core/test/tokens-studio.test.ts | 2 +- 5 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 .changeset/cuddly-cooks-kiss.md diff --git a/.changeset/cuddly-cooks-kiss.md b/.changeset/cuddly-cooks-kiss.md new file mode 100644 index 00000000..4e853764 --- /dev/null +++ b/.changeset/cuddly-cooks-kiss.md @@ -0,0 +1,5 @@ +--- +"@cobalt-ui/core": patch +--- + +Improve Tokens Studio inline aliasing diff --git a/packages/core/src/parse/tokens-studio.ts b/packages/core/src/parse/tokens-studio.ts index 847530b4..719de6f9 100644 --- a/packages/core/src/parse/tokens-studio.ts +++ b/packages/core/src/parse/tokens-studio.ts @@ -3,7 +3,8 @@ * This works by first converting the Tokens Studio format * into an equivalent DTCG result, then parsing that result */ -import { parseAlias } from '@cobalt-ui/utils'; +import { isAlias, parseAlias } from '@cobalt-ui/utils'; +import { parse as culoriParse, rgb } from 'culori'; import type { GradientStop, Group, Token } from '../token.js'; // I’m not sure this is comprehensive at all but better than nothing @@ -284,9 +285,12 @@ export function convertTokensStudioFormat(rawTokens: Record): { `Token "${tokenID}" is a multi value borderRadius token. Expanding into ${tokenID}TopLeft, ${tokenID}TopRight, ${tokenID}BottomRight, and ${tokenID}BottomLeft.`, ); let order = [values[0], values[1], values[0], values[1]] as [string, string, string, string]; // TL, BR - if (values.length === 3) - {order = [values[0], values[1], values[2], values[1]] as [string, string, string, string];} // TL, TR/BL, BR - else if (values.length === 4) {order = [values[0], values[1], values[2], values[3]] as [string, string, string, string];} // TL, TR, BR, BL + if (values.length === 3) { + order = [values[0], values[1], values[2], values[1]] as [string, string, string, string]; + } // TL, TR/BL, BR + else if (values.length === 4) { + order = [values[0], values[1], values[2], values[3]] as [string, string, string, string]; + } // TL, TR, BR, BL addToken({ $type: 'dimension', $value: order[0], $description: v.description }, [...path, `${k}TopLeft`]); addToken({ $type: 'dimension', $value: order[1], $description: v.description }, [...path, `${k}TopRight`]); addToken({ $type: 'dimension', $value: order[2], $description: v.description }, [...path, `${k}BottomRight`]); @@ -365,10 +369,65 @@ export function convertTokensStudioFormat(rawTokens: Record): { tokenPath, ); } else { + let value: string | undefined = v.value; + // resolve inline aliases (e.g. `rgba({color.black}, 0.5)`) + if (value.includes('{') && !v.value.startsWith('{')) { + value = resolveAlias(value, path); + + if (!value) { + errors.push(`Could not resolve "${v.value}"`); + continue; + } + + // note: we did some work earlier to help resolve the aliases, but + // we need to REPLACE them in this scenario so we must do a 2nd pass + const matches = value.match(ALIAS_RE); + for (const match of matches ?? []) { + let currentAlias = parseAlias(match).id; + let resolvedValue: string | undefined; + const aliasHistory = new Set([currentAlias]); + while (!resolvedValue) { + const aliasNode: any = get(rawTokens, currentAlias.split('.')); + // does this resolve to a $value? + if (aliasNode && aliasNode.value) { + // is this another alias? + if (isAlias(aliasNode.value)) { + currentAlias = parseAlias(aliasNode.value).id; + if (aliasHistory.has(currentAlias)) { + errors.push(`Couldn’t resolve circular alias "${v.value}"`); + break; + } + aliasHistory.add(currentAlias); + continue; + } + resolvedValue = aliasNode.value; + } + break; + } + + if (resolvedValue) { + value = value.replace(match, resolvedValue); + } + } + + if (!culoriParse(value)) { + // fix `rgba(#000000, 0.3)` scenario specifically (common Tokens Studio version) + // but throw err otherwise + if (value.startsWith('rgb') && value.includes('#')) { + const hexValue = value.match(/#[abcdef0-9]+/i); + if (hexValue && hexValue[0]) { + const rgbVal = rgb(hexValue[0]); + if (rgbVal) { + value = value.replace(hexValue[0], `${rgbVal.r * 100}%, ${rgbVal.g * 100}%, ${rgbVal.b * 100}%`); + } + } + } + } + } addToken( { $type: 'color', - $value: v.value, + $value: value, $description: v.description, }, tokenPath, @@ -441,9 +500,12 @@ export function convertTokensStudioFormat(rawTokens: Record): { } else if (values.length === 2 || values.length === 3 || values.length === 4) { warnings.push(`Token "${tokenID}" is a multi value spacing token. Expanding into ${tokenID}Top, ${tokenID}Right, ${tokenID}Bottom, and ${tokenID}Left.`); let order: [string, string, string, string] = [values[0], values[1], values[0], values[1]] as [string, string, string, string]; // TB, RL - if (values.length === 3) - {order = [values[0], values[1], values[2], values[1]] as [string, string, string, string];} // T, RL, B - else if (values.length === 4) {order = [values[0], values[1], values[2], values[3]] as [string, string, string, string];} // T, R, B, L + if (values.length === 3) { + order = [values[0], values[1], values[2], values[1]] as [string, string, string, string]; + } // T, RL, B + else if (values.length === 4) { + order = [values[0], values[1], values[2], values[3]] as [string, string, string, string]; + } // T, R, B, L addToken({ $type: 'dimension', $value: order[0], $description: v.description }, [...path, `${k}Top`]); addToken({ $type: 'dimension', $value: order[1], $description: v.description }, [...path, `${k}Right`]); addToken({ $type: 'dimension', $value: order[2], $description: v.description }, [...path, `${k}Bottom`]); diff --git a/packages/core/src/parse/tokens/color.ts b/packages/core/src/parse/tokens/color.ts index 8af99896..4e2e7524 100644 --- a/packages/core/src/parse/tokens/color.ts +++ b/packages/core/src/parse/tokens/color.ts @@ -24,7 +24,7 @@ export function normalizeColorValue(rawValue: unknown, options?: ParseColorOptio if (typeof rawValue === 'string') { const parsed = parse(rawValue); if (!parsed) { - throw new Error(`invalid color "${rawValue}"`); + return rawValue; // TODO: should this warn? } let value = parsed as Color; diff --git a/packages/core/test/fixtures/tokens-studio.json b/packages/core/test/fixtures/tokens-studio.json index 207d290b..2990539c 100644 --- a/packages/core/test/fixtures/tokens-studio.json +++ b/packages/core/test/fixtures/tokens-studio.json @@ -77,6 +77,10 @@ "value": "#000000", "type": "color" }, + "black-rgb": { + "value": "0, 0, 0", + "type": "color" + }, "white": { "value": "#ffffff", "type": "color" diff --git a/packages/core/test/tokens-studio.test.ts b/packages/core/test/tokens-studio.test.ts index ec9caecc..72bcd094 100644 --- a/packages/core/test/tokens-studio.test.ts +++ b/packages/core/test/tokens-studio.test.ts @@ -468,6 +468,6 @@ describe('Example files', () => { const cwd = new URL('./fixtures/', import.meta.url); const tokens = JSON.parse(fs.readFileSync(new URL('./tokens-studio.json', cwd), 'utf8')); const parsed = getTokens(tokens); - expect(parsed).toHaveLength(169); + expect(parsed).toHaveLength(170); }); });