Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
design-tokens : add support for at rules
  • Loading branch information
romainmenke committed Nov 20, 2022
commit 86ae58f21d01211a31cff541940680ec796ebc5d
3 changes: 3 additions & 0 deletions plugins/postcss-design-tokens/.tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ postcssTape(plugin)({
],
warnings: 1
},
'at-rule': {
message: "supports at rules",
},
'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-tokenizer": "^1.0.0",
"@csstools/css-parser-algorithms": "^1.0.0",
"postcss-value-parser": "^4.2.0"
},
"peerDependencies": {
Expand Down
16 changes: 14 additions & 2 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,13 +68,25 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
return;
}

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

decl.value = modifiedValue;
},
AtRule(atRule, { result }) {
if (!atRule.params.toLowerCase().includes(options.valueFunctionName)) {
return;
}

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

atRule.params = modifiedValue;
},
};
},
};
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);
}
128 changes: 128 additions & 0 deletions plugins/postcss-design-tokens/src/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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);

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

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

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);
return false;
}
});
});

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;
}
}
}
65 changes: 0 additions & 65 deletions plugins/postcss-design-tokens/src/values.ts

This file was deleted.

19 changes: 19 additions & 0 deletions plugins/postcss-design-tokens/test/at-rule.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@design-tokens url('./tokens/basic.json') format('style-dictionary3');

@media screen and (min-width: design-token('space.large-b')) {
.foo {
order: 1;
}
}

@media screen and (min-width: calc(design-token('space.large-b') * 10)) {
.foo {
order: 2;
}
}

@supports (min-width: rgb(design-token('lists.comma'))) {
.foo {
order: 3;
}
}
Loading