Skip to content

Commit

Permalink
Feat(variables-scss): Formatting color tokens #DS-1461
Browse files Browse the repository at this point in the history
  • Loading branch information
curdaj committed Sep 10, 2024
1 parent 82dfcb6 commit d519195
Show file tree
Hide file tree
Showing 18 changed files with 447 additions and 124 deletions.
31 changes: 17 additions & 14 deletions exporters/variables-scss/generated/exporter.cjs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ $grid-spacing-desktop: 32px !default;

$grid-columns: 12 !default;

$grid-spacings: (
spacing-desktop: $grid-spacing-desktop,
) !default;

$grids: (
spacing: (
desktop: $grid-spacing-desktop,
),
columns: $grid-columns,
) !default;
2 changes: 1 addition & 1 deletion exporters/variables-scss/src/formatters/cssFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const formatCSS = (css: string): string => {

for (const line of lines) {
if (line.includes('(')) {
formattedCSS += `${IDENTATION.repeat(indentationLevel)}${line}\n`;
indentationLevel += 1;
formattedCSS += `${line}\n`;
} else if (line.includes(')')) {
indentationLevel -= 1;
formattedCSS += `${IDENTATION.repeat(indentationLevel)}${line}\n`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ describe('contentGenerator', () => {
const groupNames = ['Grid', 'String'];
const withCssObject = true;
const hasParentPrefix = true;
const sortByNumValue = false;

const fileContent = generateFileContent(
tokens,
Expand All @@ -28,6 +29,7 @@ describe('contentGenerator', () => {
groupNames,
withCssObject,
hasParentPrefix,
sortByNumValue,
);

expect(fileContent).toStrictEqual({ content: mockedExpectedResult });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const dataProvider = [
token: {
id: '3',
name: 'unsupportedToken',
tokenType: TokenType.color,
tokenType: TokenType.duration,
} as Token,
expectedCss: null,
hasParentPrefix: true,
Expand All @@ -53,9 +53,16 @@ describe('cssGenerator', () => {

describe('generateCssFromTokens', () => {
it('should generate CSS from tokens', () => {
const css = generateCssFromTokens(Array.from(exampleMockedTokens.values()), mappedTokens, tokenGroups, true);
const css = generateCssFromTokens(
Array.from(exampleMockedTokens.values()),
mappedTokens,
tokenGroups,
'Grid',
true,
false,
);

expect(css).toBe('$grid-spacing-desktop: 32px !default;\n$grid-columns: 12 !default;');
expect(css).toBe('$grid-columns: 12 !default;\n\n$grid-spacing-desktop: 32px !default;');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Token, TokenGroup } from '@supernovaio/sdk-exporters';
import { generateCssObjectFromTokens, generateObjectContent } from '../cssObjectGenerator';
import { generateCssObjectFromTokens, getNonNumericPart } from '../cssObjectGenerator';
import { exampleMockedGroups, exampleMockedTokens } from '../../formatters/__fixtures__/mockedExampleTokens';

const mappedTokens: Map<string, Token> = new Map([]);
Expand All @@ -15,21 +15,19 @@ describe('cssObjectGenerator', () => {
true,
);

expect(css).toBe(
'$grid-spacings: (\nspacing-desktop: $grid-spacing-desktop,\n) !default;\n\n$grids: (\ncolumns: $grid-columns,\n) !default;\n\n',
);
expect(css).toStrictEqual({
$grids: { columns: '$grid-columns', spacing: { desktop: '$grid-spacing-desktop' } },
});
});
});

describe('generateObjectContent', () => {
it('should generate object content', () => {
const objectContent = generateObjectContent(
[exampleMockedTokens.get('dimensionRef') as Token],
tokenGroups,
true,
);
describe('getNonNumericPart', () => {
it('should return special case for radius-full', () => {
expect(getNonNumericPart('radius-full')).toBe('full');
});

expect(objectContent).toBe('spacing-desktop: $grid-spacing-desktop,\n');
it('should return lowercase token name for other cases', () => {
expect(getNonNumericPart('Grid')).toBe('grid');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('fileGenerator', () => {
{ fileName: '_other.scss', content: mockedExpectedResult },
{ fileName: '_radii.scss', content: emptyFile },
{ fileName: '_spacing.scss', content: emptyFile },
{ fileName: '_colors.scss', content: emptyFile },
]);
});
});
30 changes: 24 additions & 6 deletions exporters/variables-scss/src/generators/contentGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Token, TokenGroup, TokenType } from '@supernovaio/sdk-exporters';
import { generateCssFromTokens } from './cssGenerator';
import { generateCssObjectFromTokens } from './cssObjectGenerator';
import { CssObjectType, generateCssObjectFromTokens } from './cssObjectGenerator';
import { formatCSS } from '../formatters/cssFormatter';
import { convertToScss, deepMergeObjects } from '../helpers/cssObjectHelper';

// Add disclaimer to the top of the content
export const addDisclaimer = (content: string): string => {
Expand All @@ -12,6 +13,7 @@ export const filterTokensByTypeAndGroup = (tokens: Token[], type: TokenType, gro
return tokens.filter((token) => token.tokenType === type && token.origin?.name?.includes(group));
};

// TODO: refactor to use fileData instead of destructuring
export const generateFileContent = (
tokens: Token[],
mappedTokens: Map<string, Token>,
Expand All @@ -20,25 +22,41 @@ export const generateFileContent = (
groupNames: string[],
withCssObject: boolean,
hasParentPrefix: boolean,
sortByNumValue: boolean,
) => {
let cssTokens = '';
let cssObject = '';
let cssObject: CssObjectType = {};

// Iterate over token types and group names to filter tokens
tokenTypes.forEach((tokenType) => {
groupNames.forEach((group) => {
const filteredTokens = filterTokensByTypeAndGroup(tokens, tokenType, group);

// Generate css tokens
cssTokens += generateCssFromTokens(filteredTokens, mappedTokens, tokenGroups, hasParentPrefix);
cssTokens += generateCssFromTokens(
filteredTokens,
mappedTokens,
tokenGroups,
group,
hasParentPrefix,
sortByNumValue,
);
cssTokens += '\n\n';

// Generate css object
cssObject += generateCssObjectFromTokens(filteredTokens, mappedTokens, tokenGroups, hasParentPrefix);
// Generate css object and merge it with the existing one
const groupCssObject = generateCssObjectFromTokens(filteredTokens, mappedTokens, tokenGroups, hasParentPrefix);
cssObject = deepMergeObjects(cssObject, groupCssObject);
});
});

const content = withCssObject ? `${cssTokens}${cssObject}` : cssTokens;
let content = cssTokens;

// convert css object to scss structure
if (withCssObject) {
content += Object.entries(cssObject)
.map(([key, obj]) => `${key}: (\n${convertToScss(obj as CssObjectType)}\n) !default;\n\n`)
.join('');
}

return {
content: addDisclaimer(formatCSS(content)),
Expand Down
36 changes: 29 additions & 7 deletions exporters/variables-scss/src/generators/cssGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { DimensionToken, StringToken, Token, TokenGroup, TokenType } from '@supernovaio/sdk-exporters';
import { CSSHelper } from '@supernovaio/export-helpers';
import { formatTokenName, tokenVariableName } from '../helpers/tokenHelper';
import { ColorToken, DimensionToken, StringToken, Token, TokenGroup, TokenType } from '@supernovaio/sdk-exporters';
import { ColorFormat, CSSHelper } from '@supernovaio/export-helpers';
import { addEmptyLineBetweenTokenGroups, formatTokenName, sortTokens, tokenVariableName } from '../helpers/tokenHelper';
import { handleSpecialCase } from '../helpers/specialCaseHelper';
import { normalizeColor } from '../helpers/colorHelper';

export const tokenToCSSByType = (
token: Token,
Expand All @@ -28,17 +29,38 @@ export const tokenToCSSByType = (
return formatTokenName(name, value);
}

if (token.tokenType === TokenType.color) {
const colorToken = token as ColorToken;
const name = tokenVariableName(colorToken, tokenGroups, withParent);
let value = CSSHelper.colorTokenValueToCSS(colorToken.value, mappedTokens, {
allowReferences: true,
decimals: 3,
colorFormat: ColorFormat.hex8,
tokenToVariableRef: () => '',
});
value = normalizeColor(value);
value = handleSpecialCase(name, value);

return formatTokenName(name, value);
}

return null;
};

export const generateCssFromTokens = (
tokens: Token[],
mappedTokens: Map<string, Token>,
tokenGroups: Array<TokenGroup>,
group: string,
hasParentPrefix: boolean,
sortByNumValue: boolean,
): string => {
return tokens
.map((token) => tokenToCSSByType(token, mappedTokens, tokenGroups, hasParentPrefix))
.filter(Boolean)
.join('\n');
const sortedTokens = sortTokens(tokens, tokenGroups, hasParentPrefix, group, sortByNumValue);

const cssTokens = sortedTokens.map((token) => ({
css: tokenToCSSByType(token, mappedTokens, tokenGroups, hasParentPrefix),
parentGroupId: token.parentGroupId,
}));

return addEmptyLineBetweenTokenGroups(cssTokens);
};
84 changes: 46 additions & 38 deletions exporters/variables-scss/src/generators/cssObjectGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,59 @@
import { Token, TokenGroup } from '@supernovaio/sdk-exporters';
import { toPlural } from '../helpers/stringHelper';
import { Token, TokenGroup, TokenType } from '@supernovaio/sdk-exporters';
import { tokenVariableName } from '../helpers/tokenHelper';
import { toPlural } from '../helpers/stringHelper';

export const generateObjectContent = (
tokens: Array<Token>,
tokenGroups: Array<TokenGroup>,
withParent: boolean,
): string => {
return tokens.reduce((result, token) => {
const name = tokenVariableName(token, tokenGroups, withParent);
const numericPart = name.match(/\d+/)?.[0];
const prefix = `${token.origin?.name?.split('/')[0].toLowerCase()}-`;
const nonNumericPart = name.replace(prefix, '');

if (numericPart) {
result += `${numericPart}: $${name},\n`;
} else if (nonNumericPart) {
result += `${nonNumericPart}: $${name},\n`;
}
export type CssObjectType = { [key: string]: string | object };

// TODO : add comments to this function
// Handle invariant token aliases eg radius-full -> full
const invariantTokenAlias: { [key: string]: string } = {
'radius-full': 'full',
};

export const getNonNumericPart = (tokenName: string): string => {
if (invariantTokenAlias[tokenName]) {
return invariantTokenAlias[tokenName];
}

return result;
}, '');
return tokenName.toLowerCase();
};

export const generateCssObjectFromTokens = (
tokens: Array<Token>,
mappedTokens: Map<string, Token>,
tokenGroups: Array<TokenGroup>,
hasParentPrefix: boolean,
): string => {
const originNameMap = new Map<string, Array<Token>>();
tokens.forEach((token) => {
const originName = token.origin?.name;

if (originName) {
const nameParts = originName.split('/');
nameParts.pop();
const objectName = toPlural(nameParts.join('-').toLowerCase());
originNameMap.set(objectName, [...(originNameMap.get(objectName) || []), token]);
}
});
): CssObjectType => {
// TODO: rename cssObject to cssObjectAcumulator
return tokens.reduce((cssObject, token) => {
const nameParts = token.origin?.name?.split('/');

return Array.from(originNameMap.entries())
.map(([objectName, token]) => {
const objectContent = generateObjectContent(token, tokenGroups, hasParentPrefix);
if (nameParts) {
let currentObject: CssObjectType = cssObject;

nameParts.forEach((part, index) => {
if (index === 0) {
part = token.tokenType === TokenType.color ? `$${part}-colors` : `$${toPlural(part.toLowerCase())}`;
}
if (index === nameParts.length - 1) {
const value = tokenVariableName(token, tokenGroups, hasParentPrefix);
let result;
const numericPart = token.name.match(/\d+/)?.[0];
const nonNumericPart = getNonNumericPart(token.name);

if (token.tokenType !== TokenType.color && numericPart) {
result = numericPart;
} else {
result = nonNumericPart;
}
currentObject[result] = `$${value}`;
} else {
currentObject[part] = currentObject[part] || {};
currentObject = currentObject[part] as CssObjectType;
}
});
}

return objectContent.trim() && `$${objectName}: (\n${objectContent}) !default;\n\n`;
})
.join('');
return cssObject;
}, {});
};
Loading

0 comments on commit d519195

Please sign in to comment.