@poupe/css
Version:
A TypeScript utility library for CSS property manipulation, formatting, and CSS-in-JS operations
358 lines (352 loc) • 10.7 kB
JavaScript
import { defu } from 'defu';
const unsafeKeys = Object.keys;
function* keys(object, valid) {
for (const key of unsafeKeys(object)) {
if (typeof key === "string" && Object.prototype.hasOwnProperty.call(object, key) && (valid?.(key) ?? true)) {
yield key;
}
}
}
function defaultValidPair(key, value) {
return value !== null && value !== void 0 && !key.includes(" ") && !key.startsWith("_");
}
function* pairs(object, valid) {
for (const key of keys(object)) {
const value = object[key];
if (valid?.(key, value) ?? defaultValidPair(key, value))
yield [key, value];
}
}
function kebabCase(s) {
const kebabbed = s.trim().replaceAll(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replaceAll(/([a-z])([A-Z])/g, "$1-$2").replaceAll(/[\s_]+/g, "-").toLowerCase();
if (vendorPrefixPattern.test(kebabbed)) {
return `-${kebabbed}`;
}
return kebabbed;
}
const vendorPrefixPattern = /^(webkit|moz|ms|o|khtml)-/;
function camelCase(s) {
if (!s || s === "-" || s === "_") {
return "";
}
let result = s.trim().replace(/^-/, "");
result = result.replaceAll(/[-_\s]+([\w])/g, (_, c) => c.toUpperCase());
result = result.replaceAll(/([A-Z]+)([A-Z][a-z])/g, (_, g1, g2) => g1.toLowerCase() + g2).replaceAll(/^[A-Z]+/g, (match) => match.toLowerCase());
return result;
}
function stringifyCSSProperties(object, options) {
const {
indent = " ",
prefix = "",
newLine = "\n",
inline = false,
singleLineThreshold = 1
} = options || {};
const lines = formatCSSProperties(object);
if (lines.length === 0) {
return "{}";
}
if (inline || lines.length <= singleLineThreshold) {
return `{ ${lines.join("; ")} }`;
}
return `{${newLine}${prefix}${indent}${lines.join(`;${newLine}${prefix}${indent}`)}${newLine}${prefix}}`;
}
function formatCSSProperties(object) {
const propertyMap = /* @__PURE__ */ new Map();
for (const [key, value] of properties(object)) {
const kebabKey = kebabCase(key);
const useComma = !spaceDelimitedProperties.has(kebabKey);
const formattedValue = formatCSSValue(value, useComma);
propertyMap.set(kebabKey, formattedValue);
}
const lines = [];
for (const [key, value] of propertyMap) {
lines.push(`${key}: ${value}`);
}
return lines;
}
function formatCSSValue(value, useComma = true) {
if (Array.isArray(value)) {
return value.map((v) => quoted(v)).join(useComma ? ", " : " ");
}
return quoted(value);
}
function quoted(v) {
if (typeof v === "boolean") {
return v ? "true" : "false";
} else if (typeof v === "string") {
const isCssFunction = /^[a-zA-Z-]+\(.*\)$/.test(v.trim());
if (v.includes(" ") && !isCssFunction) {
return `"${v}"`;
}
}
return String(v);
}
function* properties(object) {
for (const [key, value] of pairs(object)) {
if (Array.isArray(value) ? value.length > 0 && value.every((v) => isValidValue(v)) : isValidValue(value)) {
yield [key, value];
}
}
}
function isValidValue(value) {
if (typeof value === "string")
return value !== "";
return typeof value === "number";
}
const spaceDelimitedProperties = /* @__PURE__ */ new Set([
"animation",
"background",
"box-shadow",
"flex",
"font",
"grid-auto-columns",
"grid-auto-flow",
"grid-auto-rows",
"grid-gap",
"grid-template-areas",
"grid-template-columns",
"grid-template-rows",
"list-style",
"margin",
"padding",
"text-decoration",
"text-shadow",
"transform",
"transition"
]);
function stringifyCSSRules(rules = {}, options = {}) {
const {
newLine = "\n"
} = options;
return formatCSSRules(rules, options).join(newLine);
}
function formatCSSRules(rules = {}, options = {}) {
return [...generateCSSRules(rules, options)];
}
function formatCSSRulesArray(rules = [], options = {}) {
return [...generateCSSRulesArray(rules, options)];
}
function defaultValidCSSRule(key, value) {
if (key === "" || value === void 0 || value === null) {
return false;
}
return true;
}
function atRuleException(key, value) {
if (!key.startsWith("@") || value === null) {
return false;
} else if (Array.isArray(value)) {
return value.length === 0;
} else if (typeof value === "object") {
return Object.keys(value).length === 0;
} else {
return false;
}
}
function* generateCSSRulesArray(rules = [], options = {}) {
let wasBlankLine = true;
for (const value of rules) {
if (typeof value === "string") {
if (value) {
yield `${value};`;
wasBlankLine = false;
} else if (!wasBlankLine) {
yield "";
wasBlankLine = true;
}
} else if (value !== null && value !== void 0) {
let hasContent = false;
const innerLines = [];
for (const line of generateCSSRules(value, options)) {
innerLines.push(line);
hasContent = true;
}
if (hasContent) {
for (const line of innerLines) {
yield line;
}
wasBlankLine = false;
} else if (!wasBlankLine) {
yield "";
wasBlankLine = true;
}
}
}
}
function* generateCSSRules(rules = {}, options = {}) {
const {
indent = " ",
prefix = "",
valid = defaultValidCSSRule,
normalizeProperties = false
} = options;
const nextOptions = {
...options,
prefix: prefix + indent
};
const mayNormalize = (key) => {
if (!normalizeProperties) return key;
if (key.startsWith(".") || key.startsWith("#") || key.startsWith("@") || key.startsWith(":") || key.includes(" ")) {
return key;
}
return kebabCase(key);
};
for (const [key, value] of pairs(rules, valid)) {
if (atRuleException(key, value)) {
yield `${prefix}${key};`;
} else if (typeof value === "string") {
if (value) {
yield `${prefix}${mayNormalize(key)}: ${value};`;
}
} else if (Array.isArray(value)) {
if (value.length === 0) ; else if (typeof value[0] === "string") {
const normalizedKey = mayNormalize(key);
const useComma = !spaceDelimitedProperties.has(normalizedKey);
const inner = formatCSSValue(value, useComma);
if (inner) {
yield `${prefix}${normalizedKey}: ${inner};`;
}
} else {
let hasContent = false;
const innerLines = [];
for (const line of generateCSSRulesArray(value, nextOptions)) {
innerLines.push(line);
hasContent = true;
}
if (hasContent) {
yield `${prefix}${key} {`;
for (const line of innerLines) {
yield line;
}
yield `${prefix}}`;
}
}
} else if (value) {
let hasContent = false;
const innerLines = [];
for (const line of generateCSSRules(value, nextOptions)) {
innerLines.push(line);
hasContent = true;
}
if (hasContent) {
yield `${prefix}${key} {`;
for (const line of innerLines) {
yield line;
}
yield `${prefix}}`;
}
}
}
}
function interleavedRules(rules) {
if (rules.length === 0) return [];
const size = rules.length * 2 - 1;
const out = Array.from({ length: size }, () => ({}));
let i = 0;
for (const entry of rules) {
out[i] = entry;
i += 2;
}
return out;
}
function renameRules(rules, fn) {
if (!fn) return rules;
const map = /* @__PURE__ */ new Map();
for (const [key, value] of pairs(rules)) {
const k2 = fn(key);
if (k2) map.set(k2, value);
}
return Object.fromEntries(map);
}
function setDeepRule(target, path, object) {
let p = target;
let lastKey = "";
if (Array.isArray(path)) {
if (path.length === 0) return target;
for (let i = 0; i < path.length - 1; i++) {
const k = path[i];
if (p[k] === void 0) {
p[k] = {};
} else if (typeof p[k] !== "object" || p[k] === null) {
throw new Error(
`Invalid path at segment ${i}: "${k}" in path: ${path.join(".")}: ${typeof p[k]}`
);
}
p = p[k];
}
lastKey = path.at(-1);
} else {
lastKey = path;
}
p[lastKey] = defu(object, p[lastKey] ?? {});
return target;
}
function getDeepRule(target, path) {
const segments = typeof path === "string" ? [path] : path;
if (segments.length === 0) {
return target;
}
let current = target;
for (const key of segments) {
if (typeof current !== "object" || current === null || !Object.prototype.hasOwnProperty.call(current, key)) {
return void 0;
}
current = current[key];
}
return current;
}
const DEFAULT_SELECTOR_ALIASES = {
media: "@media (prefers-color-scheme: dark)",
dark: "@media (prefers-color-scheme: dark)",
light: "@media (prefers-color-scheme: light)",
mobile: "@media (max-width: 768px)",
tablet: "@media (min-width: 769px) and (max-width: 1024px)",
desktop: "@media (min-width: 1025px)"
};
function expandSelectorAlias(selector, aliases = DEFAULT_SELECTOR_ALIASES) {
const trimmed = selector.trim();
return aliases[trimmed] || trimmed;
}
function processCSSSelectors(selectors, options = {}) {
const {
addStarVariants = true,
allowCommaPassthrough = true,
aliases = DEFAULT_SELECTOR_ALIASES
} = options;
const selectorArray = Array.isArray(selectors) ? selectors : [selectors];
if (!Array.isArray(selectors) && allowCommaPassthrough && selectors.includes(",")) {
const expanded = expandSelectorAlias(selectors, aliases);
return [expanded];
}
const result = [];
const currentSelectors = [];
const flushSelectors = () => {
if (currentSelectors.length > 0) {
const expandedSelectors = [];
for (const selector of currentSelectors) {
expandedSelectors.push(selector);
if (addStarVariants) {
expandedSelectors.push(`${selector} *`);
}
}
result.push(expandedSelectors.join(", "));
currentSelectors.length = 0;
}
};
for (const s of selectorArray) {
const expanded = expandSelectorAlias(s, aliases);
const trimmed = expanded.trim();
if (!trimmed) continue;
if (trimmed.startsWith("@")) {
flushSelectors();
result.push(trimmed);
} else {
currentSelectors.push(trimmed);
}
}
flushSelectors();
return result.length === 0 ? void 0 : result;
}
export { camelCase, defaultValidCSSRule, defaultValidPair, expandSelectorAlias, formatCSSProperties, formatCSSRules, formatCSSRulesArray, formatCSSValue, generateCSSRules, generateCSSRulesArray, getDeepRule, interleavedRules, kebabCase, keys, pairs, processCSSSelectors, properties, renameRules, setDeepRule, spaceDelimitedProperties, stringifyCSSProperties, stringifyCSSRules, unsafeKeys };
//# sourceMappingURL=index.mjs.map