blueprint/lib/CSSGenerator.js
2025-01-21 14:00:09 +01:00

402 lines
13 KiB
JavaScript

const { STYLE_MAPPINGS } = require("./mappings");
class CSSGenerator {
/**
* Creates a new CSS generator instance.
* @param {Object} [options] - Options object
* @param {boolean} [options.minified=true] - Minify generated class names
* @param {boolean} [options.debug=false] - Enable debug logging
*/
constructor(options = {}) {
this.options = options;
this.cssRules = new Map();
this.classCounter = 0;
if (this.options.debug) {
console.log(
"[CSSGenerator] Initialized with options:",
JSON.stringify(options, null, 2)
);
}
}
/**
* Generates a class name for the given element type, based on the counter
* and the minified option. If minified is true, the class name will be a
* single lowercase letter (a-z), or a single uppercase letter (A-Z) if
* the counter is between 26 and 51. Otherwise, it will be a complex
* class name (e.g. "zabcdefg") with a counter starting from 52.
*
* @param {string} elementType - The type of the element for which to
* generate a class name.
* @return {string} The generated class name.
*/
generateClassName(elementType) {
if (this.options.debug) {
console.log(
`\n[CSSGenerator] Generating class name for element type: "${elementType}"`
);
console.log(`[CSSGenerator] Current class counter: ${this.classCounter}`);
}
let className;
if (!this.options.minified) {
className = `blueprint-${elementType}-${this.classCounter++}`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Generated readable class name: "${className}"`
);
}
return className;
}
if (this.classCounter < 26) {
className = String.fromCharCode(97 + this.classCounter++);
if (this.options.debug) {
console.log(
`[CSSGenerator] Generated lowercase class name: "${className}" (counter: ${
this.classCounter - 1
})`
);
}
return className;
}
if (this.classCounter < 52) {
className = String.fromCharCode(65 + (this.classCounter++ - 26));
if (this.options.debug) {
console.log(
`[CSSGenerator] Generated uppercase class name: "${className}" (counter: ${
this.classCounter - 1
})`
);
}
return className;
}
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const base = chars.length;
let num = this.classCounter++ - 52;
let result = "";
do {
result = chars[num % base] + result;
num = Math.floor(num / base);
} while (num > 0);
result = "z" + result;
if (this.options.debug) {
console.log(
`[CSSGenerator] Generated complex class name: "${result}" (counter: ${
this.classCounter - 1
})`
);
}
return result;
}
/**
* Converts a node to CSS properties, using the style mappings to process
* the node's properties. The generated CSS properties are returned as a
* Map, where each key is a CSS property name and each value is the value
* for that property.
*
* @param {Object} node - The node to convert
* @return {Object} - The generated CSS properties and nested rules
*/
nodeToCSSProperties(node) {
if (this.options.debug) {
console.log(`\n[CSSGenerator] Converting node to CSS properties`);
console.log(`[CSSGenerator] Node tag: "${node.tag}"`);
console.log("[CSSGenerator] Node properties:", node.props);
}
const cssProps = new Map();
const nestedRules = new Map();
node.props.forEach((prop) => {
if (typeof prop === "object") {
if (this.options.debug) {
console.log(`[CSSGenerator] Skipping object property:`, prop);
}
return;
}
const [name, value] = prop.split(/[-:]/);
if (this.options.debug) {
console.log(
`\n[CSSGenerator] Processing property - name: "${name}", value: "${value}"`
);
}
// This is for customization of css properties
if (name === "width" && !isNaN(value)) {
cssProps.set("width", `${value}% !important`);
cssProps.set("max-width", "none !important");
if (this.options.debug) {
console.log(
`[CSSGenerator] Set width: ${value}% !important and max-width: none !important`
);
}
return;
}
if (name === "height" && !isNaN(value)) {
cssProps.set("height", `${value}% !important`);
cssProps.set("max-height", "none !important");
if (this.options.debug) {
console.log(
`[CSSGenerator] Set height: ${value}% !important and max-height: none !important`
);
}
return;
}
if (name === "padding" && !isNaN(value)) {
cssProps.set("padding", `${value}px !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set padding: ${value}px !important`);
}
return;
}
if (name === "margin" && !isNaN(value)) {
cssProps.set("margin", `${value}px !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set margin: ${value}px !important`);
}
return;
}
if (name === "marginTop" && !isNaN(value)) {
cssProps.set("margin-top", `${value}px !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set margin-top: ${value}px !important`);
}
return;
}
if (name === "marginBottom" && !isNaN(value)) {
cssProps.set("margin-bottom", `${value}px !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set margin-bottom: ${value}px !important`);
}
return;
}
if (name === "marginLeft" && !isNaN(value)) {
cssProps.set("margin-left", `${value}px !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set margin-left: ${value}px !important`);
}
return;
}
if (name === "marginRight" && !isNaN(value)) {
cssProps.set("margin-right", `${value}px !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set margin-right: ${value}px !important`);
}
return;
}
if (name === "color") {
cssProps.set("color", `${value} !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set color: ${value} !important`);
}
return;
}
if (name === "backgroundColor") {
cssProps.set("background-color", `${value} !important`);
if (this.options.debug) {
console.log(`[CSSGenerator] Set background-color: ${value} !important`);
}
return;
}
const style = STYLE_MAPPINGS[name];
if (style) {
if (this.options.debug) {
console.log(`[CSSGenerator] Processing style mapping for: "${name}"`);
}
Object.entries(style).forEach(([key, baseValue]) => {
if (typeof baseValue === "object") {
if (key.startsWith(":") || key.startsWith(">")) {
nestedRules.set(key, baseValue);
if (this.options.debug) {
console.log(
`[CSSGenerator] Added nested rule: "${key}" =>`,
baseValue
);
}
} else {
let finalValue = baseValue;
if (value && key === "gridTemplateColumns" && !isNaN(value)) {
finalValue = `repeat(${value}, 1fr)`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Set grid template columns: ${finalValue}`
);
}
}
cssProps.set(key, finalValue);
if (this.options.debug) {
console.log(
`[CSSGenerator] Set CSS property: "${key}" = "${finalValue}"`
);
}
}
} else {
let finalValue = baseValue;
if (value && key === "gridTemplateColumns" && !isNaN(value)) {
finalValue = `repeat(${value}, 1fr)`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Set grid template columns: ${finalValue}`
);
}
}
cssProps.set(key, finalValue);
if (this.options.debug) {
console.log(
`[CSSGenerator] Set CSS property: "${key}" = "${finalValue}"`
);
}
}
});
}
});
if (this.options.debug) {
console.log("\n[CSSGenerator] CSS properties generation complete");
console.log(`[CSSGenerator] Generated ${cssProps.size} CSS properties`);
console.log(`[CSSGenerator] Generated ${nestedRules.size} nested rules`);
}
return { cssProps, nestedRules };
}
/**
* Generates the CSS code for the given style mappings. If minified is true,
* the generated CSS will be minified. Otherwise, it will be formatted with
* indentation and newlines.
*
* @return {string} The generated CSS code
*/
generateCSS() {
if (this.options.debug) {
console.log("\n[CSSGenerator] Starting CSS generation");
console.log(`[CSSGenerator] Processing ${this.cssRules.size} rule sets`);
}
/**
* Converts a camelCase string to kebab-case (lowercase with hyphens
* separating words)
*
* @param {string} str The string to convert
* @return {string} The converted string
*/
const toKebabCase = (str) =>
str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
let css = "";
this.cssRules.forEach((props, selector) => {
if (props.cssProps.size > 0) {
if (this.options.debug) {
console.log(
`\n[CSSGenerator] Generating CSS for selector: "${selector}"`
);
console.log(
`[CSSGenerator] Properties count: ${props.cssProps.size}`
);
}
css += `${selector} {${this.options.minified ? "" : "\n"}`;
props.cssProps.forEach((value, prop) => {
const cssProperty = toKebabCase(prop);
css += `${
this.options.minified ? "" : " "
}${cssProperty}: ${value};${this.options.minified ? "" : "\n"}`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Added property: ${cssProperty}: ${value}`
);
}
});
css += `}${this.options.minified ? "" : "\n"}`;
}
if (props.nestedRules.size > 0) {
if (this.options.debug) {
console.log(
`\n[CSSGenerator] Processing ${props.nestedRules.size} nested rules for "${selector}"`
);
}
props.nestedRules.forEach((rules, nestedSelector) => {
const fullSelector = nestedSelector.startsWith(">")
? `${selector} ${nestedSelector}`
: `${selector}${nestedSelector}`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Generating nested selector: "${fullSelector}"`
);
}
css += `${fullSelector} {${this.options.minified ? "" : "\n"}`;
Object.entries(rules).forEach(([prop, value]) => {
if (typeof value === "object") {
const pseudoSelector = `${fullSelector}${prop}`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Generating pseudo-selector: "${pseudoSelector}"`
);
}
css += `}${this.options.minified ? "" : "\n"}${pseudoSelector} {${
this.options.minified ? "" : "\n"
}`;
Object.entries(value).forEach(([nestedProp, nestedValue]) => {
const cssProperty = toKebabCase(nestedProp);
css += `${
this.options.minified ? "" : " "
}${cssProperty}: ${nestedValue};${
this.options.minified ? "" : "\n"
}`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Added nested property: ${cssProperty}: ${nestedValue}`
);
}
});
} else {
const cssProperty = toKebabCase(prop);
css += `${
this.options.minified ? "" : " "
}${cssProperty}: ${value};${this.options.minified ? "" : "\n"}`;
if (this.options.debug) {
console.log(
`[CSSGenerator] Added property: ${cssProperty}: ${value}`
);
}
}
});
css += `}${this.options.minified ? "" : "\n"}`;
});
}
});
if (this.options.debug) {
console.log("\n[CSSGenerator] CSS generation complete");
console.log(
`[CSSGenerator] Generated ${css.split("\n").length} lines of CSS`
);
}
return css;
}
}
module.exports = CSSGenerator;