From 8021ec0c914d904333cdcc063a4e34c49d2db418 Mon Sep 17 00:00:00 2001 From: jorenbroekema Date: Thu, 12 Oct 2023 10:50:19 +0200 Subject: [PATCH] fix: allow outputReferences to work on non-string values --- .../createPropertyFormatter.test.js | 167 ++++++++++++++++-- .../formatHelpers/createPropertyFormatter.js | 57 +++--- 2 files changed, 184 insertions(+), 40 deletions(-) diff --git a/__tests__/common/formatHelpers/createPropertyFormatter.test.js b/__tests__/common/formatHelpers/createPropertyFormatter.test.js index c9cb40bdd..abcea9a59 100644 --- a/__tests__/common/formatHelpers/createPropertyFormatter.test.js +++ b/__tests__/common/formatHelpers/createPropertyFormatter.test.js @@ -30,20 +30,20 @@ const dictionary = createDictionary({ value: '5px', type: 'spacing' }, - bar: { + ref: { original: { value: '{tokens.foo}', type: 'spacing' }, attributes: { category: 'tokens', - type: 'bar' + type: 'ref' }, - name: 'tokens-bar', - path: ['tokens', 'bar'], + name: 'tokens-ref', + path: ['tokens', 'ref'], value: '5px', type: 'spacing' - }, + } } } }); @@ -65,17 +65,17 @@ const transformedDictionary = createDictionary({ value: '5px', type: 'spacing' }, - bar: { + ref: { original: { value: '{tokens.foo}', type: 'spacing' }, attributes: { category: 'tokens', - type: 'bar' + type: 'ref' }, - name: 'tokens-bar', - path: ['tokens', 'bar'], + name: 'tokens-ref', + path: ['tokens', 'ref'], value: 'changed by transitive transform', type: 'spacing' }, @@ -83,7 +83,128 @@ const transformedDictionary = createDictionary({ } }); +const numberDictionary = createDictionary({ + properties: { + tokens: { + foo: { + original: { + value: 10, + type: 'dimension' + }, + attributes: { + category: 'tokens', + type: 'foo' + }, + name: 'tokens-foo', + path: ['tokens', 'foo'], + value: 10, + type: 'dimension' + }, + ref: { + original: { + value: '{tokens.foo}', + type: 'dimension' + }, + attributes: { + category: 'tokens', + type: 'ref' + }, + name: 'tokens-ref', + path: ['tokens', 'ref'], + value: 10, + type: 'dimension' + }, + } + } +}) + +const multiDictionary = createDictionary({ + properties: { + tokens: { + foo: { + original: { + value: '10px', + type: 'spacing' + }, + attributes: { + category: 'tokens', + type: 'foo' + }, + name: 'tokens-foo', + path: ['tokens', 'foo'], + value: '10px', + type: 'spacing' + }, + bar: { + original: { + value: '15px', + type: 'spacing' + }, + attributes: { + category: 'tokens', + type: 'bar' + }, + name: 'tokens-bar', + path: ['tokens', 'bar'], + value: '15px', + type: 'spacing' + }, + ref: { + original: { + value: '{tokens.foo} 5px {tokens.bar}', + type: 'spacing' + }, + attributes: { + category: 'tokens', + type: 'ref' + }, + name: 'tokens-ref', + path: ['tokens', 'ref'], + value: '10px 5px 15px', + type: 'spacing' + }, + } + } +}) +const objectDictionary = createDictionary({ + properties: { + tokens: { + foo: { + original: { + value: '5px', + type: 'spacing' + }, + attributes: { + category: 'tokens', + type: 'foo' + }, + name: 'tokens-foo', + path: ['tokens', 'foo'], + value: '5px', + type: 'spacing' + }, + ref: { + original: { + value: { + width: '{tokens.foo}', + style: 'dashed', + color: '#FF00FF' + }, + type: 'border' + }, + attributes: { + category: 'tokens', + type: 'ref' + }, + name: 'tokens-ref', + path: ['tokens', 'ref'], + value: '5px dashed #FF00FF', + type: 'border' + } + } + } +}); describe('common', () => { @@ -92,13 +213,35 @@ describe('common', () => { it('should support outputReferences', () => { const propFormatter = createPropertyFormatter({ outputReferences: true, dictionary, format: 'css' }) expect(propFormatter(dictionary.tokens.tokens.foo)).toEqual(' --tokens-foo: 5px;'); - expect(propFormatter(dictionary.tokens.tokens.bar)).toEqual(' --tokens-bar: var(--tokens-foo);'); + expect(propFormatter(dictionary.tokens.tokens.ref)).toEqual(' --tokens-ref: var(--tokens-foo);'); }) it('should support outputReferences when values are transformed by (transitive) "value" transforms', () => { - const propFormatter = createPropertyFormatter({ outputReferences: true, dictionary, format: 'css' }) + const propFormatter = createPropertyFormatter({ outputReferences: true, dictionary: transformedDictionary, format: 'css' }) expect(propFormatter(transformedDictionary.tokens.tokens.foo)).toEqual(' --tokens-foo: 5px;'); - expect(propFormatter(transformedDictionary.tokens.tokens.bar)).toEqual(' --tokens-bar: var(--tokens-foo);'); + expect(propFormatter(transformedDictionary.tokens.tokens.ref)).toEqual(' --tokens-ref: var(--tokens-foo);'); + }) + + it('should support number values for outputReferences', () => { + const propFormatter = createPropertyFormatter({ outputReferences: true, dictionary: numberDictionary, format: 'css' }) + expect(propFormatter(numberDictionary.tokens.tokens.foo)).toEqual(' --tokens-foo: 10;'); + expect(propFormatter(numberDictionary.tokens.tokens.ref)).toEqual(' --tokens-ref: var(--tokens-foo);'); + }) + + it('should support multiple references for outputReferences', () => { + const propFormatter = createPropertyFormatter({ outputReferences: true, dictionary: multiDictionary, format: 'css' }) + expect(propFormatter(multiDictionary.tokens.tokens.foo)).toEqual(' --tokens-foo: 10px;'); + expect(propFormatter(multiDictionary.tokens.tokens.bar)).toEqual(' --tokens-bar: 15px;'); + expect(propFormatter(multiDictionary.tokens.tokens.ref)).toEqual(' --tokens-ref: var(--tokens-foo) 5px var(--tokens-bar);'); + }) + + it('should support object value references for outputReferences', () => { + // The ref is an object type value, which means there will usually be some kind of transform (e.g. a CSS shorthand transform) + // to change it from an object to a string. In our example, we use a border CSS shorthand for border token. + // In this case, since it is an object value, we will run the transformation on the transformed (string) value. + const propFormatter = createPropertyFormatter({ outputReferences: true, dictionary: objectDictionary, format: 'css' }) + expect(propFormatter(objectDictionary.tokens.tokens.foo)).toEqual(' --tokens-foo: 5px;'); + expect(propFormatter(objectDictionary.tokens.tokens.ref)).toEqual(' --tokens-ref: var(--tokens-foo) dashed #FF00FF;'); }) }) }) diff --git a/lib/common/formatHelpers/createPropertyFormatter.js b/lib/common/formatHelpers/createPropertyFormatter.js index 01f72a727..3c79090aa 100644 --- a/lib/common/formatHelpers/createPropertyFormatter.js +++ b/lib/common/formatHelpers/createPropertyFormatter.js @@ -104,41 +104,42 @@ function createPropertyFormatter({ if (outputReferences && dictionary.usesReference(prop.original.value)) { // Formats that use this function expect `value` to be a string // or else you will get '[object Object]' in the output - if (typeof value === 'string') { - const refs = dictionary.getReferences(prop.original.value); + const refs = dictionary.getReferences(prop.original.value); - // original can either be string value or an object value - const originalIsString = typeof prop.original.value === 'string'; + // original can either be an object value, which requires transitive value transformation in web CSS formats + // or a different (primitive) type, meaning it can be stringified. + const originalIsObject = typeof prop.original.value === 'object' && prop.original.value !== null; - // Set the value to the original value with refs first, undoing value-changing transitive transforms - if (originalIsString) { - value = prop.original.value; - } + if (!originalIsObject) { + // when original is object value, we replace value by matching ref.value and putting a var instead. + // Due to the original.value being an object, it requires transformation, so undoing the transformation + // by replacing value with original.value is not possible. + + // when original is string value, we replace value by matching original.value and putting a var instead + // this is more friendly to transitive transforms that transform the string values + value = prop.original.value; + } - refs.forEach(ref => { - // value should be a string that contains the resolved reference - // because Style Dictionary resolved this in the resolution step. - // Here we are undoing that by replacing the value with - // the reference's name - if (ref.value && ref.name) { - const replaceFunc = function() { - if (format === 'css') { - if (outputReferenceFallbacks) { - return `var(${prefix}${ref.name}, ${ref.value})`; - } else { - return `var(${prefix}${ref.name})`; - } + refs.forEach(ref => { + // value should be a string that contains the resolved reference + // because Style Dictionary resolved this in the resolution step. + // Here we are undoing that by replacing the value with + // the reference's name + if (ref.value && ref.name) { + const replaceFunc = function() { + if (format === 'css') { + if (outputReferenceFallbacks) { + return `var(${prefix}${ref.name}, ${ref.value})`; } else { - return `${prefix}${ref.name}`; + return `var(${prefix}${ref.name})`; } + } else { + return `${prefix}${ref.name}`; } - // when original is object value, we replace value by matching ref.value and putting a var instead - // when original is string value, we replace value by matching original.value and putting a var instead - // this is more friendly to transitive transforms that transform the string values - value = value.replace(originalIsString ? new RegExp(`{${ref.path.join('.')}(.value)?}`, 'g') : ref.value, replaceFunc); } - }); - } + value = value.replace(originalIsObject ? ref.value : new RegExp(`{${ref.path.join('.')}(.value)?}`, 'g'), replaceFunc); + } + }); } to_ret_prop += prop.attributes.category === 'asset' ? `"${value}"` : value;