@poupe/css
Version:
A TypeScript utility library for CSS property manipulation, formatting, and CSS-in-JS operations
286 lines (285 loc) • 10.1 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 result = s.trim().replaceAll(/(?<=[A-Z])(?=[A-Z][a-z])/g, "-").replaceAll(/([a-z])([A-Z])/g, "$1-$2").replaceAll(/[\s_]+/g, "-").toLowerCase();
if (vendorPrefixPattern.test(result)) return `-${result}`;
return result;
}
const vendorPrefixPattern = /^(webkit|moz|ms|o|khtml)-/;
function camelCase(s) {
if (!s || s === "-" || s === "_") return "";
let result = s.trim().replace(/^-/, "");
result = result.replaceAll(/[-_\s]+([a-zA-Z\d])/g, (_, c) => c.toUpperCase());
result = result.replaceAll(/[A-Z]+(?=[A-Z][a-z])/g, (match) => match.toLowerCase()).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 formattedValue = formatCSSValue(value, !spaceDelimitedProperties.has(kebabKey));
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 = 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 inner = formatCSSValue(value, !spaceDelimitedProperties.has(normalizedKey));
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);
}
const UNSAFE_PROTO_KEYS = new Set([
"__proto__",
"constructor",
"prototype"
]);
function isUnsafeKey(key) {
return UNSAFE_PROTO_KEYS.has(key);
}
function getOrCreateChild(parent, key, segmentIndex, fullPath) {
const existing = parent[key];
if (existing === void 0) {
const empty = {};
parent[key] = empty;
return empty;
}
if (typeof existing !== "object" || existing === null) throw new Error(`Invalid path at segment ${segmentIndex}: "${key}" in path: ${fullPath.join(".")}: ${typeof existing}`);
return existing;
}
function lookupChild(current, key) {
if (typeof current !== "object" || current === null || !Object.prototype.hasOwnProperty.call(current, key)) return;
return current[key];
}
function setDeepRule(target, path, object) {
let p = target;
let lastKey = "";
if (Array.isArray(path)) {
if (path.length === 0) return target;
for (const [i, k] of path.slice(0, -1).entries()) {
if (isUnsafeKey(k)) return target;
p = getOrCreateChild(p, k, i, path);
}
lastKey = path.at(-1);
} else lastKey = path;
if (isUnsafeKey(lastKey)) return target;
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) {
current = lookupChild(current, key);
if (current === void 0) return void 0;
}
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(",")) return [expandSelectorAlias(selectors, aliases)];
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 trimmed = expandSelectorAlias(s, aliases).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