Skip to content

Commit

Permalink
feat: add w3c dtcg draft spec compatiblity
Browse files Browse the repository at this point in the history
  • Loading branch information
jorenbroekema committed Jan 26, 2024
1 parent 9e3fb1b commit fa6f719
Show file tree
Hide file tree
Showing 11 changed files with 582 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/lemon-singers-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tokens-studio/sd-transforms': minor
---

Add [W3C Design Token Community Group draft spec](https://design-tokens.github.io/community-group/format/) forward compatibility.
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
},
"types": "./dist/src/index.d.ts",
"dependencies": {
"@tokens-studio/types": "^0.2.4",
"@tokens-studio/types": "^0.4.0",
"color2k": "^2.0.1",
"colorjs.io": "^0.4.3",
"deepmerge": "^4.3.1",
"expr-eval-fork": "^2.0.2",
"is-mergeable-object": "^1.1.1",
"postcss-calc-ast-parser": "^0.1.4",
"style-dictionary": "4.0.0-prerelease.9"
"style-dictionary": "4.0.0-prerelease.10"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
Expand Down
2 changes: 1 addition & 1 deletion src/color-modifiers/transformColorModifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ export function transformColorModifiers(
if (options?.format) {
modifier.format = options.format;
}
return modifyColor(token.value, modifier);
return modifyColor(token.$value ?? token.value, modifier);
}
15 changes: 5 additions & 10 deletions src/parsers/add-font-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,16 @@ function recurse(
if (typeof token !== 'object' || token === null) {
continue;
}
const { type, value } = token;
if (type === 'typography') {
const { value, type, $type } = token;
if ($type === 'typography' || type === 'typography') {
if (typeof value !== 'object' || value.fontWeight === undefined) {
continue;
}
let fontWeight = value.fontWeight;
if (usesReferences(fontWeight)) {
try {
const resolved = resolveReferences(fontWeight, copy);
if (resolved) {
fontWeight = `${resolved}`;
}
} catch (e) {
// we don't want to throw a fatal error, we'll just keep the ref as is
console.error(e);
const resolved = resolveReferences(fontWeight, copy);
if (resolved) {
fontWeight = `${resolved}`;
}
}
// cast because fontStyle is a prop we will add ourselves
Expand Down
79 changes: 47 additions & 32 deletions src/parsers/expand-composites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,43 @@ const typeMaps = {
},
};

function flattenValues<T extends SingleToken<false>['value']>(val: T): T {
return Object.fromEntries(Object.entries(val).map(([k, v]) => [k, v.value])) as T;
function flattenValues<T extends Required<SingleToken<false>>['value']>(val: T): T {
return Object.fromEntries(Object.entries(val).map(([k, v]) => [k, v.$value ?? v.value])) as T;
}

export function expandToken(compToken: SingleToken<false>, isShadow = false): SingleToken<false> {
if (typeof compToken.value !== 'object') {
return compToken;
export function expandToken(token: Expandables, isShadow = false): SingleToken<false> {
const uses$ = token.$value != null;
const value = uses$ ? token.$value : token.value;
// the $type and type may both be missing if the $type is coming from an ancestor,
// however, style-dictionary runs a preprocessing step so missing $type is added from the closest ancestor
// our token types are not aware of that however, so we must do an undefined check here
const tokenType = token.$type ?? token.type;
// ignore coverage, this undefined check is purely to appease TypeScript,
// but we know type cannot be undefined here since we checked it in recurse()
/* c8 ignore next 3 */
if (tokenType === undefined) {
return token;
}
const expandedObj = {} as SingleToken<false>;

const getType = (key: string) => typeMaps[compToken.type][key] ?? key;
const expandedObj = {} as SingleToken<false>;
const getType = (key: string) => typeMaps[tokenType][key] ?? key;

// multi-shadow
if (isShadow && Array.isArray(compToken.value)) {
compToken.value.forEach((shadow, index) => {
if (isShadow && Array.isArray(value)) {
value.forEach((shadow, index) => {
expandedObj[index + 1] = {};
Object.entries(shadow).forEach(([key, value]) => {
expandedObj[index + 1][key] = {
value: `${value}`,
type: getType(key),
[`${uses$ ? '$' : ''}value`]: `${value}`,
[`${uses$ ? '$' : ''}type`]: getType(key),
};
});
});
} else {
Object.entries(compToken.value).forEach(([key, value]) => {
Object.entries(value).forEach(([key, value]) => {
expandedObj[key] = {
value: `${value}`,
type: getType(key),
[`${uses$ ? '$' : ''}value`]: `${value}`,
[`${uses$ ? '$' : ''}type`]: getType(key),
};
});
}
Expand Down Expand Up @@ -99,8 +108,11 @@ function recurse(
if (typeof token !== 'object' || token === null) {
continue;
}
const { type } = token;
if (token.value && type) {

const uses$ = token.$value != null;
const value = uses$ ? token.$value : token.value;
const type = token.$type ?? token.type;
if (value && type) {
if (typeof type === 'string' && expandablesAsStringsArr.includes(type)) {
const expandType = (type as ExpandablesAsStrings) === 'boxShadow' ? 'shadow' : type;
const expand = shouldExpand<Expandables>(
Expand All @@ -110,28 +122,31 @@ function recurse(
);
if (expand) {
// if token uses a reference, attempt to resolve it
if (typeof token.value === 'string' && usesReferences(token.value)) {
try {
const resolved = resolveReferences(token.value, copy as DesignTokens);
if (resolved) {
token.value = resolved;
}
} catch (e) {
// we don't want to throw a fatal error, expansion can still occur, just with the reference kept as is
console.error(e);
}
// If every key of the result (object) is a number, the ref value is a multi-value, which means TokenBoxshadowValue[]
if (typeof token.value === 'object') {
if (Object.keys(token.value).every(key => !isNaN(Number(key)))) {
token.value = (Object.values(token.value) as TokenBoxshadowValue[]).map(part =>
if (typeof value === 'string' && usesReferences(value)) {
let resolved = resolveReferences(
value,
copy as DesignTokens,
) as SingleToken<false>['value'];
if (typeof resolved === 'object') {
// If every key of the result (object) is a number, the ref value is a multi-value, which means TokenBoxshadowValue[]
if (Object.keys(resolved).every(key => !isNaN(Number(key)))) {
resolved = (Object.values(resolved) as TokenBoxshadowValue[]).map(part =>
flattenValues(part),
);
} else {
token.value = flattenValues(token.value);
resolved = flattenValues(resolved);
}
}

if (resolved) {
if (uses$) {
token.$value = resolved;
} else {
token.value = resolved;
}
}
}
slice[key] = expandToken(token, expandType === 'shadow');
slice[key] = expandToken(token as Expandables, expandType === 'shadow');
}
}
} else {
Expand Down
80 changes: 47 additions & 33 deletions src/registerTransforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,54 +70,59 @@ export async function registerTransforms(
sd.registerTransform({
name: 'ts/descriptionToComment',
type: 'attribute',
matcher: token => token.description,
// in style-dictionary v4.0.0-prerelease.9, $description is converted to comments (createPropertyFormatter)
matcher: token => !token.$description && token.description,
transformer: token => mapDescriptionToComment(token),
});

sd.registerTransform({
name: 'ts/size/px',
type: 'value',
matcher: token =>
typeof token.type === 'string' &&
['sizing', 'spacing', 'borderRadius', 'borderWidth', 'fontSizes', 'dimension'].includes(
token.type,
),
transformer: token => transformDimension(token.value),
matcher: token => {
const type = token.$type ?? token.type;
return (
typeof type === 'string' &&
['sizing', 'spacing', 'borderRadius', 'borderWidth', 'fontSizes', 'dimension'].includes(
type,
)
);
},
transformer: token => transformDimension(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/opacity',
type: 'value',
matcher: token => token.type === 'opacity',
transformer: token => transformOpacity(token.value),
matcher: token => (token.$type ?? token.type) === 'opacity',
transformer: token => transformOpacity(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/size/css/letterspacing',
type: 'value',
matcher: token => token.type === 'letterSpacing',
transformer: token => transformLetterSpacingForCSS(token.value),
matcher: token => (token.$type ?? token.type) === 'letterSpacing',
transformer: token => transformLetterSpacingForCSS(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/size/lineheight',
type: 'value',
matcher: token => token.type === 'lineHeights',
transformer: token => transformLineHeight(token.value),
matcher: token => (token.$type ?? token.type) === 'lineHeights',
transformer: token => transformLineHeight(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/typography/fontWeight',
type: 'value',
matcher: token => token.type === 'fontWeights',
transformer: token => transformFontWeights(token.value),
matcher: token => (token.$type ?? token.type) === 'fontWeights',
transformer: token => transformFontWeights(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/typography/css/fontFamily',
type: 'value',
matcher: token => token.type === 'fontFamilies',
transformer: token => processFontFamily(token.value),
matcher: token => (token.$type ?? token.type) === 'fontFamilies',
transformer: token => processFontFamily(token.$value ?? token.value),
});

/**
Expand All @@ -135,59 +140,68 @@ export async function registerTransforms(
name: 'ts/resolveMath',
type: 'value',
transitive: true,
matcher: token => typeof token.value === 'string',
transformer: token => checkAndEvaluateMath(token.value),
matcher: token => typeof (token.$value ?? token.value) === 'string',
transformer: token => checkAndEvaluateMath(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/typography/css/shorthand',
type: 'value',
transitive: true,
matcher: token => token.type === 'typography',
transformer: token => transformTypographyForCSS(token.value),
matcher: token => (token.$type ?? token.type) === 'typography',
transformer: token => transformTypographyForCSS(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/typography/compose/shorthand',
type: 'value',
transitive: true,
matcher: token => token.type === 'typography',
transformer: token => transformTypographyForCompose(token.value),
matcher: token => (token.$type ?? token.type) === 'typography',
transformer: token => transformTypographyForCompose(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/border/css/shorthand',
type: 'value',
transitive: true,
matcher: token => token.type === 'border',
transformer: token => transformBorderForCSS(token.value),
matcher: token => {
return (token.$type ?? token.type) === 'border';
},
transformer: token => transformBorderForCSS(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/shadow/css/shorthand',
type: 'value',
transitive: true,
matcher: token => typeof token.type === 'string' && ['boxShadow'].includes(token.type),
transformer: token =>
Array.isArray(token.value)
? token.value.map(single => transformShadowForCSS(single)).join(', ')
: transformShadowForCSS(token.value),
matcher: token => {
const type = token.$type ?? token.type;
return typeof type === 'string' && ['boxShadow'].includes(type);
},
transformer: token => {
const val = token.$value ?? token.value;
return Array.isArray(val)
? val.map(single => transformShadowForCSS(single)).join(', ')
: transformShadowForCSS(val);
},
});

sd.registerTransform({
name: 'ts/color/css/hexrgba',
type: 'value',
transitive: true,
matcher: token => typeof token.value === 'string' && token.type === 'color',
transformer: token => transformHEXRGBaForCSS(token.value),
matcher: token => (token.$type ?? token.type) === 'color',
transformer: token => transformHEXRGBaForCSS(token.$value ?? token.value),
});

sd.registerTransform({
name: 'ts/color/modifiers',
type: 'value',
transitive: true,
matcher: token =>
token.type === 'color' && token.$extensions && token.$extensions['studio.tokens']?.modify,
(token.$type ?? token.type) === 'color' &&
token.$extensions &&
token.$extensions['studio.tokens']?.modify,
transformer: token => transformColorModifiers(token, transformOpts?.['ts/color/modifiers']),
});

Expand Down
Loading

0 comments on commit fa6f719

Please sign in to comment.