From fdea7d897696389c59f223e8bcf1aaadfae7bb92 Mon Sep 17 00:00:00 2001 From: jorenbroekema Date: Thu, 9 May 2024 18:48:24 +0200 Subject: [PATCH] feat: add object-value CSS shorthand transforms --- .changeset/famous-suns-rule.md | 13 + .../__snapshots__/objectValues.test.snap.js | 16 +- __integration__/objectValues.test.js | 25 +- __tests__/__helpers.js | 14 + __tests__/common/transforms.test.js | 243 ++++++++++++++ __tests__/exportPlatform.test.js | 310 +++++++++++++++++- .../Hooks/Transform Groups/predefined.md | 21 ++ .../reference/Hooks/Transforms/predefined.md | 126 +++++++ lib/StyleDictionary.js | 20 ++ lib/common/transformGroups.js | 199 ++++++----- lib/common/transforms.js | 281 ++++++++++++++++ lib/utils/deepmerge.js | 10 +- lib/utils/groupMessages.js | 1 + 13 files changed, 1170 insertions(+), 109 deletions(-) create mode 100644 .changeset/famous-suns-rule.md diff --git a/.changeset/famous-suns-rule.md b/.changeset/famous-suns-rule.md new file mode 100644 index 000000000..1484e7488 --- /dev/null +++ b/.changeset/famous-suns-rule.md @@ -0,0 +1,13 @@ +--- +'style-dictionary': minor +--- + +Added the following transforms for CSS, and added them to the `scss`, `css` and `less` transformGroups: + +- `fontFamily/css` -> wraps font names with spaces in `'` quotes +- `cubicBezier/css` -> array value, put inside `cubic-bezier()` CSS function +- `strokeStyle/css/shorthand` -> object value, transform to CSS shorthand +- `border/css/shorthand` -> object value, transform to CSS shorthand +- `typography/css/shorthand` -> object value, transform to CSS shorthand +- `transition/css/shorthand` -> object value, transform to CSS shorthand +- `shadow/css/shorthand` -> object value (or array of objects), transform to CSS shorthand \ No newline at end of file diff --git a/__integration__/__snapshots__/objectValues.test.snap.js b/__integration__/__snapshots__/objectValues.test.snap.js index 6572523ce..a09c7d6c1 100644 --- a/__integration__/__snapshots__/objectValues.test.snap.js +++ b/__integration__/__snapshots__/objectValues.test.snap.js @@ -84,8 +84,8 @@ snapshots["css/variables shadow should match snapshot"] = */ :root { - --shadow-light: #ff0000, #40bf40; - --shadow-dark: #40bf40, #ff0000; + --shadow-light: 0 0 0 #ff0000, 0 0 0 #40bf40; + --shadow-dark: 0 0 0 #40bf40, 0 0 0 #ff0000; } `; /* end snapshot css/variables shadow should match snapshot */ @@ -97,8 +97,8 @@ snapshots["css/variables shadow should match snapshot with references"] = */ :root { - --shadow-light: var(--color-red), var(--color-green); - --shadow-dark: var(--color-green), var(--color-red); + --shadow-light: 0 0 0 var(--color-red), 0 0 0 var(--color-green); + --shadow-dark: 0 0 0 var(--color-green), 0 0 0 var(--color-red); } `; /* end snapshot css/variables shadow should match snapshot with references */ @@ -127,8 +127,8 @@ snapshots["integration object values css/variables shadow should match snapshot */ :root { - --shadow-light: var(--color-red), var(--color-green); - --shadow-dark: var(--color-green), var(--color-red); + --shadow-light: 0 0 0 var(--color-red), 0 0 0 var(--color-green); + --shadow-dark: 0 0 0 var(--color-green), 0 0 0 var(--color-red); } `; /* end snapshot integration object values css/variables shadow should match snapshot with references */ @@ -209,8 +209,8 @@ snapshots["integration object values css/variables shadow should match snapshot" */ :root { - --shadow-light: #ff0000, #40bf40; - --shadow-dark: #40bf40, #ff0000; + --shadow-light: 0 0 0 #ff0000, 0 0 0 #40bf40; + --shadow-dark: 0 0 0 #40bf40, 0 0 0 #ff0000; } `; /* end snapshot integration object values css/variables shadow should match snapshot */ diff --git a/__integration__/objectValues.test.js b/__integration__/objectValues.test.js index 82fcafabb..6005b9989 100644 --- a/__integration__/objectValues.test.js +++ b/__integration__/objectValues.test.js @@ -98,22 +98,6 @@ describe('integration', async () => { ).toHexString(); }, }, - cssBorder: { - type: 'value', - transitive: true, - filter: (token) => token.path[0] === `border`, - transform: (token) => { - return `${token.value.width} ${token.value.style} ${token.value.color}`; - }, - }, - shadow: { - type: 'value', - transitive: true, - filter: (token) => token.type === 'shadow', - transform: (token) => { - return token.value.map((obj) => obj.color).join(', '); - }, - }, }, }, platforms: { @@ -141,7 +125,7 @@ describe('integration', async () => { // transformed to a hex color works with and without `outputReferences` cssHex: { buildPath, - transforms: StyleDictionary.hooks.transformGroups.css.concat([`cssBorder`, `hslToHex`]), + transforms: StyleDictionary.hooks.transformGroups.css.concat([`hslToHex`]), files: [ { destination: 'hex.css', @@ -161,7 +145,7 @@ describe('integration', async () => { // works with and without `outputReferences` cssBorder: { buildPath, - transforms: StyleDictionary.hooks.transformGroups.css.concat([`cssBorder`]), + transforms: StyleDictionary.hooks.transformGroups.css, files: [ { destination: 'border.css', @@ -176,10 +160,9 @@ describe('integration', async () => { }, ], }, - cssShadow: { buildPath, - transforms: StyleDictionary.hooks.transformGroups.css.concat([`shadow`, `hslToHex`]), + transforms: StyleDictionary.hooks.transformGroups.css.concat([`hslToHex`]), files: [ { destination: 'shadow.css', @@ -197,7 +180,7 @@ describe('integration', async () => { scss: { buildPath, - transforms: StyleDictionary.hooks.transformGroups.css.concat([`cssBorder`, `hslToHex`]), + transforms: StyleDictionary.hooks.transformGroups.css.concat([`hslToHex`]), files: [ { destination: 'border.scss', diff --git a/__tests__/__helpers.js b/__tests__/__helpers.js index 8542d1393..4c54c8bc2 100644 --- a/__tests__/__helpers.js +++ b/__tests__/__helpers.js @@ -15,6 +15,20 @@ import { expect } from 'chai'; import { fs } from 'style-dictionary/fs'; import { resolve } from '../lib/resolve.js'; +export const cleanConsoleOutput = (str) => { + const arr = str + .split(`\n`) + // Remove ANSI stuff from the console output so we get human-readable strings + // https://github.com/chalk/ansi-regex/blob/main/index.js#L3 + .map((s) => + s + // eslint-disable-next-line no-control-regex + .replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') + .trim(), + ); + return arr.join(`\n`); +}; + export const expectThrowsAsync = async (method, errorMessage) => { let error = null; try { diff --git a/__tests__/common/transforms.test.js b/__tests__/common/transforms.test.js index dd1802b0a..44fc7f221 100644 --- a/__tests__/common/transforms.test.js +++ b/__tests__/common/transforms.test.js @@ -1048,6 +1048,249 @@ describe('common', () => { }); }); + describe('fontFamily/css', () => { + const fontFamilyTransform = (value) => + transforms['fontFamily/css'].transform({ value }, {}, {}); + + it('should handle simple fontFamily as is', () => { + expect(fontFamilyTransform('Arial')).to.equal('Arial'); + }); + + it('should comma separated type fontFamily values', () => { + expect(fontFamilyTransform('Arial, sans-serif')).to.equal('Arial, sans-serif'); + }); + + it('should handle array type fontFamily values', () => { + expect(fontFamilyTransform(['Arial', 'sans-serif'])).to.equal('Arial, sans-serif'); + }); + + it('should wrap fontFamily values with spaces in quotes', () => { + expect(fontFamilyTransform('Gill Sans')).to.equal("'Gill Sans'"); + expect(fontFamilyTransform('Gill Sans, Arial, Comic Sans, Open Sans, sans-serif')).to.equal( + "'Gill Sans', Arial, 'Comic Sans', 'Open Sans', sans-serif", + ); + expect( + fontFamilyTransform(['Gill Sans', 'Arial', 'Comic Sans', 'Open Sans', 'sans-serif']), + ).to.equal("'Gill Sans', Arial, 'Comic Sans', 'Open Sans', sans-serif"); + }); + }); + + describe('cubicBezier/css', () => { + const cubicBezierTransform = (value) => + transforms['cubicBezier/css'].transform({ value }, {}, {}); + + it('should stringify cubicBezier values to cubicBezier() CSS function', () => { + expect(cubicBezierTransform([0.5, 0, 1, 1])).to.equal('cubic-bezier(0.5, 0, 1, 1)'); + expect('ease-in-out').to.equal('ease-in-out'); + }); + }); + + describe('typography/css/shorthand', () => { + const typographyTransform = (value, platformConfig = {}) => + transforms['typography/css/shorthand'].transform({ value }, platformConfig, {}); + + it('transforms typography object to typography shorthand', () => { + expect( + typographyTransform({ + fontWeight: '500', + fontSize: '20px', + fontVariant: 'small-caps', + fontWidth: 'condensed', + fontStyle: 'italic', + lineHeight: '1.5', + fontFamily: 'Arial', + }), + ).to.equal('italic small-caps 500 condensed 20px/1.5 Arial'); + }); + + it('transforms fontWeight prop according to fontweight map for CSS and px dimensions', () => { + expect( + typographyTransform({ + fontWeight: 300, + fontSize: '20px', + lineHeight: '1.5', + fontFamily: 'Arial', + }), + ).to.equal('300 20px/1.5 Arial'); + }); + + it('provides defaults for missing properties', () => { + expect(typographyTransform({})).to.equal('16px sans-serif'); + expect(typographyTransform({}, { basePxFontSize: 12 })).to.equal('12px sans-serif'); + }); + + it('sets quotes around fontFamily if it has white-spaces in name', () => { + expect( + typographyTransform({ + fontWeight: 300, + fontSize: '20px', + lineHeight: '1.5', + fontFamily: 'Arial Narrow, Arial, sans-serif', + }), + ).to.equal("300 20px/1.5 'Arial Narrow', Arial, sans-serif"); + }); + + it('handles array fontFamily values', () => { + expect( + typographyTransform({ + fontWeight: 300, + fontSize: '20px', + lineHeight: '1.5', + fontFamily: ['Arial Narrow', 'Arial', 'sans-serif'], + }), + ).to.equal("300 20px/1.5 'Arial Narrow', Arial, sans-serif"); + }); + }); + + // https://design-tokens.github.io/community-group/format/#border + describe('border/css/shorthand', () => { + const borderTransform = (value) => + transforms['border/css/shorthand'].transform({ value, type: 'border' }, {}, {}); + + it('transforms border object to border shorthand', () => { + expect( + borderTransform({ + width: '5px', + style: 'dashed', + color: '#000000', + }), + ).to.equal('5px dashed #000000'); + }); + + // https://design-tokens.github.io/community-group/format/#example-fallback-for-object-stroke-style + it('handles stroke style of type object using dashed fallback', () => { + expect( + borderTransform({ + width: '5px', + style: { + dashArray: ['0.5rem', '0.25rem'], + lineCap: 'round', + }, + color: '#000000', + }), + ).to.equal('5px dashed #000000'); + }); + }); + + describe('strokeStyle/css/shorthand', () => { + const strokeTransform = (value, platformConfig = {}) => + transforms['strokeStyle/css/shorthand'].transform({ value }, platformConfig, {}); + + it('transforms strokeStyle object value to strokeStyle CSS fallback string value', () => { + expect( + strokeTransform({ + dashArray: ['0.5rem', '0.25rem'], + lineCap: 'round', + }), + ).to.equal('dashed'); + + expect(strokeTransform('dotted')).to.equal('dotted'); + }); + }); + + describe('transition/css/shorthand', () => { + const transitionTransform = (value, platformConfig = {}) => + transforms['transition/css/shorthand'].transform({ value }, platformConfig, {}); + + it('transforms transition object value to transition CSS shorthand string value', () => { + expect( + transitionTransform({ + duration: '200ms', + delay: '0ms', + timingFunction: [0.5, 0, 1, 1], + }), + ).to.equal('200ms cubic-bezier(0.5, 0, 1, 1) 0ms'); + + expect( + transitionTransform({ + duration: '200ms', + delay: '0ms', + timingFunction: 'ease-in-out', + }), + ).to.equal('200ms ease-in-out 0ms'); + + expect(transitionTransform('200ms linear 50ms')).to.equal('200ms linear 50ms'); + }); + }); + + describe('shadow/css/shorthand', () => { + const shadowTransform = (value, platformConfig = {}) => + transforms['shadow/css/shorthand'].transform({ value }, platformConfig, {}); + + it('transforms shadow object value to shadow CSS shorthand string value', () => { + expect( + shadowTransform({ + type: 'inset', + color: '#00000080', + offsetX: '4px', + offsetY: '4px', + blur: '12px', + spread: '6px', + }), + ).to.equal('inset 4px 4px 12px 6px #00000080'); + + expect(shadowTransform('4px 4px 12px 6px #00000080')).to.equal( + '4px 4px 12px 6px #00000080', + ); + }); + + it('transforms shadow object value with missing properties using defaults', () => { + expect(shadowTransform({})).to.equal('0 0 0 #000000'); + }); + + it('handles arrays of shadows', () => { + expect( + shadowTransform([ + { + type: 'inset', + color: '#000000', + offsetX: '4px', + offsetY: '4px', + blur: '12px', + spread: '6px', + }, + { + color: 'rgba(0,0,0, 0.4)', + offsetX: '2px', + offsetY: '2px', + blur: '4px', + }, + ]), + ).to.equal('inset 4px 4px 12px 6px #000000, 2px 2px 4px rgba(0,0,0, 0.4)'); + }); + }); + + /** + * The spec for gradient type tokens is not very well thought out at this moment + * https://design-tokens.github.io/community-group/format/#gradient + * This will inevitably change in a breaking manner, so any transform written as of the time of writing (13-05-24) + * will require a breaking change when it does. + * Therefore, a community-built custom transform is the better fit for now. + */ + describe.skip('gradient/css/shorthand', () => { + const gradientTransform = (value, platformConfig = {}) => + transforms['gradient/css/shorthand'].transform({ value }, platformConfig, {}); + + it('transforms gradient object value to gradient CSS shorthand string value', () => { + expect( + gradientTransform([ + { + color: '#0000ff', + position: 0, + }, + { + color: '#ff0000', + position: 1, + }, + ]), + ).to.equal('inset 4px 4px 12px 6px #000000, 2px 2px 4px rgba(0,0,0, 0.4)'); + + expect(gradientTransform('4px 4px 12px 6px #00000080')).to.equal( + '4px 4px 12px 6px #00000080', + ); + }); + }); + // FIXME: find a browser/node cross compatible way to transform local path // current implementation incorrectly uses process.cwd() rather than using // the filePath of the token to determine where the asset is located relative to the token that refers to it diff --git a/__tests__/exportPlatform.test.js b/__tests__/exportPlatform.test.js index fbb505467..859fd6ad4 100644 --- a/__tests__/exportPlatform.test.js +++ b/__tests__/exportPlatform.test.js @@ -11,15 +11,17 @@ * and limitations under the License. */ import { expect } from 'chai'; +import { stubMethod, restore } from 'hanbi'; import StyleDictionary from 'style-dictionary'; import { usesReferences } from 'style-dictionary/utils'; -import { fileToJSON } from './__helpers.js'; +import { fileToJSON, cleanConsoleOutput } from './__helpers.js'; const config = fileToJSON('__tests__/__configs/test.json'); describe('exportPlatform', () => { let styleDictionary; beforeEach(async () => { + restore(); styleDictionary = new StyleDictionary(config); await styleDictionary.hasInitialized; }); @@ -447,6 +449,312 @@ Use log.verbosity "verbose" or use CLI option --verbose for more details.`; }); }); + describe('CSS shorthand transforms integration', () => { + describe('typography', () => { + it('should warn the user about CSS Font shorthand unknown properties', async () => { + const logStub = stubMethod(console, 'log'); + const sd = new StyleDictionary({ + tokens: { + foo: { + bar: { + value: { + fontWeight: '500', + fontSize: '20px', + letterSpacing: 'normal', + paragraphSpacing: '20px', + textColor: '#000000', + }, + type: 'typography', + }, + }, + }, + platforms: { + css: { + transforms: ['typography/css/shorthand'], + }, + }, + }); + + await sd.exportPlatform('css'); + console.warn(cleanConsoleOutput(logStub.firstCall.args[0])); + + expect(cleanConsoleOutput(logStub.firstCall.args[0])).to.equal(` +Unknown CSS Font Shorthand properties found for 1 tokens, CSS output for Font values will be missing some typography token properties as a result: +Use log.verbosity "verbose" or use CLI option --verbose for more details. +`); + + sd.log.verbosity = 'verbose'; + await sd.exportPlatform('css'); + + expect(cleanConsoleOutput(Array.from(logStub.calls)[1].args[0])).to.equal(` +Unknown CSS Font Shorthand properties found for 1 tokens, CSS output for Font values will be missing some typography token properties as a result: + +letterSpacing, paragraphSpacing, textColor for token at foo.bar +`); + + sd.tokens.foo.bar.filePath = '/tokens.json'; + await sd.exportPlatform('css'); + + expect(cleanConsoleOutput(Array.from(logStub.calls)[2].args[0])).to.equal(` +Unknown CSS Font Shorthand properties found for 1 tokens, CSS output for Font values will be missing some typography token properties as a result: + +letterSpacing, paragraphSpacing, textColor for token at foo.bar in /tokens.json +`); + + sd.log.verbosity = 'silent'; + await sd.exportPlatform('css'); + expect(Array.from(logStub.calls)[3]).to.be.undefined; + + sd.log.verbosity = 'default'; + sd.log.warnings = 'disabled'; + await sd.exportPlatform('css'); + expect(Array.from(logStub.calls)[3]).to.be.undefined; + + sd.log.warnings = 'error'; + await expect(sd.exportPlatform('css')).to.be.eventually.rejectedWith(` +Unknown CSS Font Shorthand properties found for 1 tokens, CSS output for Font values will be missing some typography token properties as a result: +Use log.verbosity "verbose" or use CLI option --verbose for more details. +`); + }); + + it('should properly transform typography tokens that are references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: '{foo}', + type: 'typography', + }, + foo: { + value: { + fontWeight: '500', + fontSize: '20px', + letterSpacing: 'normal', + paragraphSpacing: '20px', + textColor: '#000000', + }, + type: 'typography', + }, + }, + platforms: { + css: { + transforms: ['typography/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.bar.value).to.equal('500 20px sans-serif'); + }); + + it('should properly transform typography tokens that contain references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: '500', + type: 'fontWeight', + }, + foo: { + value: { + fontWeight: '{bar}', + fontSize: '20px', + letterSpacing: 'normal', + paragraphSpacing: '20px', + textColor: '#000000', + }, + type: 'typography', + }, + }, + platforms: { + css: { + transforms: ['typography/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.foo.value).to.equal('500 20px sans-serif'); + }); + }); + + describe('border', () => { + it('should properly transform typography tokens that are references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: '{foo}', + type: 'border', + }, + foo: { + value: { + style: 'dashed', + color: '#000', + width: '2px', + }, + type: 'border', + }, + }, + platforms: { + css: { + transforms: ['border/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.bar.value).to.equal('2px dashed #000'); + }); + + it('should properly transform typography tokens that contain references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: 'dashed', + type: 'strokeStyle', + }, + foo: { + value: { + style: '{bar}', + color: '#000', + width: '2px', + }, + type: 'border', + }, + }, + platforms: { + css: { + transforms: ['border/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.foo.value).to.equal('2px dashed #000'); + }); + }); + + describe('transition', () => { + it('should properly transform transition tokens that are references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: '{foo}', + type: 'transition', + }, + foo: { + value: { + duration: '200ms', + delay: '0ms', + timingFunction: [0.5, 0, 1, 1], + }, + type: 'transition', + }, + }, + platforms: { + css: { + transforms: ['transition/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.bar.value).to.equal('200ms cubic-bezier(0.5, 0, 1, 1) 0ms'); + }); + + it('should properly transform transition tokens that contain references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: [0.5, 0, 1, 1], + type: 'cubicBezier', + }, + foo: { + value: { + duration: '200ms', + delay: '0ms', + timingFunction: '{bar}', + }, + type: 'transition', + }, + }, + platforms: { + css: { + transforms: ['transition/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.foo.value).to.equal('200ms cubic-bezier(0.5, 0, 1, 1) 0ms'); + }); + }); + + describe('shadow', () => { + it('should properly transform typography tokens that are references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: '{foo}', + type: 'shadow', + }, + foo: { + value: { + offsetX: '2px', + offsetY: '4px', + blur: '10px', + color: '#000', + }, + type: 'shadow', + }, + }, + platforms: { + css: { + transforms: ['shadow/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.bar.value).to.equal('2px 4px 10px #000'); + }); + + it('should properly transform typography tokens that contain references', async () => { + const sd = new StyleDictionary({ + tokens: { + bar: { + value: '#000', + type: 'color', + }, + foo: { + value: [ + { + offsetX: '2px', + offsetY: '4px', + blur: '10px', + color: '{bar}', + }, + { + offsetX: '4px', + offsetY: '8px', + blur: '12px', + color: '{bar}', + }, + ], + type: 'shadow', + }, + }, + platforms: { + css: { + transforms: ['shadow/css/shorthand'], + }, + }, + }); + + const transformed = await sd.exportPlatform('css'); + expect(transformed.foo.value).to.equal('2px 4px 10px #000, 4px 8px 12px #000'); + }); + }); + }); + describe('DTCG forward compatibility', () => { it('should allow using $value instead of value', async () => { const sd = new StyleDictionary({ diff --git a/docs/src/content/docs/reference/Hooks/Transform Groups/predefined.md b/docs/src/content/docs/reference/Hooks/Transform Groups/predefined.md index d9318c55f..2479137b2 100644 --- a/docs/src/content/docs/reference/Hooks/Transform Groups/predefined.md +++ b/docs/src/content/docs/reference/Hooks/Transform Groups/predefined.md @@ -40,6 +40,13 @@ Transforms: [size/rem](/reference/hooks/transforms/predefined#sizerem) [color/css](/reference/hooks/transforms/predefined#colorcss) [asset/url](/reference/hooks/transforms/predefined#asseturl) +[fontFamily/css](/reference/hooks/transforms/predefined#fontfamilycss) +[cubicBezier/css](/reference/hooks/transforms/predefined#cubicbeziercss) +[strokeStyle/css/shorthand](/reference/hooks/transforms/predefined#strokestylecssshorthand) +[border/css/shorthand](/reference/hooks/transforms/predefined#bordercssshorthand) +[typography/css/shorthand](/reference/hooks/transforms/predefined#typographycssshorthand) +[transition/css/shorthand](/reference/hooks/transforms/predefined#transitioncssshorthand) +[shadow/css/shorthand](/reference/hooks/transforms/predefined#shadowcssshorthand) --- @@ -54,6 +61,13 @@ Transforms: [size/rem](/reference/hooks/transforms/predefined#sizerem) [color/css](/reference/hooks/transforms/predefined#colorcss) [asset/url](/reference/hooks/transforms/predefined#asseturl) +[fontFamily/css](/reference/hooks/transforms/predefined#fontfamilycss) +[cubicBezier/css](/reference/hooks/transforms/predefined#cubicbeziercss) +[strokeStyle/css/shorthand](/reference/hooks/transforms/predefined#strokestylecssshorthand) +[border/css/shorthand](/reference/hooks/transforms/predefined#bordercssshorthand) +[typography/css/shorthand](/reference/hooks/transforms/predefined#typographycssshorthand) +[transition/css/shorthand](/reference/hooks/transforms/predefined#transitioncssshorthand) +[shadow/css/shorthand](/reference/hooks/transforms/predefined#shadowcssshorthand) --- @@ -68,6 +82,13 @@ Transforms: [size/rem](/reference/hooks/transforms/predefined#sizerem) [color/hex](/reference/hooks/transforms/predefined#colorhex) [asset/url](/reference/hooks/transforms/predefined#asseturl) +[fontFamily/css](/reference/hooks/transforms/predefined#fontfamilycss) +[cubicBezier/css](/reference/hooks/transforms/predefined#cubicbeziercss) +[strokeStyle/css/shorthand](/reference/hooks/transforms/predefined#strokestylecssshorthand) +[border/css/shorthand](/reference/hooks/transforms/predefined#bordercssshorthand) +[typography/css/shorthand](/reference/hooks/transforms/predefined#typographycssshorthand) +[transition/css/shorthand](/reference/hooks/transforms/predefined#transitioncssshorthand) +[shadow/css/shorthand](/reference/hooks/transforms/predefined#shadowcssshorthand) --- diff --git a/docs/src/content/docs/reference/Hooks/Transforms/predefined.md b/docs/src/content/docs/reference/Hooks/Transforms/predefined.md index 5dcc646bb..6823a7231 100644 --- a/docs/src/content/docs/reference/Hooks/Transforms/predefined.md +++ b/docs/src/content/docs/reference/Hooks/Transforms/predefined.md @@ -516,6 +516,132 @@ Assumes a time in miliseconds and transforms it into a decimal --- +### fontFamily/css + +Transforms `fontFamily` type token (which can be an array) into a CSS string, putting single quotes around font names that contain spaces where necessary. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#font-family) + +```css +/** + * Matches: token.type === 'fontFamily' + * Returns: + */ +:root { + --var: 'Arial Black', Helvetica, sans-serif; +} +``` + +--- + +### cubicBezier/css + +Transforms `cubicBezier` type token into a CSS string, using the CSS `cubic-bezier` function. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#cubic-bezier) + +```css +/** + * Matches: token.type === 'cubicBezier' + * Returns: + */ +:root { + --var: cubic-bezier(0, 0, 0.5, 1); +} +``` + +--- + +### strokeStyle/css/shorthand + +Transforms `strokeStyle` type object-value token into a CSS string, using the CSS `dashed` fallback. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#stroke-style) + +```css +/** + * Matches: token.type === 'strokeStyle' + * Returns: + */ +:root { + --var: dashed; +} +``` + +--- + +### border/css/shorthand + +Transforms `border` type object-value token into a CSS string, using the CSS `border` shorthand notation. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#border) + +```css +/** + * Matches: token.type === 'border' + * Returns: + */ +:root { + --var: 2px solid #000000; +} +``` + +--- + +### typography/css/shorthand + +Transforms `typography` type object-value token into a CSS string, using the CSS `font` shorthand notation. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#typography) + +```css +/** + * Matches: token.type === 'typography' + * Returns: + */ +:root { + --var: italic 400 1.2rem/1.5 'Fira Sans', sans-serif; +} +``` + +--- + +### transition/css/shorthand + +Transforms `transition` type object-value token into a CSS string, using the CSS `transition` shorthand notation. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#transition) + +```css +/** + * Matches: token.type === 'transition' + * Returns: + */ +:root { + --var: 200ms ease-in-out 50ms; +} +``` + +--- + +### shadow/css/shorthand + +Transforms `shadow` type object-value token (which can also be an array) into a CSS string, using the CSS `shadow` shorthand notation. + +[DTCG definition](https://design-tokens.github.io/community-group/format/#shadow) + +```css +/** + * Matches: token.type === 'shadow' + * Returns: + */ +:root { + --var: 2px 4px 8px 10px #000000, 1px 1px 4px #cccccc; +} +``` + +--- + ### asset/url Wraps the value in a [CSS `url()` function](https://developer.mozilla.org/en-US/docs/Web/CSS/url) diff --git a/lib/StyleDictionary.js b/lib/StyleDictionary.js index c1a531e04..9909ea414 100644 --- a/lib/StyleDictionary.js +++ b/lib/StyleDictionary.js @@ -54,6 +54,7 @@ import { expandTokens } from './utils/expandObjectTokens.js'; const PROPERTY_VALUE_COLLISIONS = GroupMessages.GROUP.PropertyValueCollisions; const PROPERTY_REFERENCE_WARNINGS = GroupMessages.GROUP.PropertyReferenceWarnings; +const UNKNOWN_CSS_FONT_PROPS_WARNINGS = GroupMessages.GROUP.UnknownCSSFontProperties; /** * Style Dictionary module @@ -494,6 +495,25 @@ export default class StyleDictionary extends Register { throw new Error(err); } + const unknownPropsWarningCount = GroupMessages.count(UNKNOWN_CSS_FONT_PROPS_WARNINGS); + if (unknownPropsWarningCount > 0) { + const warnings = GroupMessages.flush(UNKNOWN_CSS_FONT_PROPS_WARNINGS).join('\n'); + let err = `\nUnknown CSS Font Shorthand properties found for ${unknownPropsWarningCount} tokens, CSS output for Font values will be missing some typography token properties as a result:\n`; + + if (this.log.verbosity === 'verbose') { + err += `\n${warnings}\n`; + } else { + err += `${verbosityInfo}\n`; + } + + if (this.log.warnings === 'error') { + throw new Error(err); + } else if (this.log.warnings !== 'disabled' && this.log.verbosity !== 'silent') { + // eslint-disable-next-line no-console + console.log(chalk.rgb(255, 140, 0).bold(err)); + } + } + return exportableResult; } diff --git a/lib/common/transformGroups.js b/lib/common/transformGroups.js index bd35ed106..27e509d5b 100644 --- a/lib/common/transformGroups.js +++ b/lib/common/transformGroups.js @@ -22,10 +22,10 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/kebab](/reference/hooks/transforms#namectikebab) - * [size/px](/reference/hooks/transforms#sizepx) - * [color/css](/reference/hooks/transforms#colorcss) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/kebab](/reference/hooks/transforms/predefined#namectikebab) + * [size/px](/reference/hooks/transforms/predefined#sizepx) + * [color/css](/reference/hooks/transforms/predefined#colorcss) * * @memberof TransformGroups */ @@ -34,10 +34,10 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/pascal](/reference/hooks/transforms#namectipascal) - * [size/rem](/reference/hooks/transforms#sizerem) - * [color/hex](/reference/hooks/transforms#colorhex) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/pascal](/reference/hooks/transforms/predefined#namectipascal) + * [size/rem](/reference/hooks/transforms/predefined#sizerem) + * [color/hex](/reference/hooks/transforms/predefined#colorhex) * * @memberof TransformGroups */ @@ -46,13 +46,20 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/kebab](/reference/hooks/transforms#namectikebab) - * [time/seconds](/reference/hooks/transforms#timeseconds) - * [html/icon](/reference/hooks/transforms#htmlicon) - * [size/rem](/reference/hooks/transforms#sizerem) - * [color/css](/reference/hooks/transforms#colorcss) - * [asset/url](/reference/hooks/transforms#asset/url) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/kebab](/reference/hooks/transforms/predefined#namectikebab) + * [time/seconds](/reference/hooks/transforms/predefined#timeseconds) + * [html/icon](/reference/hooks/transforms/predefined#htmlicon) + * [size/rem](/reference/hooks/transforms/predefined#sizerem) + * [color/css](/reference/hooks/transforms/predefined#colorcss) + * [asset/url](/reference/hooks/transforms/predefined#asset/url) + * [fontFamily/css](/reference/hooks/transforms/predefined#fontfamilycss) + * [cubicBezier/css](/reference/hooks/transforms/predefined#cubicbezier) + * [strokeStyle/css/shorthand](/reference/hooks/transforms/predefined#strokestylecssshorthand) + * [border/css/shorthand](/reference/hooks/transforms/predefined#bordercssshorthand) + * [typography/css/shorthand](/reference/hooks/transforms/predefined#typographycssshorthand) + * [transition/css/shorthand](/reference/hooks/transforms/predefined#transitioncssshorthand) + * [shadow/css/shorthand](/reference/hooks/transforms/predefined#shadowcssshorthand) * * @memberof TransformGroups */ @@ -64,18 +71,33 @@ export default { 'size/rem', 'color/css', 'asset/url', + 'fontFamily/css', + 'cubicBezier/css', + // object-value tokens + 'strokeStyle/css/shorthand', + 'border/css/shorthand', + 'typography/css/shorthand', + 'transition/css/shorthand', + 'shadow/css/shorthand', ], /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/kebab](/reference/hooks/transforms#namectikebab) - * [time/seconds](/reference/hooks/transforms#timeseconds) - * [html/icon](/reference/hooks/transforms#htmlicon) - * [size/rem](/reference/hooks/transforms#sizerem) - * [color/css](/reference/hooks/transforms#colorcss) - * [asset/url](/reference/hooks/transforms#asset/url) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/kebab](/reference/hooks/transforms/predefined#namectikebab) + * [time/seconds](/reference/hooks/transforms/predefined#timeseconds) + * [html/icon](/reference/hooks/transforms/predefined#htmlicon) + * [size/rem](/reference/hooks/transforms/predefined#sizerem) + * [color/css](/reference/hooks/transforms/predefined#colorcss) + * [asset/url](/reference/hooks/transforms/predefined#asset/url) + * [fontFamily/css](/reference/hooks/transforms/predefined#fontfamilycss) + * [cubicBezier/css](/reference/hooks/transforms/predefined#cubicbezier) + * [strokeStyle/css/shorthand](/reference/hooks/transforms/predefined#strokestylecssshorthand) + * [border/css/shorthand](/reference/hooks/transforms/predefined#bordercssshorthand) + * [typography/css/shorthand](/reference/hooks/transforms/predefined#typographycssshorthand) + * [transition/css/shorthand](/reference/hooks/transforms/predefined#transitioncssshorthand) + * [shadow/css/shorthand](/reference/hooks/transforms/predefined#shadowcssshorthand) * * @memberof TransformGroups */ @@ -87,18 +109,33 @@ export default { 'size/rem', 'color/css', 'asset/url', + 'fontFamily/css', + 'cubicBezier/css', + // object-value tokens + 'strokeStyle/css/shorthand', + 'border/css/shorthand', + 'typography/css/shorthand', + 'transition/css/shorthand', + 'shadow/css/shorthand', ], /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/kebab](/reference/hooks/transforms#namectikebab) - * [time/seconds](/reference/hooks/transforms#timeseconds) - * [html/icon](/reference/hooks/transforms#htmlicon) - * [size/rem](/reference/hooks/transforms#sizerem) - * [color/hex](/reference/hooks/transforms#colorhex) - * [asset/url](/reference/hooks/transforms#asset/url) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/kebab](/reference/hooks/transforms/predefined#namectikebab) + * [time/seconds](/reference/hooks/transforms/predefined#timeseconds) + * [html/icon](/reference/hooks/transforms/predefined#htmlicon) + * [size/rem](/reference/hooks/transforms/predefined#sizerem) + * [color/hex](/reference/hooks/transforms/predefined#colorhex) + * [asset/url](/reference/hooks/transforms/predefined#asset/url) + * [fontFamily/css](/reference/hooks/transforms/predefined#fontfamilycss) + * [cubicBezier/css](/reference/hooks/transforms/predefined#cubicbezier) + * [strokeStyle/css/shorthand](/reference/hooks/transforms/predefined#strokestylecssshorthand) + * [border/css/shorthand](/reference/hooks/transforms/predefined#bordercssshorthand) + * [typography/css/shorthand](/reference/hooks/transforms/predefined#typographycssshorthand) + * [transition/css/shorthand](/reference/hooks/transforms/predefined#transitioncssshorthand) + * [shadow/css/shorthand](/reference/hooks/transforms/predefined#shadowcssshorthand) * * @memberof TransformGroups */ @@ -110,14 +147,22 @@ export default { 'size/rem', 'color/hex', 'asset/url', + 'fontFamily/css', + 'cubicBezier/css', + // object-value tokens + 'strokeStyle/css/shorthand', + 'border/css/shorthand', + 'typography/css/shorthand', + 'transition/css/shorthand', + 'shadow/css/shorthand', ], /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [attribute/color](/reference/hooks/transforms#attributecolor) - * [name/human](/reference/hooks/transforms#namehuman) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [attribute/color](/reference/hooks/transforms/predefined#attributecolor) + * [name/human](/reference/hooks/transforms/predefined#namehuman) * * @memberof TransformGroups */ @@ -125,11 +170,11 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/snake](/reference/hooks/transforms#namectisnake) - * [color/hex8android](/reference/hooks/transforms#colorhex8android) - * [size/remToSp](/reference/hooks/transforms#sizeremtosp) - * [size/remToDp](/reference/hooks/transforms#sizeremtodp) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/snake](/reference/hooks/transforms/predefined#namectisnake) + * [color/hex8android](/reference/hooks/transforms/predefined#colorhex8android) + * [size/remToSp](/reference/hooks/transforms/predefined#sizeremtosp) + * [size/remToDp](/reference/hooks/transforms/predefined#sizeremtodp) * * @memberof TransformGroups */ @@ -137,12 +182,12 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/camel](/reference/hooks/transforms#namecamel) - * [color/composeColor](/reference/hooks/transforms#colorcomposecolor) - * [size/compose/em](/reference/hooks/transforms#sizecomposeem) - * [size/compose/remToSp](/reference/hooks/transforms#sizecomposeremtosp) - * [size/compose/remToDp](/reference/hooks/transforms#sizecomposeremtodp) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/camel](/reference/hooks/transforms/predefined#namecamel) + * [color/composeColor](/reference/hooks/transforms/predefined#colorcomposecolor) + * [size/compose/em](/reference/hooks/transforms/predefined#sizecomposeem) + * [size/compose/remToSp](/reference/hooks/transforms/predefined#sizecomposeremtosp) + * [size/compose/remToDp](/reference/hooks/transforms/predefined#sizecomposeremtodp) * * @memberof TransformGroups */ @@ -157,12 +202,12 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/pascal](/reference/hooks/transforms#namectipascal) - * [color/UIColor](/reference/hooks/transforms#coloruicolor) - * [content/objC/literal](/reference/hooks/transforms#contentobjcliteral) - * [asset/objC/literal](/reference/hooks/transforms#assetobjcliteral) - * [size/remToPt](/reference/hooks/transforms#sizeremtopt) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/pascal](/reference/hooks/transforms/predefined#namectipascal) + * [color/UIColor](/reference/hooks/transforms/predefined#coloruicolor) + * [content/objC/literal](/reference/hooks/transforms/predefined#contentobjcliteral) + * [asset/objC/literal](/reference/hooks/transforms/predefined#assetobjcliteral) + * [size/remToPt](/reference/hooks/transforms/predefined#sizeremtopt) * * @memberof TransformGroups */ @@ -177,12 +222,12 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/camel](/reference/hooks/transforms#namecamel) - * [color/UIColorSwift](/reference/hooks/transforms#coloruicolorswift) - * [content/swift/literal](/reference/hooks/transforms#contentswiftliteral) - * [asset/swift/literal](/reference/hooks/transforms#assetswiftliteral) - * [size/swift/remToCGFloat](/reference/hooks/transforms#sizeswiftremtocgfloat) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/camel](/reference/hooks/transforms/predefined#namecamel) + * [color/UIColorSwift](/reference/hooks/transforms/predefined#coloruicolorswift) + * [content/swift/literal](/reference/hooks/transforms/predefined#contentswiftliteral) + * [asset/swift/literal](/reference/hooks/transforms/predefined#assetswiftliteral) + * [size/swift/remToCGFloat](/reference/hooks/transforms/predefined#sizeswiftremtocgfloat) * * @memberof TransformGroups */ @@ -197,12 +242,12 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/camel](/reference/hooks/transforms#namecamel) - * [color/UIColorSwift](/reference/hooks/transforms#coloruicolorswift) - * [content/swift/literal](/reference/hooks/transforms#contentswiftliteral) - * [asset/swift/literal](/reference/hooks/transforms#assetswiftliteral) - * [size/swift/remToCGFloat](/reference/hooks/transforms#sizeswiftremtocgfloat) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/camel](/reference/hooks/transforms/predefined#namecamel) + * [color/UIColorSwift](/reference/hooks/transforms/predefined#coloruicolorswift) + * [content/swift/literal](/reference/hooks/transforms/predefined#contentswiftliteral) + * [asset/swift/literal](/reference/hooks/transforms/predefined#assetswiftliteral) + * [size/swift/remToCGFloat](/reference/hooks/transforms/predefined#sizeswiftremtocgfloat) * * This is to be used if you want to have separate files per category and you don't want the category (e.g., color) as the lead value in the name of the token (e.g., StyleDictionaryColor.baseText instead of StyleDictionary.colorBaseText). * @@ -219,7 +264,7 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) * * @memberof TransformGroups */ @@ -227,12 +272,12 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/camel](/reference/hooks/transforms#nameccamel) - * [color/hex8flutter](/reference/hooks/transforms#colorhex8flutter) - * [size/flutter/remToDouble](/reference/hooks/transforms#sizeflutterremToDouble) - * [content/flutter/literal](/reference/hooks/transforms#contentflutterliteral) - * [asset/flutter/literal](/reference/hooks/transforms#assetflutterliteral) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/camel](/reference/hooks/transforms/predefined#nameccamel) + * [color/hex8flutter](/reference/hooks/transforms/predefined#colorhex8flutter) + * [size/flutter/remToDouble](/reference/hooks/transforms/predefined#sizeflutterremToDouble) + * [content/flutter/literal](/reference/hooks/transforms/predefined#contentflutterliteral) + * [asset/flutter/literal](/reference/hooks/transforms/predefined#assetflutterliteral) * * @memberof TransformGroups */ @@ -247,12 +292,12 @@ export default { /** * Transforms: * - * [attribute/cti](/reference/hooks/transforms#attributecti) - * [name/camel](/reference/hooks/transforms#namecamel) - * [color/hex8flutter](/reference/hooks/transforms#colorhex8flutter) - * [size/flutter/remToDouble](/reference/hooks/transforms#sizeflutterremToDouble) - * [content/flutter/literal](/reference/hooks/transforms#contentflutterliteral) - * [asset/flutter/literal](/reference/hooks/transforms#assetflutterliteral) + * [attribute/cti](/reference/hooks/transforms/predefined#attributecti) + * [name/camel](/reference/hooks/transforms/predefined#namecamel) + * [color/hex8flutter](/reference/hooks/transforms/predefined#colorhex8flutter) + * [size/flutter/remToDouble](/reference/hooks/transforms/predefined#sizeflutterremToDouble) + * [content/flutter/literal](/reference/hooks/transforms/predefined#contentflutterliteral) + * [asset/flutter/literal](/reference/hooks/transforms/predefined#assetflutterliteral) * * This is to be used if you want to have separate files per category and you don't want the category (e.g., color) as the lead value in the name of the token (e.g., StyleDictionaryColor.baseText instead of StyleDictionary.colorBaseText). * @@ -270,9 +315,9 @@ export default { /** * Transforms: * - * [name/camel](/reference/hooks/transforms#namecamel) - * [size/object](/reference/hooks/transforms#sizeobject) - * [color/css](/reference/hooks/transforms#colorcss) + * [name/camel](/reference/hooks/transforms/predefined#namecamel) + * [size/object](/reference/hooks/transforms/predefined#sizeobject) + * [color/css](/reference/hooks/transforms/predefined#colorcss) * * @memberof TransformGroups */ diff --git a/lib/common/transforms.js b/lib/common/transforms.js index 51ac18822..ce0d3da9a 100644 --- a/lib/common/transforms.js +++ b/lib/common/transforms.js @@ -15,6 +15,7 @@ import Color from 'tinycolor2'; import { join } from 'path-unified'; import { snakeCase, kebabCase, camelCase } from 'change-case'; import convertToBase64 from '../utils/convertToBase64.js'; +import GroupMessages from '../utils/groupMessages.js'; /** * @typedef {import('../../types/Transform.d.ts').Transform} Transform @@ -23,6 +24,8 @@ import convertToBase64 from '../utils/convertToBase64.js'; * @typedef {import('../../types/Config.d.ts').Config} Config */ +const UNKNOWN_CSS_FONT_PROPS_WARNINGS = GroupMessages.GROUP.UnknownCSSFontProperties; + /** * @param {string} str * @returns {string} @@ -110,6 +113,45 @@ function getBasePxFontSize(config) { return (config && config.basePxFontSize) || 16; } +/** + * @param {string} fontString + */ +function quoteWrapWhitespacedFont(fontString) { + let fontName = fontString.trim(); + const isQuoted = fontName.startsWith("'") && fontName.endsWith("'"); + if (!isQuoted) { + fontName = fontName.replace(/'/g, "\\'"); + } + const hasWhiteSpace = new RegExp('\\s+').test(fontName); + return hasWhiteSpace && !isQuoted ? `'${fontName}'` : fontName; +} + +/** + * @param {string|string[]} fontFamily + */ +function processFontFamily(fontFamily) { + let f = fontFamily; + if (typeof f === 'string' && f.includes(',')) { + f = f.split(',').map((part) => part.trim()); + } + + if (Array.isArray(f)) { + return f.map((part) => quoteWrapWhitespacedFont(part)).join(', '); + } + + return quoteWrapWhitespacedFont(f); +} + +/** + * @param {number[]|string} cubicBezier + */ +function transformCubicBezierCSS(cubicBezier) { + if (Array.isArray(cubicBezier)) { + return `cubic-bezier(${cubicBezier.join(', ')})`; + } + return cubicBezier; +} + /** * @namespace Transforms * @type {Record>} @@ -1015,6 +1057,245 @@ export default { }, }, + /** + * Turns fontFamily tokens into valid CSS string values + * https://design-tokens.github.io/community-group/format/#font-family + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family + * + * ```js + * // Matches: token.type === 'fontFamily' + * // Returns: + * "'Arial Narrow', Arial, sans-serif" + * ```. + * + * @memberof Transforms + */ + 'fontFamily/css': { + type: 'value', + filter: (token) => token.type === 'fontFamily', + transform: (token, _, options) => { + /** @type {string|string[]} */ + const fontFamily = options.usesDtcg ? token.$value : token.value; + + return processFontFamily(fontFamily); + }, + }, + + /** + * Turns fontFamily tokens into valid CSS string values + * https://design-tokens.github.io/community-group/format/#font-family + * https://developer.mozilla.org/en-US/docs/Web/CSS/font-family + * + * ```js + * // Matches: token.type === 'fontFamily' + * // Returns: + * "'Arial Narrow', Arial, sans-serif" + * ```. + * + * @memberof Transforms + */ + 'cubicBezier/css': { + type: 'value', + filter: (token) => token.type === 'cubicBezier', + transform: (token, _, options) => { + /** @type {number[]|string} */ + const cubicBezier = options.usesDtcg ? token.$value : token.value; + return transformCubicBezierCSS(cubicBezier); + }, + }, + + /** + * Turns strokeStyle object-value tokens into stringified CSS fallback + * https://design-tokens.github.io/community-group/format/#stroke-style + * https://design-tokens.github.io/community-group/format/#example-fallback-for-object-stroke-style + * CSS does not allow detailed control of the dash pattern or line caps on dashed borders, so we use dashed fallback + * ```js + * // Matches: token.type === 'border' + * // Returns: + * "dashed" + * ```. + * + * @memberof Transforms + */ + 'strokeStyle/css/shorthand': { + type: 'value', + // border properties can be references + transitive: true, + filter: (token) => token.type === 'strokeStyle', + transform: (token, _, options) => { + const val = options.usesDtcg ? token.$value : token.value; + if (typeof val !== 'object') { + // already transformed to string + return val; + } + return 'dashed'; + }, + }, + + /** + * Turns border tokens object-value into stringified CSS shorthand + * https://design-tokens.github.io/community-group/format/#border + * + * ```js + * // Matches: token.type === 'border' + * // Returns: + * "2px solid #000000" + * ```. + * + * @memberof Transforms + */ + 'border/css/shorthand': { + type: 'value', + // border properties can be references + transitive: true, + filter: (token) => token.type === 'border', + transform: (token, _, options) => { + const val = options.usesDtcg ? token.$value : token.value; + if (typeof val !== 'object') { + // already transformed to string + return val; + } + const { color, width } = val; + let { style } = val; + + // use fallback for style object value, since CSS does not allow + // detailed control of the dash pattern or line caps on dashed borders + // https://design-tokens.github.io/community-group/format/#example-fallback-for-object-stroke-style + if (typeof style === 'object') { + style = 'dashed'; + } + return `${width} ${style} ${color}`; + }, + }, + + /** + * Turns typography tokens object-value into stringified CSS shorthand + * https://design-tokens.github.io/community-group/format/#typography + * + * Available props within typography has been extended here + * to include those available in CSS font shorthand: + * https://developer.mozilla.org/en-US/docs/Web/CSS/font + * + * ```js + * // Matches: token.type === 'typography' + * // Returns: + * "500 20px/1.5 Arial" + * ```. + * + * @memberof Transforms + */ + 'typography/css/shorthand': { + type: 'value', + // typography properties can be references + transitive: true, + filter: (token) => token.type === 'typography', + transform: (token, platform, options) => { + const val = options.usesDtcg ? token.$value : token.value; + if (typeof val !== 'object') { + // already transformed to string + return val; + } + let { fontFamily } = val; + const { fontWeight, fontVariant, fontWidth, fontSize, fontStyle, lineHeight } = val; + + const CSSShorthandProps = [ + 'fontStyle', + 'fontVariant', + 'fontWeight', + 'fontWidth', + 'fontSize', + 'lineHeight', + 'fontFamily', + ]; + + const unknownProps = Object.keys(val).filter((key) => !CSSShorthandProps.includes(key)); + if (unknownProps.length > 0) { + GroupMessages.add( + UNKNOWN_CSS_FONT_PROPS_WARNINGS, + `${unknownProps.join(', ')} for token at ${token.path.join('.')}${ + token.filePath ? ` in ${token.filePath}` : '' + }`, + ); + } + + fontFamily = processFontFamily(fontFamily ?? 'sans-serif'); + + return `${fontStyle ? `${fontStyle} ` : ''}${fontVariant ? `${fontVariant} ` : ''}${ + fontWeight ? `${fontWeight} ` : '' + }${fontWidth ? `${fontWidth} ` : ''}${ + fontSize ? `${fontSize}` : `${getBasePxFontSize(platform)}px` + }${lineHeight ? `/${lineHeight} ` : ' '}${fontFamily}`; + }, + }, + + /** + * Turns transition tokens object-value into stringified CSS shorthand + * https://design-tokens.github.io/community-group/format/#border + * + * ```js + * // Matches: token.type === 'transition' + * // Returns: + * "200ms linear 50ms" + * ```. + * + * @memberof Transforms + */ + 'transition/css/shorthand': { + type: 'value', + // transition properties can be references + transitive: true, + filter: (token) => token.type === 'transition', + transform: (token, _, options) => { + const val = options.usesDtcg ? token.$value : token.value; + if (typeof val !== 'object') { + // already transformed to string + return val; + } + const { duration, delay, timingFunction } = val; + + return `${duration} ${transformCubicBezierCSS(timingFunction)} ${delay}`; + }, + }, + + /** + * Turns shadow tokens object-value into stringified CSS shorthand + * https://design-tokens.github.io/community-group/format/#shadow + * + * ```js + * // Matches: token.type === 'shadow' + * // Returns: + * "inset 2px 4px 10px 5px #000000" + * ```. + * + * @memberof Transforms + */ + 'shadow/css/shorthand': { + type: 'value', + // shadow properties can be references + transitive: true, + filter: (token) => token.type === 'shadow', + transform: (token, _, options) => { + const val = options.usesDtcg ? token.$value : token.value; + if (typeof val !== 'object') { + // already transformed to string + return val; + } + + /** @param {any} val */ + const stringifyShadow = (val) => { + const { type, color, offsetX, offsetY, blur, spread } = val; + return `${type ? `${type} ` : ''}${offsetX ?? 0} ${offsetY ?? 0} ${blur ?? 0} ${ + spread ? `${spread} ` : '' + }${color ?? `#000000`}`; + }; + + if (Array.isArray(val)) { + return val.map(stringifyShadow).join(', '); + } + return stringifyShadow(val); + }, + }, + /** * Wraps the value in a CSS url() function https://developer.mozilla.org/en-US/docs/Web/CSS/url * diff --git a/lib/utils/deepmerge.js b/lib/utils/deepmerge.js index 0e2e56c1b..5cfcd490e 100644 --- a/lib/utils/deepmerge.js +++ b/lib/utils/deepmerge.js @@ -17,8 +17,9 @@ import isPlainObject from 'is-plain-obj'; * Wrapper around deepmerge that merges only plain objects and arrays * @param {Object} target * @param {Object} source + * @param {boolean} [dedupeArrays] */ -export const deepmerge = (target, source) => { +export const deepmerge = (target, source, dedupeArrays = true) => { return _deepmerge(target, source, { /** * Merge if object is array or a plain object (so not merging functions/class instances together) @@ -30,6 +31,11 @@ export const deepmerge = (target, source) => { * @param {Array} target * @param {Array} source */ - arrayMerge: (target, source) => Array.from(new Set([...target, ...source])), + arrayMerge: (target, source) => { + return dedupeArrays && target.length > 0 && source.length > 0 + ? // if both arrays have values, dedupe them + Array.from(new Set([...target, ...source])) + : [...target, ...source]; + }, }); }; diff --git a/lib/utils/groupMessages.js b/lib/utils/groupMessages.js index 413ba9224..1ad8069f3 100644 --- a/lib/utils/groupMessages.js +++ b/lib/utils/groupMessages.js @@ -26,6 +26,7 @@ export class GroupMessages { MissingRegisterTransformErrors: 'Missing Register Transform Errors', PropertyNameCollisionWarnings: 'Property Name Collision Warnings', FilteredOutputReferences: 'Filtered Output Reference Warnings', + UnknownCSSFontProperties: 'Unknown CSS Font Shorthand Properties', }; }