Skip to content

Commit

Permalink
fix: allow outputReferences to work on non-string values
Browse files Browse the repository at this point in the history
  • Loading branch information
jorenbroekema committed Oct 24, 2023
1 parent dab4161 commit 24d41c3
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-ears-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'style-dictionary': patch
---

Allow outputReferences to work on non-string values.
185 changes: 174 additions & 11 deletions __tests__/common/formatHelpers/createPropertyFormatter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ 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,20 +65,143 @@ const transformedDictionary = createDictionary({
value: '5px',
type: 'spacing',
},
bar: {
ref: {
original: {
value: '{tokens.foo}',
type: 'spacing',
},
attributes: {
category: 'tokens',
type: 'ref',
},
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: 'changed by transitive transform',
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',
},
},
},
});
Expand All @@ -93,22 +216,62 @@ describe('common', () => {
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,
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
68 changes: 35 additions & 33 deletions lib/common/formatHelpers/createPropertyFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,44 +108,46 @@ export default 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})`;
}
};
// 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,
);
}
});
}
} else {
return `${prefix}${ref.name}`;
}
};
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 24d41c3

Please sign in to comment.