UNPKG

@poupe/css

Version:

A TypeScript utility library for CSS property manipulation, formatting, and CSS-in-JS operations

286 lines (285 loc) 10.1 kB
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