♻️(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;
}
.cunningham-theme--dark{
--c--theme--colors--primary: black;
--c--globals--colors--primary: black;
}
.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;
}
.cunningham-theme--dark{
--c--theme--colors--primary: black;
--c--globals--colors--primary: black;
}
.cunningham-theme--custom{
--c--theme--colors--primary: green;
--c--globals--colors--primary: green;
} .clr-primary { color: var(--c--globals--colors--primary); }
.clr-secondary { color: var(--c--globals--colors--secondary); }
.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-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-primary-0 { background-color: var(--c--contextuals--background--primary--0); }
.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); }
.bg-primary { background-color: var(--c--contextuals--background--primary); }
.fw-medium { font-weight: var(--c--globals--font--weights--medium); }
.fs-m {
font-size: var(--c--globals--font--sizes--m);
letter-spacing: var(--c--globals--font--letterspacings--m);
}
font-size: var(--c--globals--font--sizes--m);
letter-spacing: var(--c--globals--font--letterspacings--m);
}
.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); }
.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); }
.border-clr-primary-0 { border-color: var(--c--contextuals--background--primary--0); }
.border-thin-primary-0 { border: 1px solid var(--c--contextuals--background--primary--0); }
.border-clr-primary-1 { border-color: var(--c--contextuals--background--primary--1); }
.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); }
.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 { border-color: var(--c--contextuals--border--primary); }
.border-thin-primary { border: 1px solid var(--c--contextuals--border--primary); }
.clr-content-primary { color: var(--c--contextuals--content--primary); }
"
`);
});

View File

@@ -5,8 +5,47 @@ import { Generator, resolveRefs } from "Generators/index";
import { put } from "Utils/Files";
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--";
/**
* 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) => {
// Replace refs by CSS variables.
tokens = resolveRefs(tokens, (ref) => {
@@ -72,74 +111,42 @@ function generateColorClasses(tokens: Tokens) {
* @param tokens
*/
function generateBgClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to colors (globals.colors or contextuals.background)
return (
key.startsWith("globals--colors--") ||
key.startsWith("contextuals--background--")
);
})
.map((key) => {
// 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;
});
const bgContextual = createPathValueArray(
tokens.themes.default.contextuals.background,
);
const bgContextualClasses = bgContextual.map((key) => {
return `.bg-${key.path.join("-")} { background-color: var(--${Config.sass.varPrefix}contextuals--background--${key.path.join("--")}); }`;
});
const bgGlobal = createPathValueArray(tokens.themes.default.globals.colors);
const bgGlobalClasses = bgGlobal.map((key) => {
return `.bg-${key.path.join("-")} { background-color: var(--${Config.sass.varPrefix}globals--colors--${key.path.join("--")}); }`;
});
return [...bgGlobalClasses, ...bgContextualClasses];
}
function generateBorderClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to borders (contextuals.border)
return key.startsWith("contextuals--border--");
})
.flatMap((key) => {
// Convert the flat key to CSS class name
const className = key.replace("contextuals--border--", "");
const bgContextual = createPathValueArray(
tokens.themes.default.contextuals.border,
);
const bgContextualClasses = bgContextual
.map((key) => {
return [
`.border-clr-${className} { border-color: var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.border-thin-${className} { border: 1px solid var(--${Config.sass.varPrefix}${key.toLowerCase()}); }`,
`.border-clr-${key.path.join("-")} { border-color: var(--${Config.sass.varPrefix}contextuals--border--${key.path.join("--")}); }`,
`.border-thin-${key.path.join("-")} { border: 1px solid var(--${Config.sass.varPrefix}contextuals--border--${key.path.join("--")}); }`,
];
});
})
.flat();
return bgContextualClasses;
}
function generateContentClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to content (contextuals.content)
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()}); }`;
});
const bgContextual = createPathValueArray(
tokens.themes.default.contextuals.content,
);
return bgContextual.map((key) => {
return `.clr-content-${key.path.join("-")} { color: var(--${Config.sass.varPrefix}contextuals--content--${key.path.join("--")}); }`;
});
}
/**
@@ -149,19 +156,13 @@ function generateContentClasses(tokens: Tokens) {
* @param 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)
.filter((key) => {
// 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()}); }`;
});
return bgContextual.map((key) => {
return `.clr-${key.path.join("-")} { color: var(--${Config.sass.varPrefix}globals--colors--${key.path.join("--")}); }`;
});
}
function generateFontClasses(tokens: Tokens) {
@@ -179,19 +180,12 @@ function generateFontClasses(tokens: Tokens) {
* @param tokens
*/
function generateFwClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to font weights (globals.font.weights)
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()}); }`;
});
const tokensWeights = createPathValueArray(
tokens.themes.default.globals.font.weights,
);
return tokensWeights.map((key) => {
return `.fw-${key.path.join("-")} { font-weight: var(--${Config.sass.varPrefix}globals--font--weights--${key.path.join("--")}); }`;
});
}
/**
@@ -201,22 +195,15 @@ function generateFwClasses(tokens: Tokens) {
* @param tokens
*/
function generateFsClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to font sizes (globals.font.sizes)
return key.startsWith("globals--font--sizes--");
})
.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()});
}`;
});
const tokensSizes = createPathValueArray(
tokens.themes.default.globals.font.sizes,
);
return tokensSizes.map((key) => {
return `.fs-${key.path.join("-")} {
font-size: var(--${Config.sass.varPrefix}globals--font--sizes--${key.path.join("--")});
letter-spacing: var(--${Config.sass.varPrefix}globals--font--letterspacings--${key.path.join("--")});
}`;
});
}
/**
@@ -226,19 +213,13 @@ function generateFsClasses(tokens: Tokens) {
* @param 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)
.filter((key) => {
// 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()}); }`;
});
return tokensFamilies.map((key) => {
return `.f-${key.path.join("-")} { font-family: var(--${Config.sass.varPrefix}globals--font--families--${key.path.join("--")}); }`;
});
}
function generateSpacingClasses(tokens: Tokens) {
@@ -252,25 +233,18 @@ function generateSpacingClasses(tokens: Tokens) {
* @param tokens
*/
function generateMarginClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to spacings (globals.spacings)
return key.startsWith("globals--spacings--");
})
.map((key) => {
// Convert the flat key to CSS class name
const className = key.replace("globals--spacings--", "");
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("");
});
const tokensSpacings = createPathValueArray(
tokens.themes.default.globals.spacings,
);
return tokensSpacings.map((key) => {
return [
`.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("--")}); }`,
`.mt-${key.path.join("-")} { margin-top: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
`.ml-${key.path.join("-")} { margin-left: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
`.mr-${key.path.join("-")} { margin-right: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
].join("");
});
}
/**
@@ -280,23 +254,16 @@ function generateMarginClasses(tokens: Tokens) {
* @param tokens
*/
function generatePaddingClasses(tokens: Tokens) {
const flatTokens = flatify(tokens.themes.default, Config.sass.varSeparator);
return Object.keys(flatTokens)
.filter((key) => {
// Only include keys that are related to spacings (globals.spacings)
return key.startsWith("globals--spacings--");
})
.map((key) => {
// Convert the flat key to CSS class name
const className = key.replace("globals--spacings--", "");
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("");
});
const tokensSpacings = createPathValueArray(
tokens.themes.default.globals.spacings,
);
return tokensSpacings.map((key) => {
return [
`.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("--")}); }`,
`.pt-${key.path.join("-")} { margin-top: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
`.pl-${key.path.join("-")} { margin-left: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
`.pr-${key.path.join("-")} { margin-right: var(--${Config.sass.varPrefix}globals--spacings--${key.path.join("--")}); }`,
].join("");
});
}

View File

@@ -25,6 +25,10 @@ describe("JsGenerator", () => {
expect(fs.existsSync(tokensFile)).toEqual(true);
// Verify file content exists and contains expected structure
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.length).toBeGreaterThan(0);
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 DefaultTokens = (typeof tokens)["themes"]["default"];
export type DarkTokens = (typeof tokens)["themes"]["dark"];
export const defaultTokens = tokens.themes.default;
export const darkTokens = tokens.themes.dark;
export const defaultThemes = tokens.themes;
/**