Skip to content

Commit

Permalink
feat(design): add unit override and rationing
Browse files Browse the repository at this point in the history
  • Loading branch information
kpanot committed Sep 19, 2024
1 parent dc027b9 commit b663ebc
Show file tree
Hide file tree
Showing 7 changed files with 126 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. |
| **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. |

> [!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.",
"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.",
"type": "number"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ 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.
*/
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.
*/
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 b663ebc

Please sign in to comment.