Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow outputReferences to work on non-string values #1025

Merged
merged 1 commit into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 155 additions & 12 deletions __tests__/common/formatHelpers/createPropertyFormatter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
}
}
}
});
Expand All @@ -65,25 +65,146 @@ 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'
},
}
}
});

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', () => {
Expand All @@ -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;');
})
})
})
Expand Down
57 changes: 29 additions & 28 deletions lib/common/formatHelpers/createPropertyFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading