Skip to content

design-tokens : add support for at rules #717

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

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
4 changes: 4 additions & 0 deletions package-lock.json

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

7 changes: 7 additions & 0 deletions plugins/postcss-design-tokens/.tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ postcssTape(plugin)({
],
warnings: 1
},
'at-rule': {
message: "supports at rules",
},
'at-rule-error': {
message: "supports at rules",
warnings: 1
},
'units': {
message: "supports units usage",
plugins: [
Expand Down
1 change: 1 addition & 0 deletions plugins/postcss-design-tokens/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### Unreleased (major)

- Updated: Support for Node v14+ (major).
- Added support for design tokens in at rules (`@media`, `@supports`, ...)

### 1.2.0 (September 7, 2022)

Expand Down
32 changes: 30 additions & 2 deletions plugins/postcss-design-tokens/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
},
"size": {
"spacing": {
"small": { "value": "16px" }
"small": { "value": "16px" },
"medium": { "value": "18px" }
}
},
"viewport": {
"medium": { "value": "35rem" }
}
}
```
Expand All @@ -29,6 +33,12 @@
padding-bottom: design-token('size.spacing.small' to rem);
}

@media (min-width: design-token('viewport.medium')) {
.foo {
padding-bottom: design-token('size.spacing.medium' to rem);
}
}

/* becomes */

.foo {
Expand All @@ -37,6 +47,12 @@
padding-left: 16px;
padding-bottom: 1rem;
}

@media (min-width: 35rem) {
.foo {
padding-bottom: 1.1rem;
}
}
```

