Skip to content

Commit

Permalink
Feat(variables-scss): Add export for shadows, gradients and typography
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelklibani committed Sep 18, 2024
1 parent e5c8a7b commit c7a2aa6
Show file tree
Hide file tree
Showing 16 changed files with 417 additions and 67 deletions.
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ makefile
CODEOWNERS

# variable-scss exporter example mock test files
exporters/variables-scss/src/**/*.scss
exporters/variables-scss/src/tests/fixtures/**/*.scss

# variable-scss exporter generated cjs
exporters/variables-scss/generated/**/*.cjs
34 changes: 20 additions & 14 deletions exporters/variables-scss/generated/exporter.cjs

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions exporters/variables-scss/src/config/fileConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ export const nonThemedFilesData: FileData[] = [
hasParentPrefix: false,
sortByNumValue: true,
},
{
fileName: '_shadows.scss',
tokenTypes: [TokenType.shadow],
groupNames: ['Shadow', 'Focus'],
hasParentPrefix: false,
},
{
fileName: '_gradients.scss',
tokenTypes: [TokenType.gradient],
groupNames: ['Gradient'],
hasParentPrefix: true,
},
{
fileName: '_typography.scss',
tokenTypes: [TokenType.typography],
groupNames: ['Heading', 'Body'],
withCssObject: true,
hasParentPrefix: false,
},
];

