Skip to content

Commit

Permalink
Merge pull request #75 from lukasoppermann/css-advanced-improvement
Browse files Browse the repository at this point in the history
update css-advanced
  • Loading branch information
lukasoppermann committed May 5, 2024
2 parents 4474ec5 + a1d8cd8 commit a9307be
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 69 deletions.
32 changes: 10 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,13 @@ The `css/advanced` format exports a token dictionary as a `css` file with css va

You can change the selector by defining it in `file.options.selector`.

You can define queries on a file level using `file.options.queries`. If one or more queries are defined, only tokens within any of the queries will be output. You can define as many query objects within `file.options.queries` as you want. Tokens can be part of one or multiple queries.
You can define rules on a file level using `file.options.rules`. If one or more rules are defined, only tokens within any of the rules will be output. You can define as many rule objects within `file.options.rules` as you want. Tokens can be part of one or multiple rules.

A query object has two properties, a `query` and a `matcher`.
A `query` is a string that is wrapped around your css and the selector, if present. If the `query` is undefined, only the `selector` will be wrapped around the matching tokens.
The `matcher` is a filter function that returns true for tokens that should be included in the query. If you want to match all tokens, just return true from the matcher.
A rule object may have any or all of the three properties `atRule`, `selector` and `matcher`.

- `selector` is a string that is wrapped around your css. If the `selector` is undefined, the default selector or one define at `file.options.selector` will be used. If you don't want a selector, set it to `false`.
- `atRule` can be a string or array of strings, that are wrapped around the css and `selector` with the first being the outer layer.
- `matcher` is a filter function that returns true for tokens that should be included in the query. If you want to match all tokens, just return true from the matcher.

