♻️(tokens) improve tokens generation algo

This way the code is more readable, it makes easier the future
maintenability.
This commit is contained in:
Nathan Panchout
2025-09-17 13:12:49 +02:00
committed by NathanVss
parent a420bcb1ab
commit e24845b427
5 changed files with 1316 additions and 197 deletions

View File

@@ -51,10 +51,10 @@ describe("CssGenerator", () => {
--c--components--button--font-family: Times New Roman,Helvetica Neue,Segoe UI; --c--components--button--font-family: Times New Roman,Helvetica Neue,Segoe UI;
} }
.cunningham-theme--dark{ .cunningham-theme--dark{
--c--theme--colors--primary: black; --c--globals--colors--primary: black;
} }
.cunningham-theme--custom{ .cunningham-theme--custom{
--c--theme--colors--primary: green; --c--globals--colors--primary: green;
} }
" "
`); `);
@@ -101,10 +101,10 @@ describe("CssGenerator", () => {
--c--components--button--font-family: Times New Roman,Helvetica Neue,Segoe UI; --c--components--button--font-family: Times New Roman,Helvetica Neue,Segoe UI;
} }
.cunningham-theme--dark{ .cunningham-theme--dark{
--c--theme--colors--primary: black; --c--globals--colors--primary: black;
} }
.cunningham-theme--custom{ .cunningham-theme--custom{
--c--theme--colors--primary: green; --c--globals--colors--primary: green;
} .clr-primary { color: var(--c--globals--colors--primary); } } .clr-primary { color: var(--c--globals--colors--primary); }
.clr-secondary { color: var(--c--globals--colors--secondary); } .clr-secondary { color: var(--c--globals--colors--secondary); }
.clr-ternary-900 { color: var(--c--globals--colors--ternary-900); } .clr-ternary-900 { color: var(--c--globals--colors--ternary-900); }
@@ -113,42 +113,18 @@ describe("CssGenerator", () => {
.bg-secondary { background-color: var(--c--globals--colors--secondary); } .bg-secondary { background-color: var(--c--globals--colors--secondary); }
.bg-ternary-900 { background-color: var(--c--globals--colors--ternary-900); } .bg-ternary-900 { background-color: var(--c--globals--colors--ternary-900); }
.bg-ogre-odor-is-orange-indeed { background-color: var(--c--globals--colors--ogre-odor-is-orange-indeed); } .bg-ogre-odor-is-orange-indeed { background-color: var(--c--globals--colors--ogre-odor-is-orange-indeed); }
.bg-primary-0 { background-color: var(--c--contextuals--background--primary--0); } .bg-primary { background-color: var(--c--contextuals--background--primary); }
.bg-primary-1 { background-color: var(--c--contextuals--background--primary--1); }
.bg-primary-2 { background-color: var(--c--contextuals--background--primary--2); }
.bg-primary-3 { background-color: var(--c--contextuals--background--primary--3); }
.bg-primary-4 { background-color: var(--c--contextuals--background--primary--4); }
.bg-primary-5 { background-color: var(--c--contextuals--background--primary--5); }
.bg-primary-6 { background-color: var(--c--contextuals--background--primary--6); }
.fw-medium { font-weight: var(--c--globals--font--weights--medium); } .fw-medium { font-weight: var(--c--globals--font--weights--medium); }
.fs-m { .fs-m {
font-size: var(--c--globals--font--sizes--m); font-size: var(--c--globals--font--sizes--m);
letter-spacing: var(--c--globals--font--letterspacings--m); letter-spacing: var(--c--globals--font--letterspacings--m);
} }
.f-base { font-family: var(--c--globals--font--families--base); } .f-base { font-family: var(--c--globals--font--families--base); }
.m-s { margin: var(--c--globals--spacings--s); }.mb-s { margin-bottom: var(--c--globals--spacings--s); }.mt-s { margin-top: var(--c--globals--spacings--s); }.ml-s { margin-left: var(--c--globals--spacings--s); }.mr-s { margin-right: var(--c--globals--spacings--s); } .m-s { margin: var(--c--globals--spacings--s); }.mb-s { margin-bottom: var(--c--globals--spacings--s); }.mt-s { margin-top: var(--c--globals--spacings--s); }.ml-s { margin-left: var(--c--globals--spacings--s); }.mr-s { margin-right: var(--c--globals--spacings--s); }
.p-s { padding: var(--c--globals--spacings--s); }.pb-s { padding-bottom: var(--c--globals--spacings--s); }.pt-s { padding-top: var(--c--globals--spacings--s); }.pl-s { padding-left: var(--c--globals--spacings--s); }.pr-s { padding-right: var(--c--globals--spacings--s); } .p-s { margin: var(--c--globals--spacings--s); }.pb-s { margin-bottom: var(--c--globals--spacings--s); }.pt-s { margin-top: var(--c--globals--spacings--s); }.pl-s { margin-left: var(--c--globals--spacings--s); }.pr-s { margin-right: var(--c--globals--spacings--s); }
.border-clr-primary-0 { border-color: var(--c--contextuals--background--primary--0); } .border-clr-primary { border-color: var(--c--contextuals--border--primary); }
.border-thin-primary-0 { border: 1px solid var(--c--contextuals--background--primary--0); } .border-thin-primary { border: 1px solid var(--c--contextuals--border--primary); }
.border-clr-primary-1 { border-color: var(--c--contextuals--background--primary--1); } .clr-content-primary { color: var(--c--contextuals--content--primary); }
.border-thin-primary-1 { border: 1px solid var(--c--contextuals--background--primary--1); }
.border-clr-primary-2 { border-color: var(--c--contextuals--background--primary--2); }
.border-thin-primary-2 { border: 1px solid var(--c--contextuals--background--primary--2); }
.border-clr-primary-3 { border-color: var(--c--contextuals--background--primary--3); }
.border-thin-primary-3 { border: 1px solid var(--c--contextuals--background--primary--3); }
.border-clr-primary-4 { border-color: var(--c--contextuals--background--primary--4); }
.border-thin-primary-4 { border: 1px solid var(--c--contextuals--background--primary--4); }
.border-clr-primary-5 { border-color: var(--c--contextuals--background--primary--5); }
.border-thin-primary-5 { border: 1px solid var(--c--contextuals--background--primary--5); }
.border-clr-primary-6 { border-color: var(--c--contextuals--background--primary--6); }
.border-thin-primary-6 { border: 1px solid var(--c--contextuals--background--primary--6); }
.clr-content-primary-0 { color: var(--c--contextuals--content--primary--0); }
.clr-content-primary-1 { color: var(--c--contextuals--content--primary--1); }
.clr-content-primary-2 { color: var(--c--contextuals--content--primary--2); }
.clr-content-primary-3 { color: var(--c--contextuals--content--primary--3); }
.clr-content-primary-4 { color: var(--c--contextuals--content--primary--4); }
.clr-content-primary-5 { color: var(--c--contextuals--content--primary--5); }
.clr-content-primary-6 { color: var(--c--contextuals--content--primary--6); }
" "
`); `);
}); });