export const themedFilesData: FileData[] = [
Expand Down
14 changes: 8 additions & 6 deletions exporters/variables-scss/src/formatters/cssFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const IDENTATION = ' ';
const INDENTATION = ' ';

export const removeExtraBlankLines = (css: string): string => {
return css.replace(/\n{3,}/g, '\n\n');
Expand All @@ -14,16 +14,18 @@ export const formatCSS = (css: string): string => {

const lines = css.split('\n');

// TODO: Try to replace this functionality with prettier
for (const line of lines) {
if (line.includes('(')) {
formattedCSS += `${IDENTATION.repeat(indentationLevel)}${line}\n`;
// Check if both '(' and ')' are on the same line
if (line.includes('(') && line.includes(')')) {
formattedCSS += `${INDENTATION.repeat(indentationLevel)}${line}\n`;
} else if (line.includes('(')) {
formattedCSS += `${INDENTATION.repeat(indentationLevel)}${line}\n`;
indentationLevel += 1;
} else if (line.includes(')')) {
indentationLevel -= 1;
formattedCSS += `${IDENTATION.repeat(indentationLevel)}${line}\n`;
formattedCSS += `${INDENTATION.repeat(indentationLevel)}${line}\n`;
} else {
formattedCSS += `${IDENTATION.repeat(indentationLevel)}${line}\n`;
formattedCSS += `${INDENTATION.repeat(indentationLevel)}${line}\n`;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ const mockedExpectedResult = fs.readFileSync(
const mappedTokens: Map<string, Token> = new Map([]);
const tokenGroups: Array<TokenGroup> = exampleMockedGroups;
const emptyFile = `/* This file was generated by Supernova, don't change manually */\n\n`;
const indexFile = `@forward 'borders';
@forward 'other';
@forward 'radii';
@forward 'spacing';
@forward 'shadows';
@forward 'gradients';
@forward 'typography';
`;
const indexColorFile = `@forward 'colors';\n`;

describe('fileGenerator', () => {
describe('generateOutputFilesByThemes', () => {
Expand Down Expand Up @@ -40,8 +49,14 @@ describe('fileGenerator', () => {
{ path: './globals/', fileName: '_other.scss', content: mockedExpectedResult },
{ path: './globals/', fileName: '_radii.scss', content: emptyFile },
{ path: './globals/', fileName: '_spacing.scss', content: emptyFile },
{ path: './globals/', fileName: '_shadows.scss', content: emptyFile },
{ path: './globals/', fileName: '_gradients.scss', content: emptyFile },
{ path: './globals/', fileName: '_typography.scss', content: emptyFile },
{ path: './globals/', fileName: 'index.scss', content: indexFile },
{ path: './themes/theme-light/', fileName: '_colors.scss', content: emptyFile },
{ path: './themes/theme-light/', fileName: 'index.scss', content: indexColorFile },
{ path: './themes/theme-light-inverted/', fileName: '_colors.scss', content: emptyFile },
{ path: './themes/theme-light-inverted/', fileName: 'index.scss', content: indexColorFile },
]);
});
});
Expand All @@ -59,6 +74,9 @@ describe('fileGenerator', () => {
{ fileName: '_other.scss', content: mockedExpectedResult },
{ fileName: '_radii.scss', content: emptyFile },
{ fileName: '_spacing.scss', content: emptyFile },
{ fileName: '_shadows.scss', content: emptyFile },
{ fileName: '_gradients.scss', content: emptyFile },
{ fileName: '_typography.scss', content: emptyFile },
]);
});
});
Expand Down
29 changes: 18 additions & 11 deletions exporters/variables-scss/src/generators/contentGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ export const addDisclaimer = (content: string): string => {
};

export const filterTokensByTypeAndGroup = (tokens: Token[], type: TokenType, group: string) => {
return tokens.filter((token) => token.tokenType === type && token.origin?.name?.includes(group));
};
return tokens.filter((token) => {
const isMatchingType = token.tokenType === type;
const isInGroup = token.origin?.name?.includes(group);
const isValidTypography = !(token.tokenType === TokenType.typography && token.name.includes('-Underline'));

return isMatchingType && isInGroup && isValidTypography;
});
};
export const generateFileContent = (
tokens: Token[],
mappedTokens: Map<string, Token>,
Expand All @@ -30,15 +35,17 @@ export const generateFileContent = (
const filteredTokens = filterTokensByTypeAndGroup(tokens, tokenType, group);

// Generate css tokens
cssTokens += generateCssFromTokens(
filteredTokens,
mappedTokens,
tokenGroups,
group,
hasParentPrefix,
sortByNumValue,
);
cssTokens += '\n\n';
if (tokenType !== TokenType.typography) {
cssTokens += generateCssFromTokens(
filteredTokens,
mappedTokens,
tokenGroups,
group,
hasParentPrefix,
sortByNumValue,
);
cssTokens += '\n\n';
}

// Generate css object and merge it with the existing one
const groupCssObject = generateCssObjectFromTokens(filteredTokens, mappedTokens, tokenGroups, hasParentPrefix);
Expand Down
48 changes: 46 additions & 2 deletions exporters/variables-scss/src/generators/cssGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import { ColorToken, DimensionToken, StringToken, Token, TokenGroup, TokenType } from '@supernovaio/sdk-exporters';
import {
ColorToken,
DimensionToken,
GradientToken,
ShadowToken,
StringToken,
Token,
TokenGroup,
TokenType,
} from '@supernovaio/sdk-exporters';
import { ColorFormat, CSSHelper } from '@supernovaio/export-helpers';
import { addEmptyLineBetweenTokenGroups, formatTokenName, sortTokens, tokenVariableName } from '../helpers/tokenHelper';
import {
addAngleVarToGradient,
addEmptyLineBetweenTokenGroups,
formatTokenName,
sortTokens,
tokenVariableName,
} from '../helpers/tokenHelper';
import { handleSpecialCase } from '../helpers/specialCaseHelper';
import { normalizeColor } from '../helpers/colorHelper';

Expand Down Expand Up @@ -44,6 +59,35 @@ export const tokenToCSSByType = (
return formatTokenName(name, value);
}

if (token.tokenType === TokenType.shadow) {
const shadowToken = token as ShadowToken;
const name = tokenVariableName(token, tokenGroups, withParent);
const { value } = shadowToken;
const color = CSSHelper.shadowTokenValueToCSS(value, mappedTokens, {
allowReferences: true,
decimals: 3,
colorFormat: ColorFormat.hashHex8,
tokenToVariableRef: () => '',
});

return formatTokenName(name, color).replace(/0px/g, '0');
}

if (token.tokenType === TokenType.gradient) {
const gradientToken = token as GradientToken;
const name = tokenVariableName(token, tokenGroups, withParent);
const { value } = gradientToken;
let gradient = CSSHelper.gradientTokenValueToCSS(value, mappedTokens, {
allowReferences: true,
colorFormat: ColorFormat.hashHex8,
decimals: 3,
tokenToVariableRef: () => '',
});
gradient = addAngleVarToGradient(gradient);

return formatTokenName(name, gradient);
}

return null;
};

Expand Down
112 changes: 87 additions & 25 deletions exporters/variables-scss/src/generators/cssObjectGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Token, TokenGroup, TokenType } from '@supernovaio/sdk-exporters';
import { tokenVariableName } from '../helpers/tokenHelper';
import { Token, TokenGroup, TokenType, TypographyToken } from '@supernovaio/sdk-exporters';
import { tokenVariableName, typographyValue } from '../helpers/tokenHelper';
import { toPlural } from '../helpers/stringHelper';

export const COLOR_SUFFIX = '-colors';

export type CssObjectType = { [key: string]: string | object };
export type CssObjectType = { [key: string]: (string | object) & { moveToTheEnd?: string } };

/* This function handles cases that are outside the logic of aliases for the remaining tokens.
A common condition is that for tokens with a numeric part, the non-numeric part is dropped.
Expand Down Expand Up @@ -44,36 +44,74 @@ export const getTokenAlias = (token: Token): string => {
return alias;
};

const formatTypographyName = (tokenNameParts: string[]): string => {
return tokenNameParts.length === 4
? tokenNameParts.filter((_, index) => index !== 1).join('-')
: tokenNameParts.join('-');
};

const getBreakpoint = (tokenNameParts: string[]): string => {
return tokenNameParts.length === 4 ? tokenNameParts[1] : 'mobile';
};

const handleTypographyTokens = (tokenNameParts: string[], token: Token, cssObjectRef: CssObjectType): void => {
const typographyToken = token as TypographyToken;
const reducedNameParts = tokenNameParts.slice(0, 2);
const name = formatTypographyName(tokenNameParts).toLowerCase();
const breakpoint = getBreakpoint(tokenNameParts).toLowerCase();

let currentObject = cssObjectRef;
reducedNameParts.forEach((part, index) => {
const modifiedPart = index === 0 ? `$${name}` : part;

if (index === reducedNameParts.length - 1) {
const tokenValue = breakpoint;
currentObject[tokenValue] = typographyValue(typographyToken.value, name.includes('italic'));
} else {
currentObject[modifiedPart] = currentObject[modifiedPart] || {};
currentObject = currentObject[modifiedPart] as CssObjectType;
}
});
};

const handleNonTypographyTokens = (
tokenNameParts: string[],
token: Token,
tokenGroups: Array<TokenGroup>,
hasParentPrefix: boolean,
cssObjectRef: CssObjectType,
): void => {
let currentObject = cssObjectRef;

tokenNameParts.forEach((part, index) => {
const modifiedPart = index === 0 ? normalizeFirstNamePart(part, token.tokenType) : part;

if (index === tokenNameParts.length - 1) {
const tokenValue = `$${tokenVariableName(token, tokenGroups, hasParentPrefix)}`;
const tokenAlias = getTokenAlias(token);
currentObject[tokenAlias] = tokenValue;
} else {
currentObject[modifiedPart] = currentObject[modifiedPart] || {};
currentObject = currentObject[modifiedPart] as CssObjectType;
}
});
};

export const createObjectStructureFromTokenNameParts = (
token: Token,
tokenGroups: Array<TokenGroup>,
hasParentPrefix: boolean,
cssObjectRef: CssObjectType,
): CssObjectType => {
let currentObject: CssObjectType = cssObjectRef;

const { tokenType } = token;
const tokenNameParts = token.origin?.name?.split('/');

if (tokenNameParts) {
tokenNameParts.forEach((part, index) => {
let modifiedPart = part;

// format first part of the name part as object key
if (index === 0) {
modifiedPart = normalizeFirstNamePart(part, token.tokenType);
}
// format the last part of the name part as token alias and assign token value
if (index === tokenNameParts.length - 1) {
const tokenValue = `$${tokenVariableName(token, tokenGroups, hasParentPrefix)}`;
const tokenAlias = getTokenAlias(token);

currentObject[tokenAlias] = tokenValue;
} else {
// format the rest of the name parts as object keys
currentObject[modifiedPart] = currentObject[modifiedPart] || {};
currentObject = currentObject[modifiedPart] as CssObjectType;
}
});
if (!tokenNameParts) return cssObjectRef;

if (tokenType === TokenType.typography) {
handleTypographyTokens(tokenNameParts, token, cssObjectRef);
} else {
handleNonTypographyTokens(tokenNameParts, token, tokenGroups, hasParentPrefix, cssObjectRef);
}

return cssObjectRef;
Expand All @@ -86,8 +124,20 @@ export const colorGroupsReducer = (accumulatedColorKeys: { [key: string]: string
[parseGroupName(currentColorKey)]: currentColorKey,
});

export const typographyGroupReducer = (
accumulatedTypographyKeys: { [key: string]: string },
currentTypographyKey: string,
) => ({
...accumulatedTypographyKeys,
[parseGroupName(currentTypographyKey)]: currentTypographyKey,
});

export const createGlobalColorsObject = (colorKeys: Array<string>) => colorKeys.reduce(colorGroupsReducer, {});

export const createGlobalTypographyObject = (typographyKeys: Array<string>) => {
return typographyKeys.reduce(typographyGroupReducer, {});
};

// TODO: refactor this function to not use cssObject reference
export const generateCssObjectFromTokens = (
tokens: Array<Token>,
Expand Down Expand Up @@ -117,5 +167,17 @@ export const generateCssObjectFromTokens = (
return { ...cssObject, $colors: colorsObject };
}

const typographyKeys = Object.keys(cssObject).filter((key) => key.includes('heading') || key.includes('body'));

if (typographyKeys.length > 0) {
const typographyObject = createGlobalTypographyObject(typographyKeys);

// Typography has multiple groups, which creates multiple '$styles' objects.
// After merging the '$styles' objects together, they remain in the middle of the tokens,
// so we need to move them to the end of the file using the 'moveToTheEnd' flag,
// which will be removed in the final output.
return { ...cssObject, $styles: { ...typographyObject, moveToTheEnd: 'true' } };
}

return cssObject;
};
Loading

0 comments on commit c7a2aa6

Please sign in to comment.