```css
body[theme="dark"] {
Expand All @@ -267,12 +269,12 @@ const myStyleDictionary = StyleDictionary.extend({
// ...
"format": "css/advanced",
"options": {
queryExtensionProperty: 'org.YOURCOMPANY.mediaQuery' // defaults to mediaQuery
selector: `body[theme="dark"]`, // defaults to :root; set to false to disable
queries: [
rules: [
{
query: '@media (min-width: 768px)',
matcher: (token: StyleDictionary.TransformedToken) => token.filePath.includes('mobile'), // tokens that match this filter will be added inside the media query
atRule: '@media (min-width: 768px)',
selector: `body[size="medium"]` // this will be used instead of body[theme="dark"]`
matcher: (token: StyleDictionary.TransformedToken) => token.filePath.includes('tablet'), // tokens that match this filter will be added inside the media query
}]
}
}]
Expand All @@ -281,20 +283,6 @@ const myStyleDictionary = StyleDictionary.extend({
});
```

Instead of using matchers you can also add a property to every token that defines its media query. Both strategies can also be combined.

The property has to be added inside the `$extensions` property on the token.

```js
{
$type: 'color',
$value: '#FF0000',
$extensions: {
"org.YOURCOMPANY.mediaQuery": "@media (min-width: 768px)"
}
}
```

## 🤖 Transformers
Transforms change the `value` or `name` of a token.
You can use transforms by refering the name in the array value of the [`transforms` ](https://amzn.github.io/style-dictionary/#/transforms)property of a [`platform`](https://amzn.github.io/style-dictionary/#/config?id=platform).
Expand Down
107 changes: 81 additions & 26 deletions src/format/css-advanced.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,16 @@ describe('Format: CSS Advanced', () => {
destination: 'tokens.css',
options: {
showFileHeader: false,
queries: [{
query: '@media (prefers-color-scheme: dark)',
rules: [{
atRule: '@media (prefers-color-scheme: dark)',
matcher: (token: StyleDictionary.TransformedToken) => token.filePath.includes('dark'),
},
{
query: '@media (prefers-color-scheme: light)',
atRule: '@media (prefers-color-scheme: light)',
matcher: (token: StyleDictionary.TransformedToken) => token.filePath.includes('light'),
},
{
query: '@media (screen)',
atRule: '@media screen',
matcher: (token: StyleDictionary.TransformedToken) => !token.filePath.includes('light') && !token.filePath.includes('dark'),
}]
}
Expand All @@ -95,7 +95,7 @@ describe('Format: CSS Advanced', () => {
--customPrefix-color-background-secondary: #0000ff;
}
}
@media (screen) {
@media screen {
:root {
--customPrefix-color-background-green: #00ff00;
}
Expand All @@ -112,8 +112,8 @@ describe('Format: CSS Advanced', () => {
...file,
options: {
...file.options,
queries: [{
query: '@media (prefers-color-scheme: dark)',
rules: [{
atRule: '@media (prefers-color-scheme: dark)',
matcher: (_token: StyleDictionary.TransformedToken) => true,
}]
}
Expand All @@ -132,14 +132,42 @@ describe('Format: CSS Advanced', () => {
expect(cssAdvanced({ dictionary, file: fileOptions, options: undefined, platform })).toStrictEqual(output)
})

it('Formats tokens with one rule with nested atRule', () => {

const fileOptions = {
...file,
options: {
...file.options,
rules: [{
atRule: ['@media (prefers-color-scheme: dark)', '@supports (display: grid)'],
matcher: (_token: StyleDictionary.TransformedToken) => true,
}]
}
}

const output = `@media (prefers-color-scheme: dark) {
@supports (display: grid) {
:root {
--customPrefix-color-background-primary: #ff0000;
--customPrefix-color-background-secondary: #0000ff;
--customPrefix-color-background-green: #00ff00;
}
}
}
`
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: fake values to test formatter
expect(cssAdvanced({ dictionary, file: fileOptions, options: undefined, platform })).toStrictEqual(output)
})

it('Formats tokens with only one media query without matcher', () => {

const fileOptions = {
...file,
options: {
...file.options,
queries: [{
query: '@media (prefers-color-scheme: light)',
rules: [{
atRule: '@media (prefers-color-scheme: light)',
}]
}
}
Expand Down Expand Up @@ -170,7 +198,7 @@ describe('Format: CSS Advanced', () => {
...file,
options: {
...file.options,
queries: undefined,
rules: undefined,
}
}
const output = `:root {
Expand All @@ -195,7 +223,7 @@ describe('Format: CSS Advanced', () => {
...file,
options: {
...file.options,
queries: undefined,
rules: undefined,
queryExtensionProperty: "org.primer.mediaQuery"
}
}
Expand All @@ -221,7 +249,7 @@ describe('Format: CSS Advanced', () => {
...file,
options: {
...file.options,
queries: undefined
rules: undefined
}
}

Expand All @@ -236,22 +264,21 @@ describe('Format: CSS Advanced', () => {
expect(cssAdvanced({ dictionary, file: fileOptions, options: undefined, platform })).toStrictEqual(output)
})

it('Formats tokens with no query but selector', () => {
it('Formats tokens with no atRule but selector', () => {

const fileOptions = {
...file,
options: {
...file.options,
selector: '[data-theme="dark"]',
queries: [
rules: [
{
matcher: (token: StyleDictionary.TransformedToken) => {
console.log(token.original.name)
return token.original.name === 'color-background-primary'
},
},
{
query: '@media (min-width: 768px)',
atRule: '@media (min-width: 768px)',
matcher: (token: StyleDictionary.TransformedToken) => token.original.name !== 'color-background-primary',
}
]
Expand All @@ -278,16 +305,16 @@ describe('Format: CSS Advanced', () => {
...file,
options: {
...file.options,
queries: [{
query: '@media (prefers-color-scheme: dark)',
rules: [{
atRule: '@media (prefers-color-scheme: dark)',
matcher: (token: StyleDictionary.TransformedToken) => token.filePath.includes('notDark'),
},
{
query: '@media (prefers-color-scheme: light)',
atRule: '@media (prefers-color-scheme: light)',
matcher: (token: StyleDictionary.TransformedToken) => token.filePath.includes('light'),
},
{
query: '@media (screen)',
atRule: '@media screen',
matcher: (token: StyleDictionary.TransformedToken) => !token.filePath.includes('light') && !token.filePath.includes('dark'),
}]
}
Expand All @@ -298,7 +325,7 @@ describe('Format: CSS Advanced', () => {
--customPrefix-color-background-secondary: #0000ff;
}
}
@media (screen) {
@media screen {
:root {
--customPrefix-color-background-green: #00ff00;
}
Expand All @@ -316,7 +343,7 @@ describe('Format: CSS Advanced', () => {
options: {
selector: `body[theme="dark"]`,
...file.options,
queries: undefined
rules: undefined
}
}

Expand All @@ -337,7 +364,7 @@ describe('Format: CSS Advanced', () => {
...file,
options: {
...file.options,
queries: undefined
rules: undefined
}
}

Expand All @@ -359,16 +386,18 @@ describe('Format: CSS Advanced', () => {
options: {
...file.options,
selector: false,
queries: undefined
rules: undefined
}
}

const fileOptionsTwo = {
...file,
options: {
...file.options,
selector: "",
queries: undefined
selector: "body",
rules: [{
selector: false,
}]
}
}

Expand All @@ -383,4 +412,30 @@ describe('Format: CSS Advanced', () => {
// @ts-ignore: fake values to test formatter
expect(cssAdvanced({ dictionary, file: fileOptionsTwo, options: undefined, platform })).toStrictEqual(output)
})

it('Formats tokens with invalid selector', () => {

const fileOptions = {
...file,
options: {
...file.options,
selector: undefined,
rules: [
{
selector: ["selector", "selector2"],
matcher: (token: StyleDictionary.TransformedToken) => token.original.name !== 'color-background-primary',
}
]
}
}

const output = `:root {
--customPrefix-color-background-secondary: #0000ff;
--customPrefix-color-background-green: #00ff00;
}
`
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: fake values to test formatter
expect(cssAdvanced({ dictionary, file: fileOptions, options: undefined, platform })).toStrictEqual(output)
})
})
48 changes: 28 additions & 20 deletions src/format/css-advanced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import type { LineFormatting } from 'style-dictionary/types/FormatHelpers'
const { fileHeader, formattedVariables } = StyleDictionary.formatHelpers

export const cssAdvanced: StyleDictionary.Formatter = ({ dictionary: originalDictionary, options = {
queries: []
rules: []
}, file, platform }: FormatterArguments): string => {
// get options
const { outputReferences, descriptions } = options
// selector
const selector = file?.options?.selector !== undefined ? file?.options?.selector : ':root'
const defaultSelector = file?.options?.selector !== undefined ? file?.options?.selector : ':root'
// query extension property
const queryExtProp = file?.options?.queryExtensionProperty || 'mediaQuery'
// get queries from file options
const queries = file?.options?.queries || [{
query: undefined,
const rules = file?.options?.rules || [{
atRule: undefined,
selector: undefined,
matcher: () => true
}]
// set formatting
Expand All @@ -30,35 +31,39 @@ export const cssAdvanced: StyleDictionary.Formatter = ({ dictionary: originalDic
}
// get queries from tokens
for (const designToken of dictionary.allTokens) {
const query = designToken.$extensions?.[queryExtProp]
const atRule = designToken.$extensions?.[queryExtProp]
// early abort if query does not exist on token
if (!query) continue
if (!atRule) continue
// if query exists already from other token
const currentQueryIndex = queries.findIndex((q: {
query: string,
const currentQueryIndex = rules.findIndex((q: {
atRule: string[],
matcher: () => boolean
}) => q.query === query)
}) => q.atRule === atRule)

// if query exists
if (currentQueryIndex > -1) {
queries[currentQueryIndex] = {
...queries[currentQueryIndex],
matcher: (token: TransformedToken) => queries[currentQueryIndex].matcher(token) || token.$extensions[queryExtProp] === queries[currentQueryIndex].query
rules[currentQueryIndex] = {
...rules[currentQueryIndex],
matcher: (token: TransformedToken) => rules[currentQueryIndex].matcher(token) || token.$extensions[queryExtProp] === rules[currentQueryIndex].atRule
}
}
// if query does not exist
else {
queries.push({
query,
matcher: (token: TransformedToken) => token.$extensions?.[queryExtProp] === query
rules.push({
atRule,
matcher: (token: TransformedToken) => token.$extensions?.[queryExtProp] === atRule
})
}
}
// add file header
const output = [fileHeader({ file })]
// add single theme css
for (const query of queries) {
const { query: queryString, matcher } = query
for (const { atRule, selector, matcher } of rules) {
let preludes: string[] = !Array.isArray(atRule) ? [atRule] : atRule
// add selectors to preludes
preludes.push(typeof selector === 'string' || selector === false ? selector : defaultSelector)
// remove invalid preludes
preludes = preludes.filter(Boolean)
// filter tokens to only include the ones that pass the matcher
const filteredDictionary = {
...dictionary,
Expand All @@ -68,10 +73,13 @@ export const cssAdvanced: StyleDictionary.Formatter = ({ dictionary: originalDic
if (!filteredDictionary.allTokens.length) continue
// add tokens into root
const css = formattedVariables({ format: 'css', dictionary: filteredDictionary, outputReferences, formatting })
// wrap css
const cssWithSelector = selector && selector.trim().length > 0 ? `${selector} { ${css} }` : css
// atRule css
let cssWithSelector = css
for (const prelude of preludes.reverse()) {
cssWithSelector = `${prelude} { ${cssWithSelector} }`
}
// add css with or without query
output.push(queryString ? `${queryString} { ${cssWithSelector} }` : cssWithSelector)
output.push(cssWithSelector)
}
// return prettified
return format(output.join('\n'), { parser: 'css', printWidth: 500 })
Expand Down

0 comments on commit a9307be

Please sign in to comment.