View File

@@ -5,8 +5,47 @@ import { Generator, resolveRefs } from "Generators/index";
import { put } from "Utils/Files"; import { put } from "Utils/Files";
import { Tokens } from "TokensGenerator"; import { Tokens } from "TokensGenerator";
/**
* Interface for objects containing path and value information
*/
interface PathValueObject {
path: string[];
value: any;
}
export const THEME_CLASSNAME_PREFIX = "cunningham-theme--"; export const THEME_CLASSNAME_PREFIX = "cunningham-theme--";
/**
* Creates an array of objects containing path arrays and leaf values from a nested object
* @param obj - The object to traverse
* @param currentPath - Current path being built (used internally for recursion)
* @returns Array of objects with 'path' (array of keys) and 'value' (leaf value) properties
*/
export function createPathValueArray(
obj: any,
currentPath: string[] = [],
): PathValueObject[] {
const result: PathValueObject[] = [];
Object.entries(obj).forEach(([key, value]) => {
const newPath = [...currentPath, key];
// Check if the value is an object and not null/undefined
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
// Recursively process nested objects
result.push(...createPathValueArray(value, newPath));
} else {
// This is a leaf value
result.push({
path: newPath,
value,
});
}
});
return result;
}
export const cssGenerator: Generator = async (tokens, opts) => { export const cssGenerator: Generator = async (tokens, opts) => {
// Replace refs by CSS variables. // Replace refs by CSS variables.
tokens = resolveRefs(tokens, (ref) => { tokens = resolveRefs(tokens, (ref) => {
@@ -72,74 +111,42 @@ function generateColorClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generateBgClasses(tokens: Tokens) { function generateBgClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const bgContextual = createPathValueArray(
tokens.themes.default.contextuals.background,
return Object.keys(flatTokens) );
.filter((key) => { const bgContextualClasses = bgContextual.map((key) => {
// Only include keys that are related to colors (globals.colors or contextuals.background) return `.bg-${key.path.join("-")} { background-color: var(--${Config.sass.varPrefix}contextuals--background--${key.path.join("--")}); }`;
return ( });
key.startsWith("globals--colors--") || const bgGlobal = createPathValueArray(tokens.themes.default.globals.colors);
key.startsWith("contextuals--background--") const bgGlobalClasses = bgGlobal.map((key) => {
); return `.bg-${key.path.join("-")} { background-color: var(--${Config.sass.varPrefix}globals--colors--${key.path.join("--")}); }`;
}) });
.map((key) => { return [...bgGlobalClasses, ...bgContextualClasses];
// Convert the flat key to CSS class name
let className = key;
// Handle globals.colors
if (key.startsWith("globals--colors--")) {
className = key.replace("globals--colors--", "");
}
// Handle contextuals.background
else if (key.startsWith("contextuals--background--")) {
className = key.replace("contextuals--background--", "");
}
// Convert separators to hyphens for CSS class names
className = className.replace(
new RegExp(Config.sass.varSeparator, "g"),
"-",
);
const a = `.bg-${className} { background-color: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`;
// console.log(a);
return a;
});
} }
function generateBorderClasses(tokens: Tokens) { function generateBorderClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const bgContextual = createPathValueArray(
tokens.themes.default.contextuals.border,
return Object.keys(flatTokens) );
.filter((key) => { const bgContextualClasses = bgContextual
// Only include keys that are related to borders (contextuals.border) .map((key) => {
return key.startsWith("contextuals--border--");
})
.flatMap((key) => {
// Convert the flat key to CSS class name
const className = key.replace("contextuals--border--", "");
return [ return [
`.border-clr-${className} { border-color: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`, `.border-clr-${key.path.join("-")} { border-color: var(--${Config.sass.varPrefix}contextuals--border--${key.path.join("--")}); }`,
`.border-thin-${className} { border: 1px solid var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`, `.border-thin-${key.path.join("-")} { border: 1px solid var(--${Config.sass.varPrefix}contextuals--border--${key.path.join("--")}); }`,
]; ];
}); })
.flat();
return bgContextualClasses;
} }
function generateContentClasses(tokens: Tokens) { function generateContentClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const bgContextual = createPathValueArray(
tokens.themes.default.contextuals.content,
return Object.keys(flatTokens) );
.filter((key) => { return bgContextual.map((key) => {
// Only include keys that are related to content (contextuals.content) return `.clr-content-${key.path.join("-")} { color: var(--${Config.sass.varPrefix}contextuals--content--${key.path.join("--")}); }`;
return key.startsWith("contextuals--content--"); });
})
.map((key) => {
// Convert the flat key to CSS class name
const className = key.replace("contextuals--content--", "");
return `.clr-content-${className} { color: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`;
});
} }
/** /**
@@ -149,19 +156,13 @@ function generateContentClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generateClrClasses(tokens: Tokens) { function generateClrClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const bgContextual = createPathValueArray(
tokens.themes.default.globals.colors,
);
return Object.keys(flatTokens) return bgContextual.map((key) => {
.filter((key) => { return `.clr-${key.path.join("-")} { color: var(--${Config.sass.varPrefix}globals--colors--${key.path.join("--")}); }`;
// Only include keys that are related to colors (globals.colors) });
return key.startsWith("globals--colors--");
})
.map((key) => {
// Convert the flat key to CSS class name
const className = key.replace("globals--colors--", "");
return `.clr-${className} { color: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`;
});
} }
function generateFontClasses(tokens: Tokens) { function generateFontClasses(tokens: Tokens) {
@@ -179,19 +180,12 @@ function generateFontClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generateFwClasses(tokens: Tokens) { function generateFwClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const tokensWeights = createPathValueArray(
tokens.themes.default.globals.font.weights,
return Object.keys(flatTokens) );
.filter((key) => { return tokensWeights.map((key) => {
// Only include keys that are related to font weights (globals.font.weights) return `.fw-${key.path.join("-")} { font-weight: var(--${Config.sass.varPrefix}globals--font--weights--${key.path.join("--")}); }`;
return key.startsWith("globals--font--weights--"); });
})
.map((key) => {
// Convert the flat key to CSS class name
const className = key.replace("globals--font--weights--", "");
return `.fw-${className} { font-weight: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`;
});
} }
/** /**
@@ -201,22 +195,15 @@ function generateFwClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generateFsClasses(tokens: Tokens) { function generateFsClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const tokensSizes = createPathValueArray(
tokens.themes.default.globals.font.sizes,
return Object.keys(flatTokens) );
.filter((key) => { return tokensSizes.map((key) => {
// Only include keys that are related to font sizes (globals.font.sizes) return `.fs-${key.path.join("-")} {
return key.startsWith("globals--font--sizes--"); font-size: var(--${Config.sass.varPrefix}globals--font--sizes--${key.path.join("--")});
}) letter-spacing: var(--${Config.sass.varPrefix}globals--font--letterspacings--${key.path.join("--")});
.map((key) => { }`;
// Convert the flat key to CSS class name });
const className = key.replace("globals--font--sizes--", "");
return `.fs-${className} {
font-size: var(--${Config.sass.varPrefix}${key.toLowerCase()});
letter-spacing: var(--${Config.sass.varPrefix}${key.replace("sizes", "letterspacings").toLowerCase()});
}`;
});
} }
/** /**
@@ -226,19 +213,13 @@ function generateFsClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generateFClasses(tokens: Tokens) { function generateFClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const tokensFamilies = createPathValueArray(
tokens.themes.default.globals.font.families,
);
return Object.keys(flatTokens) return tokensFamilies.map((key) => {
.filter((key) => { return `.f-${key.path.join("-")} { font-family: var(--${Config.sass.varPrefix}globals--font--families--${key.path.join("--")}); }`;
// Only include keys that are related to font families (globals.font.families) });
return key.startsWith("globals--font--families--");
})
.map((key) => {
// Convert the flat key to CSS class name
const className = key.replace("globals--font--families--", "");
return `.f-${className} { font-family: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`;
});
} }
function generateSpacingClasses(tokens: Tokens) { function generateSpacingClasses(tokens: Tokens) {
@@ -252,25 +233,18 @@ function generateSpacingClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generateMarginClasses(tokens: Tokens) { function generateMarginClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const tokensSpacings = createPathValueArray(
tokens.themes.default.globals.spacings,
return Object.keys(flatTokens) );
.filter((key) => { return tokensSpacings.map((key) => {
// Only include keys that are related to spacings (globals.spacings) return [
return key.startsWith("globals--spacings--"); `.m-${key.path.join("-")} { margin: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
}) `.mb-${key.path.join("-")} { margin-bottom: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
.map((key) => { `.mt-${key.path.join("-")} { margin-top: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
// Convert the flat key to CSS class name `.ml-${key.path.join("-")} { margin-left: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
const className = key.replace("globals--spacings--", ""); `.mr-${key.path.join("-")} { margin-right: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
].join("");
return [ });
`.m-${className} { margin: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.mb-${className} { margin-bottom: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.mt-${className} { margin-top: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.ml-${className} { margin-left: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.mr-${className} { margin-right: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
].join("");
});
} }
/** /**
@@ -280,23 +254,16 @@ function generateMarginClasses(tokens: Tokens) {
* @param tokens * @param tokens
*/ */
function generatePaddingClasses(tokens: Tokens) { function generatePaddingClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator); const tokensSpacings = createPathValueArray(
tokens.themes.default.globals.spacings,
return Object.keys(flatTokens) );
.filter((key) => { return tokensSpacings.map((key) => {
// Only include keys that are related to spacings (globals.spacings) return [
return key.startsWith("globals--spacings--"); `.p-${key.path.join("-")} { margin: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
}) `.pb-${key.path.join("-")} { margin-bottom: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
.map((key) => { `.pt-${key.path.join("-")} { margin-top: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
// Convert the flat key to CSS class name `.pl-${key.path.join("-")} { margin-left: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
const className = key.replace("globals--spacings--", ""); `.pr-${key.path.join("-")} { margin-right: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
].join("");
return [ });
`.p-${className} { padding: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.pb-${className} { padding-bottom: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.pt-${className} { padding-top: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.pl-${className} { padding-left: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.pr-${className} { padding-right: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
].join("");
});
} }

View File

@@ -25,6 +25,10 @@ describe("JsGenerator", () => {
expect(fs.existsSync(tokensFile)).toEqual(true); expect(fs.existsSync(tokensFile)).toEqual(true);
// Verify file content exists and contains expected structure // Verify file content exists and contains expected structure
const content = fs.readFileSync(tokensFile).toString(); const content = fs.readFileSync(tokensFile).toString();
expect(fs.readFileSync(tokensFile).toString()).toMatchInlineSnapshot(`
"export const tokens = {"themes":{"default":{"globals":{"colors":{"primary":"#055FD2","secondary":"#DA0000","ternary-900":"#022858","ogre-odor-is-orange-indeed":"#FD5240"},"font":{"sizes":{"m":"1rem"},"weights":{"medium":400},"families":{"base":"Roboto"}},"spacings":{"s":"1rem"},"transitions":{"ease":"linear"},"input":{"border-color":"#022858"}},"contextuals":{"background":{"primary":"#055FD2"},"content":{"primary":"#055FD2"},"border":{"primary":"#055FD2"}},"theme":{"colors":{"primary":"#055FD2","secondary":"#DA0000","ternary-900":"#022858","ogre-odor-is-orange-indeed":"#FD5240"},"font":{"sizes":{"m":"1rem"},"weights":{"medium":400},"families":{"base":"Roboto"}},"spacings":{"s":"1rem"},"transitions":{"ease":"linear"},"input":{"border-color":"#022858"}},"components":{"button":{"font-family":"Times New Roman,Helvetica Neue,Segoe UI"}}},"dark":{"globals":{"colors":{"primary":"black"}}},"custom":{"globals":{"colors":{"primary":"green"}}}}};
"
`);
expect(content).toBeTruthy(); expect(content).toBeTruthy();
expect(content.length).toBeGreaterThan(0); expect(content.length).toBeGreaterThan(0);
expect(content).toContain("export const tokens = {"); expect(content).toContain("export const tokens = {");

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,8 @@ import { tokens } from "./cunningham-tokens";
export type Configuration = typeof tokens; export type Configuration = typeof tokens;
export type DefaultTokens = (typeof tokens)["themes"]["default"]; export type DefaultTokens = (typeof tokens)["themes"]["default"];
export type DarkTokens = (typeof tokens)["themes"]["dark"];
export const defaultTokens = tokens.themes.default; export const defaultTokens = tokens.themes.default;
export const darkTokens = tokens.themes.dark;
export const defaultThemes = tokens.themes; export const defaultThemes = tokens.themes;
/** /**