## Usage
Expand Down Expand Up @@ -195,6 +211,12 @@ postcssDesignTokens({
padding-bottom: design-token('size.spacing.small' to rem);
}

@media (min-width: design-token('viewport.medium')) {
.foo {
padding-bottom: design-token('size.spacing.medium' to rem);
}
}

/* becomes */

.foo {
Expand All @@ -203,6 +225,12 @@ postcssDesignTokens({
padding-left: 16px;
padding-bottom: 0.8rem;
}

@media (min-width: 35rem) {
.foo {
padding-bottom: 0.9rem;
}
}
```

### Customize function and at rule names
Expand Down Expand Up @@ -290,7 +318,7 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into
@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark');
```

You can also import tokens from an `npm` pacakge:
You can also import tokens from an `npm` package:

```pcss
@design-tokens url('node_modules://my-npm-package/tokens.json') format('style-dictionary3');
Expand Down
2 changes: 1 addition & 1 deletion plugins/postcss-design-tokens/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ The `@design-tokens` rule is used to import design tokens from a JSON file into
@design-tokens url('./tokens-dark-mode.json') format('style-dictionary3') when('dark');
```

You can also import tokens from an `npm` pacakge:
You can also import tokens from an `npm` package:

```pcss
@design-tokens url('node_modules://my-npm-package/tokens.json') format('style-dictionary3');
Expand Down
2 changes: 2 additions & 0 deletions plugins/postcss-design-tokens/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
"dist"
],
"dependencies": {
"@csstools/css-parser-algorithms": "^1.0.0",
"@csstools/css-tokenizer": "^1.0.0",
"postcss-value-parser": "^4.2.0"
},
"peerDependencies": {
Expand Down
28 changes: 24 additions & 4 deletions plugins/postcss-design-tokens/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Token } from './data-formats/base/token';
import { tokensFromImport } from './data-formats/parse-import';
import { mergeTokens } from './data-formats/token';
import { parsePluginOptions, pluginOptions } from './options';
import { onCSSValue } from './values';
import { transform } from './transform';

const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
const options = parsePluginOptions(opts);
Expand Down Expand Up @@ -68,12 +68,32 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
return;
}

const modifiedValue = onCSSValue(tokens, result, decl, options);
if (modifiedValue === decl.value) {
try {
const modifiedValue = transform(tokens, result, decl, decl.value, options);
if (modifiedValue === decl.value) {
return;
}

decl.value = modifiedValue;
} catch (err) {
decl.warn(result, `Failed to parse and transform "${decl.value}"`);
}
},
AtRule(atRule, { result }) {
if (!atRule.params.toLowerCase().includes(options.valueFunctionName)) {
return;
}

decl.value = modifiedValue;
try {
const modifiedValue = transform(tokens, result, atRule, atRule.params, options);
if (modifiedValue === atRule.params) {
return;
}

atRule.params = modifiedValue;
} catch(err) {
atRule.warn(result, `Failed to parse and transform "${atRule.params}"`);
}
},
};
},
Expand Down
33 changes: 33 additions & 0 deletions plugins/postcss-design-tokens/src/parse-component-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { parseListOfComponentValues } from '@csstools/css-parser-algorithms';
import { CSSToken, tokenizer } from '@csstools/css-tokenizer';

export function parseComponentValuesFromTokens(tokens: Array<CSSToken>) {
return parseListOfComponentValues(tokens, {
onParseError: (err) => {
throw new Error(JSON.stringify(err));
},
});
}

export function parseComponentValues(source: string) {
const t = tokenizer({ css: source }, {
commentsAreTokens: true,
onParseError: (err) => {
throw new Error(JSON.stringify(err));
},
});

const tokens: Array<CSSToken> = [];

{
while (!t.endOfFile()) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tokens.push(t.nextToken()!);
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
tokens.push(t.nextToken()!); // EOF-token
}

return parseComponentValuesFromTokens(tokens);
}
135 changes: 135 additions & 0 deletions plugins/postcss-design-tokens/src/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { ComponentValue, isCommentNode, isFunctionNode, isTokenNode, isWhitespaceNode } from '@csstools/css-parser-algorithms';
import { TokenType } from '@csstools/css-tokenizer';
import type { Node, Result } from 'postcss';
import { Token, TokenTransformOptions } from './data-formats/base/token';
import { parsedPluginOptions } from './options';
import { parseComponentValues } from './parse-component-values';

export function transform(tokens: Map<string, Token>, result: Result, postCSSNode: Node, source: string, opts: parsedPluginOptions) {
const componentValues = parseComponentValues(source);

let didChangeSomething = false;
componentValues.forEach((componentValue, index) => {
if (!('walk' in componentValue)) {
return;
}

{
const replacements = transformComponentValue(componentValue, tokens, result, postCSSNode, opts);
if (replacements) {
componentValues.splice(index, 1, ...replacements);
didChangeSomething = true;
return false;
}
}

componentValue.walk((entry, nodeIndex) => {
if (typeof nodeIndex === 'string') {
// Should never happen in FunctionNode
return;
}

const replacements = transformComponentValue(entry.node, tokens, result, postCSSNode, opts);
if (replacements) {
entry.parent.value.splice(nodeIndex, 1, ...replacements);
didChangeSomething = true;
return false;
}
});
});

if (!didChangeSomething) {
return source;
}

return componentValues.map((x) => x.toString()).join('');
}


function transformComponentValue(node: ComponentValue, tokens: Map<string, Token>, result: Result, postCSSNode: Node, opts: parsedPluginOptions) {
if (!isFunctionNode(node)) {
return;
}

if (node.nameTokenValue().toLowerCase() !== opts.valueFunctionName) {
return;
}

let tokenName = '';
let operator = '';
let operatorSubject = '';

for (let i = 0; i < node.value.length; i++) {
const subValue = node.value[i];
if (isWhitespaceNode(subValue) || isCommentNode(subValue)) {
continue;
}

if (
!tokenName &&
isTokenNode(subValue) &&
subValue.value[0] === TokenType.String
) {
tokenName = subValue.value[4].value;
continue;
}

if (
tokenName &&
!operator &&
isTokenNode(subValue) &&
subValue.value[0] === TokenType.Ident &&
subValue.value[4].value.toLowerCase() === 'to'
) {
operator = 'to';
continue;
}

if (
tokenName &&
operator &&
isTokenNode(subValue) &&
subValue.value[0] === TokenType.Ident
) {
operatorSubject = subValue.value[4].value;
continue;
}

break;
}

if (!tokenName) {
postCSSNode.warn(result, 'Expected at least a single string literal for the design-token function.');
return;
}

const replacement = tokens.get(tokenName);
if (!replacement) {
postCSSNode.warn(result, `design-token: "${tokenName}" is not configured.`);
return;
}

if (!operator) {
return parseComponentValues(replacement.cssValue());
}

const transformOptions: TokenTransformOptions = {
pluginOptions: opts.unitsAndValues,
};

if (operator === 'to') {
if (!operatorSubject) {
postCSSNode.warn(result, `Invalid or missing unit in "${node.toString()}"`);
return;
}

transformOptions.toUnit = operatorSubject;

try {
return parseComponentValues(replacement.cssValue(transformOptions));
} catch (err) {
postCSSNode.warn(result, (err as Error).message);
return;
}
}
}
Loading