Skip to content

design tokens : unit conversion #388

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
12 changes: 11 additions & 1 deletion plugins/postcss-design-tokens/.tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@ import postcssImport from 'postcss-import';
postcssTape(plugin)({
basic: {
message: "supports basic usage",
options: {},
plugins: [
postcssImport(),
plugin()
]
},
'basic:rootFontSize-20': {
message: "supports basic usage with { unitsAndValues { rootFontSize: 20 } }",
plugins: [
postcssImport(),
plugin({
unitsAndValues: {
rootFontSize: 20
}
})
]
},
'errors': {
message: "handles issues correctly",
options: {},
Expand Down
9 changes: 8 additions & 1 deletion plugins/postcss-design-tokens/src/data-formats/base/token.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export interface TokenTransformOptions {
pluginOptions?: {
rootFontSize?: number;
};
toUnit?: string;
}

export interface Token {
cssValue(): string
cssValue(opts?: TokenTransformOptions): string
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TokenTransformOptions } from '../../base/token';
import { toposort } from '../../toposort/toposort';
import { StyleDictionaryV3TokenValue } from './value';
import { applyTransformsToValue, StyleDictionaryV3TokenValue } from './value';

export function dereferenceTokenValues(tokens: Map<string, StyleDictionaryV3TokenValue>): Map<string, StyleDictionaryV3TokenValue> {
const tainted = new Set<string>();
Expand Down Expand Up @@ -53,8 +54,8 @@ export function dereferenceTokenValues(tokens: Map<string, StyleDictionaryV3Toke
const currentToken = tokens.get(id);

currentToken.value = value;
currentToken.cssValue = () => {
return value ?? '';
currentToken.cssValue = (transformOptions: TokenTransformOptions) => {
return applyTransformsToValue(value, transformOptions);
};

tokens.set(id, currentToken);
Expand Down Expand Up @@ -125,8 +126,8 @@ export function dereferenceTokenValues(tokens: Map<string, StyleDictionaryV3Toke
const currentToken = tokens.get(id);

currentToken.value = value;
currentToken.cssValue = () => {
return value ?? '';
currentToken.cssValue = (transformOptions: TokenTransformOptions) => {
return applyTransformsToValue(value, transformOptions);
};

tokens.set(id, currentToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { TokenTransformOptions } from '../../base/token';
import valueParser from 'postcss-value-parser';

export type StyleDictionaryV3TokenValue = {
cssValue(): string
cssValue(transformOptions?: TokenTransformOptions): string
// The value of the design token. This can be any type of data, a hex string, an integer, a file path to a file, even an object or array.
value: unknown
// Usually the name for a design token is generated with a name transform, but you can write your own if you choose. By default Style Dictionary will add a default name which is the key of the design token object.
Expand Down Expand Up @@ -33,8 +36,8 @@ export function extractStyleDictionaryV3Token(node: unknown, key: string, path:

return {
value: value,
cssValue: () => {
return value ?? '';
cssValue: (transformOptions?: TokenTransformOptions) => {
return applyTransformsToValue(value, transformOptions);
},
name: node['name'] ?? key,
comment: node['comment'] ?? undefined,
Expand All @@ -46,3 +49,58 @@ export function extractStyleDictionaryV3Token(node: unknown, key: string, path:
},
};
}

export function applyTransformsToValue(value: string|undefined|null, transformOptions?: TokenTransformOptions): string {
if (!value) {
return '';
}

if (!transformOptions) {
return value;
}

if (transformOptions.toUnit) {
const dimension = valueParser.unit(value ?? '');
if (!dimension || dimension.unit === transformOptions.toUnit) {
return `${value}`;
}

if (dimension.unit === 'rem' && transformOptions.toUnit === 'px') {
return remToPx(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16);
}

if (dimension.unit === 'px' && transformOptions.toUnit === 'rem') {
return pxToRem(parseFloat(dimension.number), transformOptions.pluginOptions?.rootFontSize ?? 16);
}
}

return value;
}

function remToPx(value: number, rootFontSize: number): string {
return `${formatFloat(value * rootFontSize)}px`;
}

function pxToRem(value: number, rootFontSize: number): string {
return `${formatFloat(value / rootFontSize)}rem`;
}

function formatFloat(value: number): string {
if (Number.isInteger(value)) {
return value.toString();
}

let fixedPrecision = value.toFixed(5);
for (let i = fixedPrecision.length; i > 0; i--) {
if (fixedPrecision[i] === '.') {
break;
}

if (fixedPrecision[i] !== '0') {
fixedPrecision = fixedPrecision.slice(0, i + 1);
continue;
}
}

return fixedPrecision;
}
6 changes: 2 additions & 4 deletions plugins/postcss-design-tokens/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ import type { Node, PluginCreator } from 'postcss';
import { Token } from './data-formats/base/token';
import { tokensFromImport } from './data-formats/parse-import';
import { mergeTokens } from './data-formats/token';
import { pluginOptions } from './options';
import { onCSSValue } from './values';

type pluginOptions = {
is?: Array<string>
}

const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
const buildIs = opts?.is ?? [];
Expand Down Expand Up @@ -70,7 +68,7 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
return;
}

const modifiedValue = onCSSValue(tokens, result, decl);
const modifiedValue = onCSSValue(tokens, result, decl, opts);
if (modifiedValue === decl.value) {
return;
}
Expand Down
6 changes: 6 additions & 0 deletions plugins/postcss-design-tokens/src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type pluginOptions = {
is?: Array<string>
unitsAndValues?: {
rootFontSize?: number
}
}
41 changes: 33 additions & 8 deletions plugins/postcss-design-tokens/src/values.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,57 @@
import type { Declaration, Result } from 'postcss';
import valueParser from 'postcss-value-parser';
import { Token } from './data-formats/base/token';
import { Token, TokenTransformOptions } from './data-formats/base/token';
import { pluginOptions } from './options';

export function onCSSValue(tokens: Map<string, Token>, result: Result, decl: Declaration) {
export function onCSSValue(tokens: Map<string, Token>, result: Result, decl: Declaration, opts?: pluginOptions) {
const valueAST = valueParser(decl.value);

valueAST.walk(node => {
if (node.type !== 'function' || node.value !== 'design-token') {
return;
}

if (!node.nodes || node.nodes.length !== 1) {
decl.warn(result, 'Expected a single string literal for the design-token function.');
if (!node.nodes || node.nodes.length === 0) {
decl.warn(result, 'Expected at least a single string literal for the design-token function.');
return;
}

if (node.nodes[0].type !== 'string') {
decl.warn(result, 'Expected a single string literal for the design-token function.');
decl.warn(result, 'Expected at least a single string literal for the design-token function.');
return;
}

const replacement = tokens.get(node.nodes[0].value);
const tokenName = node.nodes[0].value;
const replacement = tokens.get(tokenName);
if (!replacement) {
decl.warn(result, `design-token: "${node.nodes[0].value}" is not configured.`);
decl.warn(result, `design-token: "${tokenName}" is not configured.`);
return;
}

node.value = replacement.cssValue();
const remainingNodes = node.nodes.slice(1).filter(x => x.type === 'word');
if (!remainingNodes.length) {
node.value = replacement.cssValue();
node.nodes = undefined;
return;
}

const transformOptions: TokenTransformOptions = {
pluginOptions: opts?.unitsAndValues,
};
for (let i = 0; i < remainingNodes.length; i++) {
if (
remainingNodes[i].type === 'word' &&
remainingNodes[i].value === 'to' &&
remainingNodes[i + 1] &&
remainingNodes[i + 1].type === 'word' &&
['px', 'rem'].includes(remainingNodes[i + 1].value)
) {
transformOptions.toUnit = remainingNodes[i + 1].value;
i++;
}
}

node.value = replacement.cssValue(transformOptions);
node.nodes = undefined;
});

Expand Down
41 changes: 41 additions & 0 deletions plugins/postcss-design-tokens/test/basic.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,45 @@
.card {
background-color: design-token('card.background');
color: design-token('card.foreground');
color: design-token( 'card.foreground');
color: design-token('card.foreground' );
color: design-token(
/* a foreground color */
'card.foreground'
);
color: design-token(
'card.foreground'
/* a foreground color */
);
}

.px-to-px {
padding-bottom: design-token('space.small' to px);
padding-bottom: design-token('space.default' to px);
padding-bottom: design-token('space.large' to px);
}

.px-to-rem {
padding-bottom: design-token('space.small' to rem);
padding-bottom: design-token('space.default' to rem);
padding-bottom: design-token('space.large' to rem);
}

.rem-to-rem {
padding-bottom: design-token('space.small-b' to rem);
padding-bottom: design-token('space.default-b' to rem);
padding-bottom: design-token('space.large-b' to rem);
}

.rem-to-px {
padding-bottom: design-token('space.small-b' to px);
padding-bottom: design-token('space.default-b' to px);
padding-bottom: design-token('space.large-b' to px);
}

.invalid-conversion {
color: design-token('card.foreground' to rem);
color: design-token('card.foreground' to px);
color: design-token('space.lh' to rem);
color: design-token('space.lh' to px);
}
30 changes: 30 additions & 0 deletions plugins/postcss-design-tokens/test/basic.expect.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,34 @@
.card {
background-color: blue;
color: red;
color: red;
color: red;
color: red;
color: red;
}
.px-to-px {
padding-bottom: 8px;
padding-bottom: 18px;
padding-bottom: 32px;
}
.px-to-rem {
padding-bottom: 0.5rem;
padding-bottom: 1.1rem;
padding-bottom: 2rem;
}
.rem-to-rem {
padding-bottom: 0.5rem;
padding-bottom: 1.125rem;
padding-bottom: 2rem;
}
.rem-to-px {
padding-bottom: 8px;
padding-bottom: 18px;
padding-bottom: 32px;
}
.invalid-conversion {
color: red;
color: red;
color: 1lh;
color: 1lh;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.foo {
font-family: Helvetica sans;
font-size: 10;
color: #111111;
}
.card {
background-color: blue;
color: red;
color: red;
color: red;
color: red;
color: red;
}
.px-to-px {
padding-bottom: 8px;
padding-bottom: 18px;
padding-bottom: 32px;
}
.px-to-rem {
padding-bottom: 0.4rem;
padding-bottom: 0.9rem;
padding-bottom: 1.6rem;
}
.rem-to-rem {
padding-bottom: 0.5rem;
padding-bottom: 1.125rem;
padding-bottom: 2rem;
}
.rem-to-px {
padding-bottom: 10px;
padding-bottom: 22.5px;
padding-bottom: 40px;
}
.invalid-conversion {
color: red;
color: red;
color: 1lh;
color: 1lh;
}
23 changes: 23 additions & 0 deletions plugins/postcss-design-tokens/test/tokens/basic.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,28 @@
"logical-color": {
"foreground": { "value": "{base-color.red}" },
"background": { "value": "{base-color.blue}" }
},
"space": {
"small": {
"value": "8px"
},
"default": {
"value": "18px"
},
"large": {
"value": "32px"
},
"small-b": {
"value": "0.5rem"
},
"default-b": {
"value": "1.125rem"
},
"large-b": {
"value": "2rem"
},
"lh": {
"value": "1lh"
}
}
}