From 069d4a67b0df0c400e3bb0c332f46ae4948c09c4 Mon Sep 17 00:00:00 2001 From: Kilian Panot Date: Tue, 17 Sep 2024 12:25:40 +0900 Subject: [PATCH] feat(design): add unit override and rationing --- packages/@o3r/design/README.md | 19 ++++++++ .../design/schemas/design-token.schema.json | 8 ++++ .../design-token-specification.interface.ts | 12 +++++ .../parsers/design-token.parser.ts | 46 +++++++++++++++---- .../css/design-token-value.renderers.spec.ts | 26 +++++++++++ .../design-token-value.renderers.ts | 2 +- .../testing/mocks/design-token-theme.json | 24 ++++++++++ 7 files changed, 128 insertions(+), 9 deletions(-) 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"