@poupe/css
Version:
A TypeScript utility library for CSS property manipulation, formatting, and CSS-in-JS operations
1 lines • 36.2 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":[],"sources":["../src/utils.ts","../src/properties.ts","../src/rules.ts","../src/selectors.ts"],"sourcesContent":["// cspell:words khtml\n\n/**\n * A type-safe wrapper around Object.keys that preserves the object's key types.\n *\n * @returns a typed array of keys of the object\n */\nexport const unsafeKeys = Object.keys as <T>(object: T) => Array<keyof T>;\n\n/**\n * A generator function that yields keys of an object that pass an optional validation function.\n *\n * Iterates through all own properties of the given object and yields\n * each key that passes the optional validation function.\n *\n * @param object - The object to iterate over\n * @param valid - Optional validation function that determines which keys to yield\n * @returns A generator of valid keys from the object\n */\nexport function* keys<T, K extends keyof T>(object: T, valid?: (key: keyof T) => boolean): Generator<K> {\n for (const key of unsafeKeys(object)) {\n if (typeof key === 'string' && Object.prototype.hasOwnProperty.call(object, key) && (valid?.(key) ?? true)) {\n yield key as K;\n }\n }\n}\n\n/**\n * Validates if a key-value pair meets default criteria.\n *\n * A key-value pair is considered valid when:\n * - The value is neither null nor undefined\n * - The key doesn't contain spaces\n * - The key doesn't start with an underscore (_)\n *\n * @param key - The key to validate\n * @param value - The value to validate\n * @returns true if the key-value pair is valid, false otherwise\n *\n */\nexport function defaultValidPair<K extends string, T = unknown>(key: K, value: T): boolean {\n return value !== null &&\n value !== undefined &&\n !key.includes(' ') &&\n !key.startsWith('_');\n}\n\n/**\n * A generator function that yields valid key-value pairs from an object.\n *\n * Iterates through all own properties of the given object and yields\n * each key-value pair that passes the validation function.\n *\n * @param object - The object to iterate over\n * @param valid - Optional validation function that determines which key-value pairs to yield\n * @returns A generator of valid key-value pairs from the object\n */\nexport function* pairs<K extends string = string, T = unknown>(\n object: Record<K, T>,\n valid?: (k: K, v: T) => boolean,\n): Generator<[K, T]> {\n for (const key of keys(object)) {\n const value = object[key];\n if (valid?.(key, value) ?? defaultValidPair(key, value))\n yield [key, value];\n }\n}\n\n/*\n * Converts a given string to kebab-case.\n *\n * Transforms various string formats (camelCase, PascalCase, snake_case)\n * into a lowercase string with words separated by hyphens.\n * Adds leading hyphen to recognized vendor prefixes.\n *\n * @param s - The input string to convert\n * @returns A kebab-case representation of the input string\n *\n * @example\n * kebabCase('XMLHttpRequest') // returns 'xml-http-request'\n * kebabCase('camelCase') // returns 'camel-case'\n * kebabCase('snake_case') // returns 'snake-case'\n * kebabCase('WebkitTransition') // returns '-webkit-transition'\n */\nexport function kebabCase(s: string): string {\n // Apply standard kebab-case transformations\n const result = s\n .trim()\n // handle multiple uppercase letters (e.g., XMLHttpRequest -> xml-http-request)\n // use lookbehind + lookahead to avoid ReDoS from overlapping quantifiers\n .replaceAll(/(?<=[A-Z])(?=[A-Z][a-z])/g, '-')\n // handle camelCase\n .replaceAll(/([a-z])([A-Z])/g, '$1-$2')\n // handle snakeCase\n .replaceAll(/[\\s_]+/g, '-')\n .toLowerCase();\n\n // Check for vendor prefixes using a regex and add leading hyphen if needed\n if (vendorPrefixPattern.test(result)) {\n return `-${result}`;\n }\n\n return result;\n}\n\nconst vendorPrefixPattern = /^(webkit|moz|ms|o|khtml)-/;\n\n/**\n * Converts a given string to camelCase.\n *\n * Transforms various string formats (kebab-case, PascalCase, snake_case)\n * into a camelCase string. Properly handles vendor prefixes and internal\n * capitalization patterns.\n *\n * @param s - The input string to convert\n * @returns A camelCase representation of the input string\n *\n * @example\n * camelCase('xml-http-request') // returns 'xmlHttpRequest'\n * camelCase('PascalCase') // returns 'pascalCase'\n * camelCase('snake_case') // returns 'snakeCase'\n * camelCase('-webkit-transition') // returns 'webkitTransition'\n * camelCase('BGColor') // returns 'bgColor'\n * camelCase('HTMLElement') // returns 'htmlElement'\n */\nexport function camelCase(s: string): string {\n // Handle empty strings and single delimiters\n if (!s || s === '-' || s === '_') {\n return '';\n }\n\n // Remove leading hyphens (for vendor prefixes) and trim\n let result = s.trim().replace(/^-/, '');\n\n // Handle explicit delimiter-separated words (kebab-case, snake_case, spaces)\n result = result.replaceAll(/[-_\\s]+([a-zA-Z\\d])/g, (_, c: string) => c.toUpperCase());\n\n // Handle internal capitalization patterns like \"BGColor\" -> \"bgColor\"\n // Look for uppercase letters that are preceded by lowercase or are the start\n // of a capital sequence followed by lowercase (like in \"BGColor\" or \"HTMLElement\")\n result = result\n // First handle patterns like \"BGColor\" -> \"bgColor\" by lowercasing\n // uppercase runs before a capital+lowercase boundary\n .replaceAll(/[A-Z]+(?=[A-Z][a-z])/g, (match) => match.toLowerCase())\n // Then ensure the first letter is lowercase (handling both PascalCase and\n // cases like \"BG\" at the start)\n .replaceAll(/^[A-Z]+/g, (match) => match.toLowerCase());\n\n return result;\n}\n","import {\n kebabCase,\n pairs,\n} from './utils';\n\nexport type CSSProperties<K extends string = string> = Record<K, CSSValue>;\nexport type CSSValue = (boolean | number | string)[] | boolean | number | string;\n\n/**\n * Configuration options for CSS properties stringification.\n */\nexport type CSSPropertiesOptions = {\n /** Indentation string, defaults to two spaces. */\n indent?: string\n /** Whether to format output on a single line, defaults to false. */\n inline?: boolean\n /** New line character, defaults to LF. */\n newLine?: string\n /** Prefix string added before each line, defaults to empty string. */\n prefix?: string\n /** Maximum number of properties to format on a single line, defaults to 1. */\n singleLineThreshold?: number\n};\n\n/**\n * Converts a CSSProperties object into a formatted CSS string representation.\n *\n * @param object - The CSSProperties object to stringify.\n * @param options - Configuration options for string formatting.\n * @returns A string representing the CSS properties enclosed in curly braces.\n * @remarks no newLine at the end to aid composition.\n */\nexport function stringifyCSSProperties<K extends string>(\n object: CSSProperties<K>,\n options?: CSSPropertiesOptions,\n): string {\n const {\n indent = ' ',\n prefix = '',\n newLine = '\\n',\n inline = false,\n singleLineThreshold = 1,\n } = options || {};\n\n const lines = formatCSSProperties(object);\n\n // Handle empty blocks with a simple format\n if (lines.length === 0) {\n return '{}';\n }\n\n // Handle inline mode or when property count is below threshold\n if (inline || lines.length <= singleLineThreshold) {\n return `{ ${lines.join('; ')} }`;\n }\n\n // Standard multiline format\n return `{${newLine}${prefix}${indent}${lines.join(`;${newLine}${prefix}${indent}`)}${newLine}${prefix}}`;\n}\n\n/**\n * Formats a CSSProperties object into an array of CSS property strings.\n *\n * @param object - The CSSProperties object to format.\n * @returns An array of strings, where each string is a CSS property in the format \"key: value;\".\n */\nexport function formatCSSProperties<K extends string>(object: CSSProperties<K>): string[] {\n const propertyMap = new Map<string, string>();\n for (const [key, value] of properties(object)) {\n const kebabKey = kebabCase(key);\n const useComma = !spaceDelimitedProperties.has(kebabKey);\n const formattedValue = formatCSSValue(value, useComma);\n propertyMap.set(kebabKey, formattedValue);\n }\n\n const lines: string[] = [];\n for (const [key, value] of propertyMap) {\n lines.push(`${key}: ${value}`);\n }\n return lines;\n}\n\n/**\n * Formats a CSS value into a string representation.\n *\n * @param value - The CSS value to format, which can be a single value or an array of values.\n * @param useComma - Flag to determine whether array values should be comma-separated (true)\n * or space-separated (false). Defaults to true.\n * @returns A formatted string representation of the CSS value.\n * @remarks\n\n * - For array values, elements are joined with commas or spaces based on the useComma parameter.\n * - The choice between commas and spaces depends on the CSS property being formatted.\n * - Properties like 'font-family' use commas while properties like 'margin' use spaces.\n */\nexport function formatCSSValue(value: CSSValue, useComma = true): string {\n if (Array.isArray(value)) {\n return value.map((v) => quoted(v)).join(useComma ? ', ' : ' ');\n }\n return quoted(value);\n}\n\n/**\n * Encloses a CSS value in double quotes if it is a string containing spaces,\n * except for CSS functions which should not be quoted.\n *\n * @param v - The CSS value to process.\n * @returns The processed CSS value as a string.\n * @example\n * quoted('Open Sans') // Returns \"\\\"Open Sans\\\"\"\n * quoted('rgb(255, 0, 0)') // Returns \"rgb(255, 0, 0)\" (no quotes - CSS function)\n * quoted(16) // Returns \"16\"\n * quoted(true) // Returns \"true\"\n */\nexport function quoted(v: CSSValue): string {\n if (typeof v === 'boolean') {\n return v ? 'true' : 'false';\n } else if (typeof v === 'string') {\n // Check if this is a CSS function (contains parentheses)\n const isCssFunction = /^[a-zA-Z-]+\\(.*\\)$/.test(v.trim());\n\n // Only quote strings with spaces that are not CSS functions\n if (v.includes(' ') && !isCssFunction) {\n return `\"${v}\"`;\n }\n }\n return String(v);\n}\n\n/**\n * Generates a sequence of valid CSS property key-value pairs from a CSSProperties object.\n *\n * @param object - The object containing CSS properties.\n * @returns A generator of valid key-value CSS property pairs.\n * @remarks Filters out invalid or empty CSS property values, returning only valid entries.\n */\nexport function* properties<K extends string>(object: CSSProperties<K>): Generator<[K, CSSValue]> {\n for (const [key, value] of pairs(object)) {\n if (Array.isArray(value) ? (value.length > 0 && value.every((v) => isValidValue(v))) : isValidValue(value)) {\n yield [key, value as CSSValue];\n }\n }\n}\n\n/**\n * Checks if a CSS value is valid.\n * A value is considered valid if it is a non-empty string or a number.\n *\n * @param value - The value to check.\n * @returns True if the value is valid, false otherwise.\n */\nfunction isValidValue(value: unknown): boolean {\n if (typeof value === 'string')\n return value !== '';\n return typeof value === 'number';\n}\n\n/**\n * A set of CSS properties that typically have space-delimited values.\n * These properties often require multiple values to be specified in a single declaration.\n *\n * @remarks\n * When formatting CSS values for these properties, values are space-separated rather than comma-separated.\n * For example:\n * - margin: 10px 20px 30px 40px (spaces between values)\n * - padding: 5px 10px (spaces between values)\n * - font: bold 16px Arial (spaces between values)\n *\n * This differs from comma-separated properties like font-family:\n * - font-family: Arial, Helvetica, sans-serif (commas between values)\n */\nexport const spaceDelimitedProperties: ReadonlySet<string> = new Set([\n 'animation',\n 'background',\n 'box-shadow',\n 'flex',\n 'font',\n 'grid-auto-columns',\n 'grid-auto-flow',\n 'grid-auto-rows',\n 'grid-gap',\n 'grid-template-areas',\n 'grid-template-columns',\n 'grid-template-rows',\n 'list-style',\n 'margin',\n 'padding',\n 'text-decoration',\n 'text-shadow',\n 'transform',\n 'transition',\n]);\n","import { defu } from 'defu';\nimport { kebabCase, pairs } from './utils';\n\nimport {\n formatCSSValue,\n spaceDelimitedProperties,\n} from './properties';\n\n/**\n * Represents a structured CSS rule set that can contain nested rules.\n *\n * This type allows for representing complex CSS structures including:\n * - Simple property/value pairs\n * - At-rules (like `@media`, `@keyframes`)\n * - Nested rule sets\n * - Arrays of values or rule sets\n *\n * @example\n * ```\n * // Example CSS rule structure\n * const rules = {\n * body: {\n * color: 'red',\n * fontSize: '16px',\n * '@media (max-width: 768px)': {\n * fontSize: '14px'\n * }\n * }\n * };\n * ```\n */\nexport type CSSRules = {\n [name: string]: CSSRules | CSSRules[] | null | string | string[]\n};\n\n/**\n * Represents the possible value types that can be assigned to a CSS rule.\n *\n * This is a type alias for the union of all possible values in a CSSRules\n * object.\n */\nexport type CSSRulesValue = CSSRules[string];\n\n/**\n * Configuration options for formatting CSS rules.\n */\nexport interface CSSRulesFormatOptions {\n /**\n * Indentation string to use for each level of nesting.\n * @defaultValue `' '` (two spaces)\n */\n indent?: string\n\n /**\n * Prefix string added before each line.\n * @defaultValue `''` (empty string)\n */\n prefix?: string\n\n /**\n * Optional validation function to determine which rules to include.\n * @param key - The rule name/selector\n * @param value - The rule value\n * @returns `true` if the rule should be included, `false` otherwise\n */\n valid?: (key: string, value: CSSRulesValue) => boolean\n\n /**\n * Whether to normalize CSS property names from camelCase to kebab-case.\n * Only applies to property names, not selectors.\n * @defaultValue `false`\n */\n normalizeProperties?: boolean\n}\n\n/**\n * A subset of CSSRules that is compatible with tailwindcss plugin API.\n *\n * This type is more restrictive than CSSRules:\n * - It doesn't allow null values\n * - It uses itself for nested rules rather than the broader CSSRules type\n */\nexport type CSSRuleObject = {\n [key: string]: CSSRuleObject | string | string[]\n};\n\n/**\n * Converts a CSS rule object into a formatted string representation.\n *\n * This function takes a CSS rule object and returns a formatted string with\n * proper indentation and nesting.\n *\n * @param rules - The CSS rules to stringify\n * @param options - Configuration options for string formatting\n * @returns A string representing the CSS rules with proper formatting\n * @remarks a newLine is not appended at the end to aid composition.\n *\n * @example\n * ```\n * // Simple example of converting CSS rules to string format\n * const rules = {\n * 'body': {\n * 'color': 'red',\n * 'font-size': '16px'\n * }\n * };\n *\n * const result = stringifyCSSRules(rules);\n * // Result will be:\n * // body {\n * // color: red;\n * // font-size: 16px;\n * // };\n * ```\n */\nexport function stringifyCSSRules(\n rules: CSSRuleObject | CSSRules = {},\n options: CSSRulesFormatOptions & {\n /**\n * Character(s) to use for line breaks.\n * @defaultValue `'\\n'`\n */\n newLine?: string\n } = {},\n): string {\n const {\n newLine = '\\n',\n } = options;\n\n return formatCSSRules(rules, options).join(newLine);\n}\n\n/**\n * Formats CSS rule objects into an array of formatted lines.\n *\n * This function processes a CSS rule object and returns an array of strings,\n * where each string represents a line in the formatted CSS output. It\n * handles various value types including strings, numbers, arrays, and nested\n * objects.\n *\n * @param rules - The CSS rules to format\n * @param options - Configuration options for formatting\n * @returns An array of strings, each representing a line in the formatted\n * CSS\n *\n * @example\n * ```\n * const rules = {\n * 'body': {\n * 'color': 'red'\n * }\n * };\n *\n * const lines = formatCSSRules(rules);\n * // Returns: ['body {', ' color: red;', '}']\n * ```\n */\nexport function formatCSSRules(\n rules: CSSRuleObject | CSSRules = {},\n options: CSSRulesFormatOptions = {},\n): string[] {\n return [...generateCSSRules(rules, options)];\n}\n\n/**\n * Formats an array of CSS rules into an array of formatted string lines.\n *\n * This function processes various CSS rule representations recursively and\n * converts them into strings representing CSS code with proper formatting.\n * It handles:\n *\n * - String values (treated as direct CSS with semicolons added)\n * - Empty strings (converted to blank lines for spacing if appropriate)\n * - CSS rule objects (recursively processed with formatCSSRules)\n * - Empty rule objects (possibly generating blank lines)\n *\n * The function maintains proper whitespace by tracking whether the last\n * inserted item was a blank line to avoid consecutive empty lines.\n *\n * @param rules - The array of CSS rules to format (strings or rule objects)\n * @param options - Configuration options for formatting\n * @returns An array of strings, each representing a line in the formatted\n * CSS\n *\n * @example\n * ```\n * // Mixed strings and objects\n * formatCSSRulesArray([\n * 'display: block',\n * { color: 'red' },\n * '',\n * { fontSize: '16px' }\n * ]);\n * // Returns: ['display: block;', 'color: red;', '', 'fontSize: 16px;']\n * ```\n */\nexport function formatCSSRulesArray(\n rules: (CSSRuleObject | CSSRules | string)[] = [],\n options: CSSRulesFormatOptions = {},\n): string[] {\n return [...generateCSSRulesArray(rules, options)];\n}\n\n/**\n * Default validation function for CSS rules.\n *\n * Determines if a CSS rule key-value pair should be included in the output.\n * By default, a rule is valid if:\n * - The key is not an empty string\n * - The value is neither undefined nor null\n *\n * @param key - The rule key/selector to validate\n * @param value - The rule value to validate\n * @returns `true` if the rule should be included, `false` otherwise\n */\nexport function defaultValidCSSRule(\n key: string,\n value: CSSRulesValue,\n): boolean {\n if (key === '' || value === undefined || value === null) {\n return false;\n }\n return true;\n}\n\n/**\n * Special handling for CSS at-rules with empty content.\n *\n * At-rules (rules starting with `@`) with empty content are treated\n * differently:\n * - Empty at-rules (like `@import`, `@charset`) are rendered as a single\n * line with semicolon\n * - Normal CSS rules with empty content would be omitted entirely\n *\n * @example\n * `@supports (display: grid) {}` becomes `@supports (display: grid);`\n */\nfunction atRuleException(key: string, value: CSSRulesValue): boolean {\n if (!key.startsWith('@') || value === null) {\n return false;\n } else if (Array.isArray(value)) {\n return value.length === 0;\n } else if (typeof value === 'object') {\n return Object.keys(value).length === 0;\n } else {\n return false;\n }\n}\n\n/**\n * Generator version of formatCSSRulesArray that yields lines as they're\n * generated. This avoids building arrays in memory and is more efficient for\n * large files.\n *\n * @param rules - The array of CSS rules to format\n * @param options - Configuration options for formatting\n * @returns Generator that yields individual CSS lines without line\n * endings\n */\nexport function* generateCSSRulesArray(\n rules: (CSSRuleObject | CSSRules | string)[] = [],\n options: CSSRulesFormatOptions = {},\n): Generator<string, void, unknown> {\n // Track if the last item was a blank line to avoid consecutive empty lines\n let wasBlankLine = true;\n\n for (const value of rules) {\n if (typeof value === 'string') {\n // String rule, preserve empty for whitespace\n if (value) {\n yield `${value};`;\n wasBlankLine = false;\n } else if (!wasBlankLine) {\n yield '';\n wasBlankLine = true;\n }\n } else if (value !== null && value !== undefined) {\n // Object rule\n let hasContent = false;\n const innerLines: string[] = [];\n\n // Collect to check if empty (we need to peek ahead)\n for (const line of generateCSSRules(value, options)) {\n innerLines.push(line);\n hasContent = true;\n }\n\n if (hasContent) {\n for (const line of innerLines) {\n yield line;\n }\n wasBlankLine = false;\n } else if (!wasBlankLine) {\n yield '';\n wasBlankLine = true;\n }\n }\n }\n}\n\n/**\n * Generator version of formatCSSRules that yields lines as they're\n * generated.\n *\n * @param rules - The CSS rules to format\n * @param options - Configuration options for formatting\n * @returns Generator that yields individual CSS lines without line\n * endings\n */\nexport function* generateCSSRules(\n rules: CSSRuleObject | CSSRules = {},\n options: CSSRulesFormatOptions = {},\n): Generator<string, void, unknown> {\n const {\n indent = ' ',\n prefix = '',\n valid = defaultValidCSSRule,\n normalizeProperties = false,\n } = options;\n\n const nextOptions: CSSRulesFormatOptions = {\n ...options,\n prefix: prefix + indent,\n };\n\n // Helper to normalize key if appropriate (property vs selector/at-rule)\n const mayNormalize = (key: string): string => {\n if (!normalizeProperties) return key;\n // Don't normalize selectors or at-rules\n if (key.startsWith('.') || key.startsWith('#') ||\n key.startsWith('@') || key.startsWith(':') ||\n key.includes(' ')) {\n return key;\n }\n return kebabCase(key);\n };\n\n for (const [key, value] of pairs(rules, valid)) {\n if (atRuleException(key, value)) {\n // at-function\n yield `${prefix}${key};`;\n } else if (typeof value === 'string') {\n // string, omit empty\n if (value) {\n // Apply kebab-case conversion only to properties, like formatCSSProperties\n yield `${prefix}${mayNormalize(key)}: ${value};`;\n }\n } else if (Array.isArray(value)) {\n if (value.length === 0) {\n // Skip empty arrays\n } else if (typeof value[0] === 'string') {\n // multi-value - follow formatCSSProperties pattern\n const normalizedKey = mayNormalize(key);\n const useComma = !spaceDelimitedProperties.has(normalizedKey);\n const inner = formatCSSValue(value as string[], useComma);\n if (inner) {\n yield `${prefix}${normalizedKey}: ${inner};`;\n }\n } else {\n // nested rules array\n let hasContent = false;\n const innerLines: string[] = [];\n\n // Collect to check if empty\n for (const line of generateCSSRulesArray(value, nextOptions)) {\n innerLines.push(line);\n hasContent = true;\n }\n\n if (hasContent) {\n yield `${prefix}${key} {`;\n for (const line of innerLines) {\n yield line;\n }\n yield `${prefix}}`;\n }\n }\n } else if (value) {\n // nested rules object\n let hasContent = false;\n const innerLines: string[] = [];\n\n // Collect to check if empty\n for (const line of generateCSSRules(value, nextOptions)) {\n innerLines.push(line);\n hasContent = true;\n }\n\n if (hasContent) {\n yield `${prefix}${key} {`;\n for (const line of innerLines) {\n yield line;\n }\n yield `${prefix}}`;\n }\n }\n }\n}\n\n/**\n * Interleaves an array of CSS rule objects with empty objects.\n *\n * @param rules - An array of CSS rule objects to be interleaved\n * @returns An array with the original rules spaced out with empty objects\n *\n * @example\n * ```\n * // Input: [{ color: 'red' }, { background: 'blue' }]\n * // Output: [{ color: 'red' }, {}, { background: 'blue' }]\n * ```\n */\nexport function interleavedRules(rules: CSSRules[]): CSSRules[] {\n if (rules.length === 0) return [];\n\n const size = rules.length * 2 - 1;\n const out: Array<CSSRules> = Array.from({ length: size }, () => ({}));\n\n let i = 0;\n for (const entry of rules) {\n out[i] = entry;\n i += 2;\n }\n\n return out;\n}\n\n/**\n * Renames the keys in a CSS rules object using the provided function.\n *\n * @param rules - The CSS rules object whose keys should be renamed\n * @param fn - A function that takes an original key name and returns a new\n * key name (or falsy value to skip)\n * @returns A new CSS rules object with renamed keys\n *\n * @example\n * ```\n * // Input: { '.button': { color: 'blue' } }, key => `@utility\n * // ${key.slice(1)}`\n * // Output: { '@utility button': { color: 'blue' } }\n * ```\n */\nexport function renameRules(\n rules: CSSRules,\n fn: (name: string) => string,\n): CSSRules {\n if (!fn) return rules;\n\n const map = new Map<string, CSSRules[string]>();\n for (const [key, value] of pairs(rules)) {\n const k2 = fn(key);\n if (k2) map.set(k2, value);\n }\n\n return Object.fromEntries(map);\n}\n\nconst UNSAFE_PROTO_KEYS: ReadonlySet<string> = new Set([\n '__proto__',\n 'constructor',\n 'prototype',\n]);\n\n/** True for keys that would mutate the prototype chain when assigned. */\nfunction isUnsafeKey(key: string): boolean {\n return UNSAFE_PROTO_KEYS.has(key);\n}\n\n/** Descends into `parent[key]`, creating an empty CSSRules if missing.\n * Throws if a non-object value already occupies the slot. */\nfunction getOrCreateChild(\n parent: CSSRules,\n key: string,\n segmentIndex: number,\n fullPath: string[],\n): CSSRules {\n const existing = parent[key];\n if (existing === undefined) {\n const empty = {} as CSSRules;\n parent[key] = empty;\n return empty;\n }\n if (typeof existing !== 'object' || existing === null) {\n throw new Error(\n `Invalid path at segment ${segmentIndex}: \"${key}\" in path: ` +\n `${fullPath.join('.')}: ${typeof existing}`,\n );\n }\n return existing as CSSRules;\n}\n\n/** Returns the own-property at `key` of `current`, or undefined when\n * `current` is not a non-null object or does not own the key. */\nfunction lookupChild(\n current: CSSRulesValue | undefined,\n key: string,\n): CSSRulesValue | undefined {\n if (typeof current !== 'object' || current === null ||\n !Object.prototype.hasOwnProperty.call(current, key)) {\n return undefined;\n }\n return (current as CSSRules)[key];\n}\n\n/**\n * Sets a CSS rule object at a specified path within a target object,\n * merging with existing objects and creating intermediate objects as needed.\n *\n * This function allows for deep setting of CSS rules in a nested object\n * structure. It can handle both string paths for top-level assignments and\n * array paths for nested assignments. When the target path already contains\n * an object, the new object is merged with the existing one, with new values\n * taking precedence.\n *\n * The function is overloaded to provide type safety for both general\n * `CSSRules` objects and TailwindCSS-compatible `CSSRuleObject` types.\n *\n * @param target - The target CSS rules object to modify\n * @param path - Either a string key for direct assignment or an array of\n * string keys for nested assignment\n * @param object - The CSS rule object to set at the specified path\n * @returns The modified target object (same type as input)\n * @remarks The target object is modified in place, returned reference is\n * only a convenience.\n *\n * @example\n * ```\n * // Direct assignment\n * setDeepRule(rules, 'button', { color: 'blue' });\n * // Result: { button: { color: 'blue' } }\n *\n * // Nested assignment\n * setDeepRule(rules, ['components', 'button'], { color: 'blue' });\n * // Result: { components: { button: { color: 'blue' } } }\n *\n * // Merging with existing object (new values take precedence)\n * const rules = { button: { color: 'red', margin: '5px' } };\n * setDeepRule(rules, 'button', { color: 'blue', padding: '10px' });\n * // Result: { button: { color: 'blue', margin: '5px', padding: '10px' } }\n * ```\n */\nexport function setDeepRule(\n target: CSSRuleObject,\n path: string | string[],\n object: CSSRuleObject,\n): CSSRuleObject;\nexport function setDeepRule(\n target: CSSRules,\n path: string | string[],\n object: CSSRules,\n): CSSRules;\nexport function setDeepRule(\n target: CSSRules,\n path: string | string[],\n object: CSSRules,\n): CSSRules {\n let p: CSSRules = target;\n let lastKey = '';\n\n if (Array.isArray(path)) {\n if (path.length === 0) return target;\n\n for (const [i, k] of path.slice(0, -1).entries()) {\n if (isUnsafeKey(k)) return target;\n p = getOrCreateChild(p, k, i, path);\n }\n\n lastKey = path.at(-1) as string;\n } else {\n lastKey = path;\n }\n\n if (isUnsafeKey(lastKey)) return target;\n\n p[lastKey] = defu(object, p[lastKey] ?? {} as typeof object);\n\n return target;\n}\n\n/**\n * Retrieves a CSS rule value from a specified path within a target object.\n *\n * This function allows for deep retrieval of CSS rules from a nested object\n * structure. It can handle both string paths for top-level access and array\n * paths for nested access.\n *\n * The function is overloaded to provide type safety for both general\n * `CSSRules` objects and TailwindCSS-compatible `CSSRuleObject` types.\n *\n * @param target - The target CSS rules object to search within\n * @param path - Either a string key for direct access or an array of\n * string keys for nested access\n * @returns The value at the specified path, or `undefined` if the path\n * does not exist\n *\n * @example\n * ```\n * const rules = {\n * components: { button: { color: 'blue' } },\n * utils: ['clearfix', 'sr-only']\n * };\n *\n * // Direct access\n * getDeepRule(rules, 'utils');\n * // Result: ['clearfix', 'sr-only']\n *\n * // Nested access\n * getDeepRule(rules, ['components', 'button', 'color']);\n * // Result: 'blue'\n *\n * // Non-existent path\n * getDeepRule(rules, ['components', 'header']);\n * // Result: undefined\n *\n * // Root access (empty array)\n * getDeepRule(rules, []);\n * // Result: { components: { ... }, utils: [...] }\n * ```\n */\nexport function getDeepRule(\n target: CSSRuleObject,\n path: string | string[],\n): CSSRuleObject | undefined;\nexport function getDeepRule(\n target: CSSRules,\n path: string | string[],\n): CSSRulesValue | undefined;\nexport function getDeepRule(\n target: CSSRules,\n path: string | string[],\n): CSSRulesValue | undefined {\n const segments = typeof path === 'string' ? [path] : path;\n\n if (segments.length === 0) {\n // Empty path returns the target object itself\n return target;\n }\n\n let current: CSSRulesValue | undefined = target;\n for (const key of segments) {\n current = lookupChild(current, key);\n if (current === undefined) return undefined;\n }\n\n return current;\n}\n","/**\n * Default selector aliases that expand simple names into complex at-rules\n */\nconst DEFAULT_SELECTOR_ALIASES: Record<string, string> = {\n media: '@media (prefers-color-scheme: dark)',\n dark: '@media (prefers-color-scheme: dark)',\n light: '@media (prefers-color-scheme: light)',\n mobile: '@media (max-width: 768px)',\n tablet: '@media (min-width: 769px) and (max-width: 1024px)',\n desktop: '@media (min-width: 1025px)',\n};\n\n/**\n * Expands selector aliases into their full forms\n * @param selector - The selector to potentially expand\n * @param aliases - Custom aliases to use (defaults to built-in ones)\n * @returns Expanded selector or original if no alias found\n */\nexport function expandSelectorAlias(\n selector: string,\n aliases: Record<string, string> = DEFAULT_SELECTOR_ALIASES,\n): string {\n const trimmed = selector.trim();\n return aliases[trimmed] || trimmed;\n}\n\nexport interface ProcessCSSSelectorOptions {\n /** Whether to add \"selector *\" variants to each selector */\n addStarVariants?: boolean\n /** Custom selector aliases to use for expansion */\n aliases?: Record<string, string>\n /** Whether to allow comma-separated selectors to pass through */\n allowCommaPassthrough?: boolean\n}\n\n/**\n * Processes CSS selectors and at-rules, handling both strings and arrays.\n * Merges consecutive selectors with OR and adds * variants,\n * while keeping at-rules stacked separately.\n *\n * @param selectors - CSS selector(s) and at-rules\n * @param options - Processing options\n * @returns Array of processed selector strings or undefined\n */\nexport function processCSSSelectors(\n selectors: string | string[],\n options: ProcessCSSSelectorOptions = {},\n): string[] | undefined {\n const {\n addStarVariants = true,\n allowCommaPassthrough = true,\n aliases = DEFAULT_SELECTOR_ALIASES,\n } = options;\n\n // Convert string to array for unified processing\n const selectorArray = Array.isArray(selectors) ? selectors : [selectors];\n\n // Handle comma pass-through for single strings\n if (!Array.isArray(selectors) && allowCommaPassthrough && selectors.includes(',')) {\n const expanded = expandSelectorAlias(selectors, aliases);\n return [expanded];\n }\n\n const result: string[] = [];\n const currentSelectors: string[] = [];\n\n const flushSelectors = () => {\n if (currentSelectors.length > 0) {\n // Merge consecutive selectors with OR, optionally adding * variants\n const expandedSelectors: string[] = [];\n for (const selector of currentSelectors) {\n expandedSelectors.push(selector);\n if (addStarVariants) {\n expandedSelectors.push(`${selector} *`);\n }\n }\n result.push(expandedSelectors.join(', '));\n currentSelectors.length = 0;\n }\n };\n\n for (const s of selectorArray) {\n const expanded = expandSelectorAlias(s, aliases);\n const trimmed = expanded.trim();\n if (!trimmed) continue;\n\n if (trimmed.startsWith('@')) {\n // At-rule: flush current selectors and add at-rule separately\n flushSelectors();\n result.push(trimmed);\n } else {\n // Regular selector: add to current batch\n currentSelectors.push(trimmed);\n }\n }\n\n // Flush any remaining selectors\n flushSelectors();\n\n return result.length === 0 ? undefined : result;\n}\n"],"mappings":";;;;;;;;;;;;;;SAoBO,UAAa,GAAA;;;;;;;;;;;;;SAqBX,uBACL,QAAU,SACT;;;;;;;;;;;EAcL,MAAA,iBAEE,eACmB,OAAA,CAAA,yBAAA,IAAA,SAAA,CAAA;EACnB,YAAW,IAAA,UAAY,eAAS;;OAE1B,QAAQ,EAAA;;;;SAmCV,eAAA,OAAyB,WAC3B,MAAO;CAGT,IAAA,MAAO,QAAA,MAAA,EAAA,OAAA,MAAA,KAAA,MAAA,OAAA,EAAA,CAAA,CAAA,KAAA,WAAA,OAAA,IAAA;;;;;;;;;;;;;;;;;;MAyBF,2BAAwB,IAC3B,IAAA;CAIF;CAGA;CAKA;CAQA;;;;;;;;;;;CCpHF;CAIE;CAQA;CAGA;CAKA;CAKA,CAAA;;;;;AASF,SAAgB,eAAA,QAAsC,EAAA,EAAA,UAAoC,EAAA,EAAA;CACxF,OAAM,CAAA,GAAA,iBAAA,OAAA,QAAkB,CAAA;;SAIhB,oBAAiB,QAAA,EAAA,EAAe,UADpB,EAAA,EAAA;QAElB,CAAA,GAAA,sBAA0B,OAAA,QAAe,CAAA;;SAItC,oBAAsB,KAAA,OACzB;CAEF,IAAA,QAAO,MAAA,UAAA,KAAA,KAAA,UAAA,MAAA,OAAA;;;;;;;;;;;;;GAgBT,MAAgB,GAAA,MAAA;GACd,eAAU;SAGH,IAAA,CAAA,cAAa;;;;;;;;;;;;;;SAeN,IAAA,CAAA,cAA4B;GAC1C,MAAI;kBAEc;;;;;;;;;;;;EAmBpB,IAAA,IAAiB,WAA6B,IAAA,IAAoD,IAAA,WAAA,IAAA,IAAA,IAAA,WAAA,IAAA,IAAA,IAAA,WAAA,IAAA,IAAA,IAAA,SAAA,IAAA,EAAA,OAAA;EAChG,OAAK,UAAY,IAAA;;;;;;;;;QAcnB;EACE,IAAI,aAAO;EAEX,MAAO,aAAO,EAAA;;;;;;;;;;;;;;;;GAiBhB,aAAa;;EAEX,IAAA,YAAA;GACA,MAAA,GAAA,SAAA,IAAA;GACA,KAAA,MAAA,QAAA,YAAA,MAAA;GACA,MAAA,GAAA,OAAA;;;;SAKA,iBAAA,OAAA;CACA,IAAA,MAAA,WAAA,GAAA,OAAA,EAAA;CACA,MAAA,OAAA,MAAA,SAAA,IAAA;CACA,MAAA,MAAA,MAAA,KAAA,EAAA,QAAA,MAAA,SAAA,EAAA,EAAA;CACA,IAAA,IAAA;CACA,KAAA,MAAA,SAAA,OAAA;EACA,IAAA,KAAA;EACA,KAAA;;CAEA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SChEE,YAAU,SACR,KAAA;CAEJ,IAAA,OAAO,YAAe,YAAO,YAAc,QAAQ,CAAA,OAAA,UAAA,eAAA,KAAA,SAAA,IAAA,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmErD,MAAA,UAAgB,oBACiC,GAC/C,QAAA,CAAA,MACU;EACV,IAAA,CAAA,SAAW"}