www/node_modules/@siimple/core/index.js
2022-08-03 14:47:34 +02:00

302 lines
10 KiB
JavaScript

// Css properties to scales mapping
const scales = {
backgroundColor: "colors",
borderColor: "colors",
borderBottomColor: "colors",
borderLeftColor: "colors",
borderRightColor: "colors",
borderTopColor: "colors",
borderRadius: "radius",
borderBottomLeftRadius: "radius",
borderBottomRightRadius: "radius",
borderTopLeftRadius: "radius",
borderTopRightRadius: "radius",
borderWidth: "sizes",
borderBottomWidth: "sizes",
borderLeftWidth: "sizes",
borderRightWidth: "sizes",
borderTopWidth: "sizes",
bottom: "spacing",
boxShadow: "shadows",
color: "colors",
fill: "colors",
fontFamily: "fonts",
fontSize: "fontSizes",
fontWeight: "fontWeights",
height: "sizes",
left: "spacing",
lineHeight: "lineHeights",
margin: "spacing",
marginBottom: "spacing",
marginLeft: "spacing",
marginRight: "spacing",
marginTop: "spacing",
maxHeight: "sizes",
maxWidth: "sizes",
minHeight: "sizes",
minWidth: "sizes",
opacity: "opacities",
padding: "spacing",
paddingBottom: "spacing",
paddingLeft: "spacing",
paddingRight: "spacing",
paddingTop: "spacing",
right: "spacing",
textShadow: "shadows",
top: "spacing",
width: "sizes",
};
// CSS aliases
const aliases = {
bc: ["border-color"],
bg: ["background-color"],
m: ["margin"],
mb: ["margin-bottom"],
ml: ["margin-left"],
mr: ["margin-right"],
mt: ["margin-top"],
mx: ["margin-left", "margin-right"],
my: ["margin-top", "margin-bottom"],
p: ["padding"],
pb: ["padding-bottom"],
pl: ["padding-left"],
pr: ["padding-right"],
pt: ["padding-top"],
px: ["padding-left", "padding-right"],
py: ["padding-top", "padding-bottom"],
radius: ["border-radius"],
size: ["width", "height"],
};
// Merge two object
const mergeObject = (source, target) => ({...source, ...target});
// Get value in object from path
const getInObject = (obj, path) => {
return path.split(".").reduce((o, p) => o?.[p], obj);
};
// Exclude a field from the specified object
const excludeInObject = (obj, field) => {
return Object.fromEntries(Object.entries(obj).filter(e => e[0] !== field));
};
// Tiny reducer alias
const toObject = (list, fn) => list.reduce(fn, {});
// Replace variables in the provided string
const format = (str, vars) => {
return str.replace(/\{\{\s*([^{}\s]+)\s*\}\}/g, (match, key) => {
return typeof vars[key] !== "undefined" ? vars[key].toString() : match;
});
};
// Merge configurations
export const mergeConfig = (source, target) => ({
...source,
...target,
prefix: target.prefix || source.prefix || "",
breakpoints: target.breakpoints || source.breakpoints || {},
root: mergeObject(source.root || {}, target.root || {}),
styles: mergeStyles(source.styles || {}, target.styles || {}),
});
// Parse CSS property name
const parseProperty = prop => {
return (aliases[prop] || [prop]).map(item => {
return item.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
})
};
// Wrap CSS Rule
const wrapRule = (ruleName, ruleContent, separator) => {
return `${ruleName} {${separator || ""}${ruleContent}${separator || ""}}`;
};
// Generate media query rule
const buildMediaQuery = values => {
const conditions = Object.keys(values).map(key => {
if (values[key] === null || (key !== "min" && key !== "max")) {
return null; // Not valid condition
}
// Return this condition
return `(${key}-width: ${values[key]})`;
});
// Return the media rule
return `@media screen and ${conditions.filter(item => !!item).join(" and ")}`;
};
// Merge CSS styles
export const mergeStyles = (source, target) => {
Object.keys(target).forEach(key => {
// Check for @font-face attribute
if (key === "@font-face") {
if (typeof source["@font-face"] !== "undefined") {
source[key] = [source[key]].flat(1).concat(target[key]).flat(1);
return;
}
}
// Check for object property --> deep merge
else if (typeof source[key] === "object" && typeof target[key] === "object") {
return mergeStyles(source[key], target[key]);
}
// Other value --> override source property
source[key] = target[key];
});
return source;
};
// Build css value
export const buildValue = (property, value, config, vars) => {
const values = [value].flat(1);
if (values[0] === "value" && typeof vars["value"] !== "undefined") {
values[0] = vars["value"]; // Replace value for vars
}
if (scales[property] && typeof values[0] === "string") {
const key = scales[property];
values[0] = config[key]?.[values[0]] || values[0];
}
return values.join(" ");
};
// Build mixin styles
export const buildMixin = (styles, theme, prev) => {
prev = prev || new Set();
if (typeof styles.apply === "string" && styles.apply) {
// Check for circular mixins found
if (prev.has(styles.apply)) {
const items = Array.from(prev);
throw new Error(`Circular mixins found: ${items.join("->")}->${styles.apply}`);
}
// Apply styles from this mixin
prev.add(styles.apply);
let appliedStyles = getInObject(theme, styles.apply) || {};
if (appliedStyles.default && typeof appliedStyles.default === "object") {
appliedStyles = appliedStyles.default;
}
return {
...excludeInObject(styles, "apply"),
...buildMixin(appliedStyles, theme, prev),
};
}
// No mixin to apply --> return styles
return styles;
};
// Build css rule
export const buildRule = (parent, styles, config, vars) => {
if (styles && Array.isArray(styles)) {
return styles.map(item => buildRule(parent, item, config, vars)).flat();
}
// Check for mixins to apply to this styles
if (typeof styles.apply === "string" && styles.apply) {
return buildRule(parent, buildMixin(styles, config), config, vars);
}
const result = [""];
Object.keys(styles).forEach(key => {
// key = key.trim(); //Trim current key
const value = styles[key];
if (value === null) {
return; // Ignore this property
}
//Check for object value --> build wrapped style
else if (typeof value === "object" && Array.isArray(value) === false) {
// Check for breakpoints rule
if (key === "@breakpoints") {
return Object.keys(config.breakpoints || {}).forEach(breakpointName => {
const mediaQuery = buildMediaQuery(config.breakpoints[breakpointName]);
const newContent = buildRule(parent, value, config, {
...(vars || {}),
breakpoint: breakpointName,
});
if (newContent.length === 0) {
return; // Skip this rule
}
// Wrap the media rule
return result.push(wrapRule(mediaQuery, newContent.join("\n"), "\n"));
});
}
// Check for theme rule
else if (/^@theme (\w*)$/.test(key.trim())) {
const scale = key.trim().match(/^@theme (\w*)$/)[1];
return Object.keys(config[scale] || {}).forEach(name => {
const newContent = buildRule(parent, value, config, {
...(vars || {}),
name: name,
value: config[scale][name],
});
return result.push(newContent.join("\n"));
});
}
// Check for media rule --> wrap content inside the media rule
else if (/^@/.test(key)) {
const newContent = buildRule(parent, value, config, vars || {});
if (newContent.length === 0) {
return; // Skip this rule
}
// Wrap the media rule
return result.push(wrapRule(key, newContent.join("\n"), "\n"));
}
// Add nested styles
return result.push(buildRule(parent.map(p => key.replace(/&/g, p)), value, config, vars));
}
// Other value --> append to the current css
parseProperty(key).forEach(prop => {
result[0] = result[0] + `${prop}:${buildValue(key, value, config, vars)};`;
});
});
// Wrap the main rule
if (result[0] !== "") {
result[0] = wrapRule(format(parent.join(","), vars), result[0], "");
}
// Filter items to remove empty
return result.flat(2).filter(value => {
return typeof value === "string" && value !== "";
});
};
// Build css styles
export const buildStyles = (styles, config) => {
const result = Object.keys(styles || {}).map(key => {
const value = styles[key];
// Check for at rule (media or keyframes)
if (/^@(media|keyframes)/.test(key.trim())) {
return wrapRule(key, buildStyles(value, config), "\n");
}
// Other value --> parse as regular classname
return buildRule([key], value, config, {
prefix: config.prefix || "",
});
});
return result.flat().join("\n");
};
// Generate CSS styles from a configuration object
export const css = config => {
const styles = {};
// Add borderbox styles
if (typeof config.useBorderBox === "undefined" || !!config.useBorderBox) {
styles["html"] = {
boxSizing: "border-box",
};
styles["*,*:before,*:after"] = {
boxSizing: "inherit",
};
}
// Add root styles
if (typeof config.useRootStyles === "undefined" || !!config.useRootStyles) {
styles["html"] = {
...(styles["html"] || {}),
background: "background",
color: "text",
...(config.root || {}),
};
}
// Add custom styles
if (config.styles) {
mergeStyles(styles, config.styles);
}
return buildStyles(styles, config);
};