diff --git a/packages/@o3r/design/README.md b/packages/@o3r/design/README.md
index 7aa5cfa35f..61ad57db2b 100644
--- a/packages/@o3r/design/README.md
+++ b/packages/@o3r/design/README.md
@@ -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.
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.
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).
diff --git a/packages/@o3r/design/schemas/design-token.schema.json b/packages/@o3r/design/schemas/design-token.schema.json
index 76c0d8f989..1fef3a234e 100644
--- a/packages/@o3r/design/schemas/design-token.schema.json
+++ b/packages/@o3r/design/schemas/design-token.schema.json
@@ -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"
}
}
},
diff --git a/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts b/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts
index c76bb01070..4edd5b9d98 100644
--- a/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts
+++ b/packages/@o3r/design/src/core/design-token/design-token-specification.interface.ts
@@ -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 */
diff --git a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts
index aa22d12622..63d9161be7 100644
--- a/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts
+++ b/packages/@o3r/design/src/core/design-token/parsers/design-token.parser.ts
@@ -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) => {
@@ -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);
@@ -46,7 +72,7 @@ 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':
@@ -54,18 +80,20 @@ const getCssRawValue = (variableSet: DesignTokenVariableSet, {node, getType}: De
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') {
@@ -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: {
diff --git a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts
index 3a6bea02dc..cb9d315a18 100644
--- a/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts
+++ b/packages/@o3r/design/src/core/design-token/renderers/css/design-token-value.renderers.spec.ts
@@ -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
+ });
+ });
});
diff --git a/packages/@o3r/design/src/core/design-token/renderers/json-schema/design-token-value.renderers.ts b/packages/@o3r/design/src/core/design-token/renderers/json-schema/design-token-value.renderers.ts
index 49d227f025..4cda68b172 100644
--- a/packages/@o3r/design/src/core/design-token/renderers/json-schema/design-token-value.renderers.ts
+++ b/packages/@o3r/design/src/core/design-token/renderers/json-schema/design-token-value.renderers.ts
@@ -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();
diff --git a/packages/@o3r/design/testing/mocks/design-token-theme.json b/packages/@o3r/design/testing/mocks/design-token-theme.json
index 288afb1e35..a5d46bc7e1 100644
--- a/packages/@o3r/design/testing/mocks/design-token-theme.json
+++ b/packages/@o3r/design/testing/mocks/design-token-theme.json
@@ -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"