Skip to content

Commit

Permalink
feat(design): add unit override and rationing (#2171)
Browse files Browse the repository at this point in the history
## Proposed change

Add unit override and rationing capability to Design Token generation.
The purpose of this feature is to provide workaround capability to Figma
limitations
  • Loading branch information
kpanot authored Sep 20, 2024
2 parents ec0d7d4 + 069d4a6 commit f6c1fe3
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 9 deletions.
19 changes: 19 additions & 0 deletions packages/@o3r/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,23 @@ It comes with the following options:

## Technical documentation

### Additional feature on top of standard Design Token

To enhance the features of default Design Token standard and provide additional information to renderers, the [$extensions](https://tr.designtokens.org/format/#extensions) properties has been enhanced by Otter Tooling with the following options:

| Extension property | Supporting Renderers | Description |
| ------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **o3rTargetFile** | `css`, `sass` | Information regarding the path to file where the token requests to be generated |
| **o3rPrivate** | `css`, `sass`, `json-schema`, `metadata`, `design-token` | Determine if the token is flagged as private |
| **o3rImportant** | `css` | Determine if the token should be flagged as important when generated |
| **o3rScope** | `css`, `sass` | Scope to apply to the generated variable |
| **o3rMetadata** | `css`, `sass`, `json-schema`, `metadata`, `design-token` | Additional information to provide to the metadata if generated |
| **o3rUnit** | `css`, `sass`, `metadata`, `design-token` | Convert a numeric value from the specified unit to the new unit. It will add a unit to the token with type "number" for which the unit is not specified.<br />In case of complex type (such as shadow, transition, etc...), the unit will be applied to all numeric types in it. |
| **o3rRatio** | `css`, `sass`, `metadata`, `design-token` | Ratio to apply to previous value. The ratio will be applied only on token with "number" type or on the first numbers determined in "string" like types.<br />In case of complex type (such as shadow, transition, etc...), the ratio will be applied to all numeric types in it. |

> [!NOTE]
> In case of implementation of custom renderer, additional properties dedicated to this renderer can be added following Design Token Extensions [guidelines](https://tr.designtokens.org/format/#extensions).
### Going deeper

Documentation providing explanations on the use and customization of the `Design Token` parser and renderers is available in the [technical documentation](https://github.com/AmadeusITGroup/otter/blob/main/docs/design/TECHNICAL_DOCUMENTATION.md).
8 changes: 8 additions & 0 deletions packages/@o3r/design/schemas/design-token.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@
"o3rMetadata": {
"description": "Additional information to provide to the metadata if generated",
"$ref": "#/definitions/otterExtensionMetadata"
},
"o3rUnit": {
"description": "Convert a numeric value from the specified unit to the new unit. It will add a unit to the token with type \"number\" for which the unit is not specified.\nIn case of complex type (such as shadow, transition, etc...), the unit will be applied to all numeric types in it.",
"type": "string"
},
"o3rRatio": {
"description": "Ratio to apply to previous value. The ratio will be applied only on token with \"number\" type or on the first numbers determined in \"string\" like types.\nIn case of complex type (such as shadow, transition, etc...), the ratio will be applied to all numeric types in it.",
"type": "number"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ export interface DesignTokenGroupExtensions {
o3rMetadata?: DesignTokenMetadata;
/** Scope of the Design Token value */
o3rScope?: string;
/**
* Convert a numeric value from the specified unit to the new unit.
* It will add a unit to the token with type "number" for which the unit is not specified.
* In case of complex type (such as shadow, transition, etc...), the unit will be applied to all numeric types in it.
*/
o3rUnit?: string;
/**
* Ratio to apply to previous value.
* The ratio will be applied only on token with "number" type or on the first numbers determined in "string" like types.
* In case of complex type (such as shadow, transition, etc...), the ratio will be applied to all numeric types in it.
*/
o3rRatio?: number;
}

/** Design Token Extension fields supported by the default renderer */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { dirname } from 'node:path';

const tokenReferenceRegExp = /\{([^}]+)\}/g;
const splitValueNumericRegExp = /^([-+]?[0-9]+[.,]?[0-9]*)\s*([^\s.,;]+)?/;

const getTokenReferenceName = (tokenName: string, parents: string[]) => parents.join('.') + (parents.length ? '.' : '') + tokenName;
const getExtensions = (nodes: NodeReference[], context: DesignTokenContext | undefined) => {
Expand All @@ -30,11 +31,36 @@ const getExtensions = (nodes: NodeReference[], context: DesignTokenContext | und
}, {} as DesignTokenGroupExtensions & DesignTokenExtensions);
};
const getReferences = (cssRawValue: string) => Array.from(cssRawValue.matchAll(tokenReferenceRegExp)).map(([,tokenRef]) => tokenRef);
const applyConversion = (token: DesignTokenVariableStructure, value: string) => {
if (typeof token.extensions.o3rUnit === 'undefined' || typeof token.extensions.o3rRatio === 'undefined') {
return value;
}

const splitValue = splitValueNumericRegExp.exec(value);
if (!splitValue) {
return value;
}

const [, floatValue, unit] = splitValue;

const newValue = value.replace(floatValue, (parseFloat((parseFloat(floatValue) * token.extensions.o3rRatio).toFixed(3))).toString());

if (unit) {
return newValue.replace(unit, token.extensions.o3rUnit);
}

if (floatValue === value) {
return newValue + token.extensions.o3rUnit;
}

return newValue;
};
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
const renderCssTypeStrokeStyleValue = (value: DesignTokenTypeStrokeStyleValue | string) => isTokenTypeStrokeStyleValueComplex(value) ? `${value.lineCap} ${value.dashArray.join(' ')}` : value;
const sanitizeStringValue = (value: string) => value.replace(/[\\]/g, '\\\\').replace(/"/g, '\\"');
const sanitizeKeyName = (name: string) => name.replace(/[ .]+/g, '-').replace(/[()[\]]+/g, '');
const getCssRawValue = (variableSet: DesignTokenVariableSet, {node, getType}: DesignTokenVariableStructure) => {
const getCssRawValue = (variableSet: DesignTokenVariableSet, token: DesignTokenVariableStructure) => {
const { node, getType } = token;
const nodeType = getType(variableSet, false);
if (!nodeType && node.$value) {
return typeof node.$value.toString !== 'undefined' ? (node.$value as any).toString() : JSON.stringify(node.$value);
Expand All @@ -46,26 +72,28 @@ const getCssRawValue = (variableSet: DesignTokenVariableSet, {node, getType}: De

switch (checkNode.$type) {
case 'string': {
return `"${sanitizeStringValue(checkNode.$value.toString())}"`;
return `"${applyConversion(token, sanitizeStringValue(checkNode.$value.toString()))}"`;
}
case 'color':
case 'number':
case 'duration':
case 'fontWeight':
case 'fontFamily':
case 'dimension': {
return checkNode.$value.toString();
return applyConversion(token, checkNode.$value.toString());
}
case 'strokeStyle': {
return renderCssTypeStrokeStyleValue(checkNode.$value);
}
case 'cubicBezier': {
return typeof checkNode.$value === 'string' ? checkNode.$value :
checkNode.$value.join(', ');
checkNode.$value
.map((value) => applyConversion(token, value.toString()))
.join(', ');
}
case 'border': {
return typeof checkNode.$value === 'string' ? checkNode.$value :
`${checkNode.$value.width} ${renderCssTypeStrokeStyleValue(checkNode.$value.style)} ${checkNode.$value.color}`;
`${applyConversion(token, checkNode.$value.width)} ${renderCssTypeStrokeStyleValue(checkNode.$value.style)} ${checkNode.$value.color}`;
}
case 'gradient': {
if (typeof checkNode.$value === 'string') {
Expand All @@ -83,17 +111,19 @@ const getCssRawValue = (variableSet: DesignTokenVariableSet, {node, getType}: De

const values = Array.isArray(checkNode.$value) ? checkNode.$value : [checkNode.$value];
return values
.map((value) => `${value.offsetX} ${value.offsetY} ${value.blur} ${value.spread} ${value.color}`)
.map((value) => `${applyConversion(token, value.offsetX)} ${applyConversion(token, value.offsetY)} ${applyConversion(token, value.blur)} ${applyConversion(token, value.spread)}`
+ ` ${value.color}`)
.join(', ');
}
case 'transition': {
return typeof checkNode.$value === 'string' ? checkNode.$value :
typeof checkNode.$value.timingFunction === 'string' ? checkNode.$value.timingFunction : checkNode.$value.timingFunction.join(' ') +
` ${checkNode.$value.duration} ${checkNode.$value.delay}`;
` ${applyConversion(token, checkNode.$value.duration)} ${applyConversion(token, checkNode.$value.delay)}`;
}
case 'typography': {
return typeof checkNode.$value === 'string' ? checkNode.$value :
`${checkNode.$value.fontWeight} ${checkNode.$value.fontFamily} ${checkNode.$value.fontSize} ${checkNode.$value.letterSpacing} ${checkNode.$value.lineHeight}`;
`${applyConversion(token, checkNode.$value.fontWeight.toString())} ${checkNode.$value.fontFamily}`
+ ` ${applyConversion(token, checkNode.$value.fontSize)} ${applyConversion(token, checkNode.$value.letterSpacing)} ${applyConversion(token, checkNode.$value.lineHeight.toString())}`;
}
// TODO: Add support for Grid type when available in the Design Token Standard
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,30 @@ describe('getCssTokenValueRenderer', () => {
expect(debug).toHaveBeenCalledWith(expect.stringContaining('var(--does-not-exist)'));
expect(result).toBe('var(--does-not-exist)');
});

describe('with extension value override', () => {
test('should not override non-numeric value', () => {
const renderer = getCssTokenValueRenderer();
const variable = designTokens.get('example.var-color-unit-ratio-override');

const result = renderer(variable, designTokens);
expect(result).toBe('#000');
});

test('should override numeric value and add unit', () => {
const renderer = getCssTokenValueRenderer();
const variable = designTokens.get('example.var-number-unit-ratio-override');

const result = renderer(variable, designTokens);
expect(result).toBe('5px'); // default value: 2
});

test('should override numeric value and unit', () => {
const renderer = getCssTokenValueRenderer();
const variable = designTokens.get('example.var-unit-override');

const result = renderer(variable, designTokens);
expect(result).toBe('5rem'); // default value: 2px
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const getJsonSchemaTokenValueRenderer = (options?: JsonSchemaTokenValueRe
const cssType = variable.getType(variableSet);
const variableValue: any = {
description: variable.description,
default: variable.node.$value
default: variable.getCssRawValue(variableSet)
};
if (!cssType) {
variableValue.$ref = referenceUrl();
Expand Down
24 changes: 24 additions & 0 deletions packages/@o3r/design/testing/mocks/design-token-theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@
"$type": "color",
"$value": "#000"
},
"var-color-unit-ratio-override": {
"$type": "color",
"$value": "#000",
"$extensions": {
"o3rUnit": "px",
"o3rRatio": 2.5
}
},
"var-number-unit-ratio-override": {
"$type": "number",
"$value": 2,
"$extensions": {
"o3rUnit": "px",
"o3rRatio": 2.5
}
},
"var-unit-override": {
"$type": "dimension",
"$value": "2px",
"$extensions": {
"o3rUnit": "rem",
"o3rRatio": 2.5
}
},
"var-string": {
"$type": "string",
"$value": "test value"
Expand Down

0 comments on commit f6c1fe3

Please sign in to comment.