UNPKG

@poupe/css

Version:

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

358 lines (352 loc) 10.7 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 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