@tenphi/tasty 0.0.0-snapshot.09c74e2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/dist/_virtual/_rolldown/runtime.js +8 -0
- package/dist/chunks/cacheKey.js +70 -0
- package/dist/chunks/cacheKey.js.map +1 -0
- package/dist/chunks/definitions.d.ts +37 -0
- package/dist/chunks/definitions.js +260 -0
- package/dist/chunks/definitions.js.map +1 -0
- package/dist/chunks/renderChunk.js +61 -0
- package/dist/chunks/renderChunk.js.map +1 -0
- package/dist/config.d.ts +280 -0
- package/dist/config.js +403 -0
- package/dist/config.js.map +1 -0
- package/dist/core/index.d.ts +33 -0
- package/dist/core/index.js +26 -0
- package/dist/debug.d.ts +204 -0
- package/dist/debug.js +733 -0
- package/dist/debug.js.map +1 -0
- package/dist/hooks/useGlobalStyles.d.ts +27 -0
- package/dist/hooks/useGlobalStyles.js +56 -0
- package/dist/hooks/useGlobalStyles.js.map +1 -0
- package/dist/hooks/useKeyframes.d.ts +56 -0
- package/dist/hooks/useKeyframes.js +54 -0
- package/dist/hooks/useKeyframes.js.map +1 -0
- package/dist/hooks/useProperty.d.ts +79 -0
- package/dist/hooks/useProperty.js +91 -0
- package/dist/hooks/useProperty.js.map +1 -0
- package/dist/hooks/useRawCSS.d.ts +53 -0
- package/dist/hooks/useRawCSS.js +28 -0
- package/dist/hooks/useRawCSS.js.map +1 -0
- package/dist/hooks/useStyles.d.ts +40 -0
- package/dist/hooks/useStyles.js +169 -0
- package/dist/hooks/useStyles.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +33 -0
- package/dist/injector/index.d.ts +157 -0
- package/dist/injector/index.js +154 -0
- package/dist/injector/index.js.map +1 -0
- package/dist/injector/injector.d.ts +139 -0
- package/dist/injector/injector.js +404 -0
- package/dist/injector/injector.js.map +1 -0
- package/dist/injector/sheet-manager.d.ts +127 -0
- package/dist/injector/sheet-manager.js +714 -0
- package/dist/injector/sheet-manager.js.map +1 -0
- package/dist/injector/types.d.ts +135 -0
- package/dist/keyframes/index.js +206 -0
- package/dist/keyframes/index.js.map +1 -0
- package/dist/parser/classify.js +319 -0
- package/dist/parser/classify.js.map +1 -0
- package/dist/parser/const.js +33 -0
- package/dist/parser/const.js.map +1 -0
- package/dist/parser/lru.js +109 -0
- package/dist/parser/lru.js.map +1 -0
- package/dist/parser/parser.d.ts +25 -0
- package/dist/parser/parser.js +116 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/parser/tokenizer.js +69 -0
- package/dist/parser/tokenizer.js.map +1 -0
- package/dist/parser/types.d.ts +51 -0
- package/dist/parser/types.js +46 -0
- package/dist/parser/types.js.map +1 -0
- package/dist/pipeline/conditions.d.ts +134 -0
- package/dist/pipeline/conditions.js +406 -0
- package/dist/pipeline/conditions.js.map +1 -0
- package/dist/pipeline/exclusive.js +231 -0
- package/dist/pipeline/exclusive.js.map +1 -0
- package/dist/pipeline/index.d.ts +53 -0
- package/dist/pipeline/index.js +660 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/materialize.js +844 -0
- package/dist/pipeline/materialize.js.map +1 -0
- package/dist/pipeline/parseStateKey.d.ts +15 -0
- package/dist/pipeline/parseStateKey.js +438 -0
- package/dist/pipeline/parseStateKey.js.map +1 -0
- package/dist/pipeline/simplify.js +516 -0
- package/dist/pipeline/simplify.js.map +1 -0
- package/dist/pipeline/warnings.js +18 -0
- package/dist/pipeline/warnings.js.map +1 -0
- package/dist/plugins/okhsl-plugin.d.ts +35 -0
- package/dist/plugins/okhsl-plugin.js +371 -0
- package/dist/plugins/okhsl-plugin.js.map +1 -0
- package/dist/plugins/types.d.ts +69 -0
- package/dist/properties/index.js +158 -0
- package/dist/properties/index.js.map +1 -0
- package/dist/states/index.d.ts +49 -0
- package/dist/states/index.js +416 -0
- package/dist/states/index.js.map +1 -0
- package/dist/static/index.d.ts +5 -0
- package/dist/static/index.js +5 -0
- package/dist/static/tastyStatic.d.ts +46 -0
- package/dist/static/tastyStatic.js +31 -0
- package/dist/static/tastyStatic.js.map +1 -0
- package/dist/static/types.d.ts +49 -0
- package/dist/static/types.js +24 -0
- package/dist/static/types.js.map +1 -0
- package/dist/styles/align.d.ts +15 -0
- package/dist/styles/align.js +14 -0
- package/dist/styles/align.js.map +1 -0
- package/dist/styles/border.d.ts +25 -0
- package/dist/styles/border.js +114 -0
- package/dist/styles/border.js.map +1 -0
- package/dist/styles/color.d.ts +14 -0
- package/dist/styles/color.js +23 -0
- package/dist/styles/color.js.map +1 -0
- package/dist/styles/createStyle.js +77 -0
- package/dist/styles/createStyle.js.map +1 -0
- package/dist/styles/dimension.js +97 -0
- package/dist/styles/dimension.js.map +1 -0
- package/dist/styles/display.d.ts +37 -0
- package/dist/styles/display.js +67 -0
- package/dist/styles/display.js.map +1 -0
- package/dist/styles/fade.d.ts +15 -0
- package/dist/styles/fade.js +58 -0
- package/dist/styles/fade.js.map +1 -0
- package/dist/styles/fill.d.ts +42 -0
- package/dist/styles/fill.js +52 -0
- package/dist/styles/fill.js.map +1 -0
- package/dist/styles/flow.d.ts +16 -0
- package/dist/styles/flow.js +12 -0
- package/dist/styles/flow.js.map +1 -0
- package/dist/styles/gap.d.ts +31 -0
- package/dist/styles/gap.js +37 -0
- package/dist/styles/gap.js.map +1 -0
- package/dist/styles/height.d.ts +17 -0
- package/dist/styles/height.js +20 -0
- package/dist/styles/height.js.map +1 -0
- package/dist/styles/index.d.ts +2 -0
- package/dist/styles/index.js +9 -0
- package/dist/styles/index.js.map +1 -0
- package/dist/styles/inset.d.ts +52 -0
- package/dist/styles/inset.js +150 -0
- package/dist/styles/inset.js.map +1 -0
- package/dist/styles/justify.d.ts +15 -0
- package/dist/styles/justify.js +14 -0
- package/dist/styles/justify.js.map +1 -0
- package/dist/styles/list.d.ts +16 -0
- package/dist/styles/list.js +98 -0
- package/dist/styles/list.js.map +1 -0
- package/dist/styles/margin.d.ts +24 -0
- package/dist/styles/margin.js +104 -0
- package/dist/styles/margin.js.map +1 -0
- package/dist/styles/outline.d.ts +29 -0
- package/dist/styles/outline.js +65 -0
- package/dist/styles/outline.js.map +1 -0
- package/dist/styles/padding.d.ts +24 -0
- package/dist/styles/padding.js +104 -0
- package/dist/styles/padding.js.map +1 -0
- package/dist/styles/predefined.d.ts +73 -0
- package/dist/styles/predefined.js +241 -0
- package/dist/styles/predefined.js.map +1 -0
- package/dist/styles/preset.d.ts +47 -0
- package/dist/styles/preset.js +126 -0
- package/dist/styles/preset.js.map +1 -0
- package/dist/styles/radius.d.ts +14 -0
- package/dist/styles/radius.js +51 -0
- package/dist/styles/radius.js.map +1 -0
- package/dist/styles/scrollbar.d.ts +21 -0
- package/dist/styles/scrollbar.js +112 -0
- package/dist/styles/scrollbar.js.map +1 -0
- package/dist/styles/shadow.d.ts +14 -0
- package/dist/styles/shadow.js +24 -0
- package/dist/styles/shadow.js.map +1 -0
- package/dist/styles/styledScrollbar.d.ts +47 -0
- package/dist/styles/styledScrollbar.js +38 -0
- package/dist/styles/styledScrollbar.js.map +1 -0
- package/dist/styles/transition.d.ts +14 -0
- package/dist/styles/transition.js +158 -0
- package/dist/styles/transition.js.map +1 -0
- package/dist/styles/types.d.ts +498 -0
- package/dist/styles/width.d.ts +17 -0
- package/dist/styles/width.js +20 -0
- package/dist/styles/width.js.map +1 -0
- package/dist/tasty.d.ts +982 -0
- package/dist/tasty.js +206 -0
- package/dist/tasty.js.map +1 -0
- package/dist/tokens/typography.d.ts +19 -0
- package/dist/tokens/typography.js +237 -0
- package/dist/tokens/typography.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/utils/cache-wrapper.js +26 -0
- package/dist/utils/cache-wrapper.js.map +1 -0
- package/dist/utils/case-converter.js +8 -0
- package/dist/utils/case-converter.js.map +1 -0
- package/dist/utils/colors.d.ts +5 -0
- package/dist/utils/colors.js +9 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/css-types.d.ts +7 -0
- package/dist/utils/dotize.d.ts +26 -0
- package/dist/utils/dotize.js +122 -0
- package/dist/utils/dotize.js.map +1 -0
- package/dist/utils/filter-base-props.d.ts +15 -0
- package/dist/utils/filter-base-props.js +45 -0
- package/dist/utils/filter-base-props.js.map +1 -0
- package/dist/utils/get-display-name.d.ts +7 -0
- package/dist/utils/get-display-name.js +10 -0
- package/dist/utils/get-display-name.js.map +1 -0
- package/dist/utils/hsl-to-rgb.js +38 -0
- package/dist/utils/hsl-to-rgb.js.map +1 -0
- package/dist/utils/is-dev-env.js +19 -0
- package/dist/utils/is-dev-env.js.map +1 -0
- package/dist/utils/is-valid-element-type.js +15 -0
- package/dist/utils/is-valid-element-type.js.map +1 -0
- package/dist/utils/merge-styles.d.ts +7 -0
- package/dist/utils/merge-styles.js +146 -0
- package/dist/utils/merge-styles.js.map +1 -0
- package/dist/utils/mod-attrs.d.ts +8 -0
- package/dist/utils/mod-attrs.js +21 -0
- package/dist/utils/mod-attrs.js.map +1 -0
- package/dist/utils/okhsl-to-rgb.js +296 -0
- package/dist/utils/okhsl-to-rgb.js.map +1 -0
- package/dist/utils/process-tokens.d.ts +31 -0
- package/dist/utils/process-tokens.js +171 -0
- package/dist/utils/process-tokens.js.map +1 -0
- package/dist/utils/resolve-recipes.d.ts +17 -0
- package/dist/utils/resolve-recipes.js +147 -0
- package/dist/utils/resolve-recipes.js.map +1 -0
- package/dist/utils/string.js +8 -0
- package/dist/utils/string.js.map +1 -0
- package/dist/utils/styles.d.ts +178 -0
- package/dist/utils/styles.js +590 -0
- package/dist/utils/styles.js.map +1 -0
- package/dist/utils/typography.d.ts +36 -0
- package/dist/utils/typography.js +53 -0
- package/dist/utils/typography.js.map +1 -0
- package/dist/utils/warnings.d.ts +16 -0
- package/dist/utils/warnings.js +16 -0
- package/dist/utils/warnings.js.map +1 -0
- package/dist/zero/babel.d.ts +108 -0
- package/dist/zero/babel.js +282 -0
- package/dist/zero/babel.js.map +1 -0
- package/dist/zero/css-writer.d.ts +45 -0
- package/dist/zero/css-writer.js +74 -0
- package/dist/zero/css-writer.js.map +1 -0
- package/dist/zero/extractor.d.ts +24 -0
- package/dist/zero/extractor.js +150 -0
- package/dist/zero/extractor.js.map +1 -0
- package/dist/zero/index.d.ts +3 -0
- package/dist/zero/index.js +4 -0
- package/dist/zero/next.d.ts +60 -0
- package/dist/zero/next.js +78 -0
- package/dist/zero/next.js.map +1 -0
- package/package.json +189 -0
- package/tasty.config.ts +14 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseStateKey.js","names":[],"sources":["../../src/pipeline/parseStateKey.ts"],"sourcesContent":["/**\n * State Key Parser\n *\n * Parses state notation strings (like 'hovered & !disabled', '@media(w < 768px)')\n * into ConditionNode trees for processing in the pipeline.\n */\n\nimport { Lru } from '../parser/lru';\nimport type { StateParserContext } from '../states';\nimport {\n expandDimensionShorthands,\n expandTastyUnits,\n findTopLevelComma,\n resolvePredefinedState,\n} from '../states';\nimport { camelToKebab } from '../utils/case-converter';\n\nimport type { ConditionNode, NumericBound } from './conditions';\nimport { emitWarning } from './warnings';\nimport {\n and,\n createContainerDimensionCondition,\n createContainerRawCondition,\n createContainerStyleCondition,\n createMediaDimensionCondition,\n createMediaFeatureCondition,\n createMediaTypeCondition,\n createModifierCondition,\n createOwnCondition,\n createParentCondition,\n createPseudoCondition,\n createRootCondition,\n createStartingCondition,\n createSupportsCondition,\n not,\n or,\n trueCondition,\n} from './conditions';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n/**\n * Maximum XOR operands before emitting a performance warning.\n * A ^ B ^ C ^ D = 8 OR branches (2^(n-1)), so chains above 4\n * risk exponential blowup in downstream processing.\n */\nconst MAX_XOR_CHAIN_LENGTH = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface ParseStateKeyOptions {\n context?: StateParserContext;\n isSubElement?: boolean;\n}\n\n// ============================================================================\n// Caching\n// ============================================================================\n\n// Cache for parsed state keys (key -> ConditionNode)\nconst parseCache = new Lru<string, ConditionNode>(5000);\n\n// ============================================================================\n// Tokenizer Patterns\n// ============================================================================\n\n/**\n * Pattern for tokenizing state notation.\n * Matches: operators, parentheses, @-prefixed states, value mods, boolean mods,\n * pseudo-classes, class selectors, and attribute selectors.\n *\n * Note: For @supports and @(...) container queries we need to handle nested parentheses\n * like @supports($, :has(*)) or @(scroll-state(stuck: top)).\n * We use a pattern that allows one level of nesting: [^()]*(?:\\([^)]*\\))?[^)]*\n */\nconst STATE_TOKEN_PATTERN =\n /([&|!^])|([()])|(@media:[a-z]+)|(@media\\([^)]+\\))|(@supports\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@root\\([^)]+\\))|(@parent\\([^)]+\\))|(@own\\([^)]+\\))|(@\\([^()]*(?:\\([^)]*\\))?[^)]*\\))|(@starting)|(@[A-Za-z][A-Za-z0-9-]*)|([a-z][a-z0-9-]*(?:\\^=|\\$=|\\*=|=)(?:\"[^\"]*\"|'[^']*'|[^\\s&|!^()]+))|([a-z][a-z0-9-]+)|(:[-a-z][a-z0-9-]*(?:\\([^)]+\\))?)|(\\.[a-z][a-z0-9-]+)|(\\[[^\\]]+\\])/gi;\n\n// ============================================================================\n// Token Types\n// ============================================================================\n\ntype TokenType = 'AND' | 'OR' | 'NOT' | 'XOR' | 'LPAREN' | 'RPAREN' | 'STATE';\n\ninterface Token {\n type: TokenType;\n value: string;\n raw: string;\n}\n\n// ============================================================================\n// Tokenizer\n// ============================================================================\n\n/**\n * Tokenize a state notation string\n */\nfunction tokenize(stateKey: string): Token[] {\n const tokens: Token[] = [];\n let match: RegExpExecArray | null;\n\n // Replace commas with | outside of parentheses (for compatibility)\n const normalized = replaceCommasOutsideParens(stateKey);\n\n STATE_TOKEN_PATTERN.lastIndex = 0;\n while ((match = STATE_TOKEN_PATTERN.exec(normalized)) !== null) {\n const fullMatch = match[0];\n\n if (match[1]) {\n // Operator: &, |, !, ^\n switch (fullMatch) {\n case '&':\n tokens.push({ type: 'AND', value: '&', raw: fullMatch });\n break;\n case '|':\n tokens.push({ type: 'OR', value: '|', raw: fullMatch });\n break;\n case '!':\n tokens.push({ type: 'NOT', value: '!', raw: fullMatch });\n break;\n case '^':\n tokens.push({ type: 'XOR', value: '^', raw: fullMatch });\n break;\n }\n } else if (match[2]) {\n // Parenthesis\n if (fullMatch === '(') {\n tokens.push({ type: 'LPAREN', value: '(', raw: fullMatch });\n } else {\n tokens.push({ type: 'RPAREN', value: ')', raw: fullMatch });\n }\n } else {\n // State token (all other capture groups)\n tokens.push({ type: 'STATE', value: fullMatch, raw: fullMatch });\n }\n }\n\n return tokens;\n}\n\n/**\n * Replace commas with | only outside of parentheses\n */\nfunction replaceCommasOutsideParens(str: string): string {\n let result = '';\n let depth = 0;\n\n for (const char of str) {\n if (char === '(') {\n depth++;\n result += char;\n } else if (char === ')') {\n depth--;\n result += char;\n } else if (char === ',' && depth === 0) {\n result += '|';\n } else {\n result += char;\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Recursive Descent Parser\n// ============================================================================\n\n/**\n * Parser state\n */\nclass Parser {\n private tokens: Token[];\n private pos = 0;\n private options: ParseStateKeyOptions;\n\n constructor(tokens: Token[], options: ParseStateKeyOptions) {\n this.tokens = tokens;\n this.options = options;\n }\n\n parse(): ConditionNode {\n if (this.tokens.length === 0) {\n return trueCondition();\n }\n const result = this.parseExpression();\n return result;\n }\n\n private current(): Token | undefined {\n return this.tokens[this.pos];\n }\n\n private advance(): Token | undefined {\n return this.tokens[this.pos++];\n }\n\n private match(type: TokenType): boolean {\n if (this.current()?.type === type) {\n this.advance();\n return true;\n }\n return false;\n }\n\n /**\n * Parse expression with operator precedence:\n * ! (NOT) > ^ (XOR) > | (OR) > & (AND)\n */\n private parseExpression(): ConditionNode {\n return this.parseAnd();\n }\n\n private parseAnd(): ConditionNode {\n let left = this.parseOr();\n\n while (this.current()?.type === 'AND') {\n this.advance();\n const right = this.parseOr();\n left = and(left, right);\n }\n\n return left;\n }\n\n private parseOr(): ConditionNode {\n let left = this.parseXor();\n\n while (this.current()?.type === 'OR') {\n this.advance();\n const right = this.parseXor();\n left = or(left, right);\n }\n\n return left;\n }\n\n private parseXor(): ConditionNode {\n let left = this.parseUnary();\n let operandCount = 1;\n\n while (this.current()?.type === 'XOR') {\n this.advance();\n const right = this.parseUnary();\n operandCount++;\n\n if (operandCount > MAX_XOR_CHAIN_LENGTH) {\n emitWarning(\n 'XOR_CHAIN_TOO_LONG',\n `XOR chain with ${operandCount} operands produces ${Math.pow(2, operandCount - 1)} OR branches. ` +\n `Consider breaking into smaller expressions to avoid exponential growth.`,\n );\n }\n\n // XOR: (A & !B) | (!A & B)\n left = or(and(left, not(right)), and(not(left), right));\n }\n\n return left;\n }\n\n private parseUnary(): ConditionNode {\n if (this.match('NOT')) {\n const operand = this.parseUnary();\n return not(operand);\n }\n return this.parsePrimary();\n }\n\n private parsePrimary(): ConditionNode {\n // Handle parentheses\n if (this.match('LPAREN')) {\n const expr = this.parseExpression();\n this.match('RPAREN'); // Consume closing paren (lenient if missing)\n return expr;\n }\n\n // Handle state tokens\n const token = this.current();\n if (token?.type === 'STATE') {\n this.advance();\n return this.parseStateToken(token.value);\n }\n\n // Fallback for empty/invalid - return TRUE\n return trueCondition();\n }\n\n /**\n * Parse a state token into a ConditionNode\n */\n private parseStateToken(value: string): ConditionNode {\n // @starting\n if (value === '@starting') {\n return createStartingCondition(false, value);\n }\n\n // @media:type (e.g., @media:print)\n if (value.startsWith('@media:')) {\n const mediaType = value.slice(7) as 'print' | 'screen' | 'all' | 'speech';\n return createMediaTypeCondition(mediaType, false, value);\n }\n\n // @media(...) - media query\n if (value.startsWith('@media(')) {\n return this.parseMediaQuery(value);\n }\n\n // @supports(...) - feature/selector support query\n if (value.startsWith('@supports(')) {\n return this.parseSupportsQuery(value);\n }\n\n // @root(...) - root state\n if (value.startsWith('@root(')) {\n return this.parseRootState(value);\n }\n\n // @parent(...) - parent element state\n if (value.startsWith('@parent(')) {\n return this.parseParentState(value);\n }\n\n // @own(...) - own state (sub-element)\n if (value.startsWith('@own(')) {\n return this.parseOwnState(value);\n }\n\n // @(...) - container query\n if (value.startsWith('@(')) {\n return this.parseContainerQuery(value);\n }\n\n // @name - predefined state\n if (value.startsWith('@') && /^@[A-Za-z][A-Za-z0-9-]*$/.test(value)) {\n return this.parsePredefinedState(value);\n }\n\n // Pseudo-class (e.g., :hover, :focus-visible, :nth-child(2n))\n if (value.startsWith(':')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Class selector (e.g., .active)\n if (value.startsWith('.')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Attribute selector (e.g., [disabled], [data-state=\"active\"])\n if (value.startsWith('[')) {\n return createPseudoCondition(value, false, value);\n }\n\n // Value modifier (e.g., theme=danger, size=large)\n if (value.includes('=')) {\n return this.parseValueModifier(value);\n }\n\n // Boolean modifier (e.g., hovered, disabled)\n return this.parseBooleanModifier(value);\n }\n\n /**\n * Parse @media(...) query\n */\n private parseMediaQuery(raw: string): ConditionNode {\n const content = raw.slice(7, -1); // Remove '@media(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Expand shorthands and units\n let condition = expandDimensionShorthands(content);\n condition = expandTastyUnits(condition);\n\n // Check for feature queries (contains ':' but not dimension comparison)\n if (\n condition.includes(':') &&\n !condition.includes('<') &&\n !condition.includes('>') &&\n !condition.includes('=')\n ) {\n // Feature query: @media(prefers-contrast: high)\n const colonIdx = condition.indexOf(':');\n const feature = condition.slice(0, colonIdx).trim();\n const featureValue = condition.slice(colonIdx + 1).trim();\n return createMediaFeatureCondition(feature, featureValue, false, raw);\n }\n\n // Boolean feature query: @media(prefers-reduced-motion)\n if (\n !condition.includes('<') &&\n !condition.includes('>') &&\n !condition.includes('=')\n ) {\n return createMediaFeatureCondition(\n condition.trim(),\n undefined,\n false,\n raw,\n );\n }\n\n // Dimension query - parse bounds\n const { dimension, lowerBound, upperBound } =\n this.parseDimensionCondition(condition);\n\n if (!dimension) {\n // Fallback for unparseable - treat as pseudo\n return createPseudoCondition(raw, false, raw);\n }\n\n return createMediaDimensionCondition(\n dimension as 'width' | 'height',\n lowerBound,\n upperBound,\n false,\n raw,\n );\n }\n\n /**\n * Parse dimension condition string (e.g., \"width < 768px\", \"600px <= width < 1200px\")\n */\n private parseDimensionCondition(condition: string): {\n dimension?: string;\n lowerBound?: NumericBound;\n upperBound?: NumericBound;\n } {\n // Range syntax: \"600px <= width < 1200px\"\n const rangeMatch = condition.match(\n /^(.+?)\\s*(<=|<)\\s*(width|height|inline-size|block-size)\\s*(<=|<)\\s*(.+)$/,\n );\n if (rangeMatch) {\n const [, lowerValue, lowerOp, dimension, upperOp, upperValue] =\n rangeMatch;\n return {\n dimension,\n lowerBound: {\n value: lowerValue.trim(),\n valueNumeric: parseNumericValue(lowerValue.trim()),\n inclusive: lowerOp === '<=',\n },\n upperBound: {\n value: upperValue.trim(),\n valueNumeric: parseNumericValue(upperValue.trim()),\n inclusive: upperOp === '<=',\n },\n };\n }\n\n // Simple comparison: \"width < 768px\"\n const simpleMatch = condition.match(\n /^(width|height|inline-size|block-size)\\s*(<=|>=|<|>|=)\\s*(.+)$/,\n );\n if (simpleMatch) {\n const [, dimension, operator, value] = simpleMatch;\n const numeric = parseNumericValue(value.trim());\n\n if (operator === '<' || operator === '<=') {\n return {\n dimension,\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '<=',\n },\n };\n } else if (operator === '>' || operator === '>=') {\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '>=',\n },\n };\n } else if (operator === '=') {\n // Exact match: both bounds are the same and inclusive\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: true,\n },\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: true,\n },\n };\n }\n }\n\n // Reversed: \"768px > width\"\n const reversedMatch = condition.match(\n /^(.+?)\\s*(<=|>=|<|>|=)\\s*(width|height|inline-size|block-size)$/,\n );\n if (reversedMatch) {\n const [, value, operator, dimension] = reversedMatch;\n const numeric = parseNumericValue(value.trim());\n\n // Reverse the operator\n if (operator === '<' || operator === '<=') {\n return {\n dimension,\n lowerBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '<=',\n },\n };\n } else if (operator === '>' || operator === '>=') {\n return {\n dimension,\n upperBound: {\n value: value.trim(),\n valueNumeric: numeric,\n inclusive: operator === '>=',\n },\n };\n }\n }\n\n return {};\n }\n\n /**\n * Parse @root(...) state\n */\n private parseInnerCondition(\n raw: string,\n prefixLen: number,\n wrap: (inner: ConditionNode) => ConditionNode,\n ): ConditionNode {\n const content = raw.slice(prefixLen, -1);\n if (!content.trim()) return trueCondition();\n return wrap(parseStateKey(content, this.options));\n }\n\n private parseRootState(raw: string): ConditionNode {\n return this.parseInnerCondition(raw, 6, (inner) =>\n createRootCondition(inner, false, raw),\n );\n }\n\n /**\n * Parse @parent(...) state\n *\n * Syntax:\n * @parent(hovered) → :is([data-hovered] *)\n * @parent(theme=dark) → :is([data-theme=\"dark\"] *)\n * @parent(hovered, >) → :is([data-hovered] > *) (direct parent)\n * @parent(.my-class) → :is(.my-class *)\n */\n private parseParentState(raw: string): ConditionNode {\n const content = raw.slice(8, -1);\n if (!content.trim()) {\n return trueCondition();\n }\n\n let condition = content.trim();\n let direct = false;\n\n const lastCommaIdx = condition.lastIndexOf(',');\n if (lastCommaIdx !== -1) {\n const afterComma = condition.slice(lastCommaIdx + 1).trim();\n if (afterComma === '>') {\n direct = true;\n condition = condition.slice(0, lastCommaIdx).trim();\n }\n }\n\n const innerCondition = parseStateKey(condition, this.options);\n return createParentCondition(innerCondition, direct, false, raw);\n }\n\n /**\n * Parse @supports(...) query\n *\n * Syntax:\n * @supports(display: grid) → @supports (display: grid)\n * @supports($, :has(*)) → @supports selector(:has(*))\n */\n private parseSupportsQuery(raw: string): ConditionNode {\n const content = raw.slice(10, -1); // Remove '@supports(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Check for selector syntax: @supports($, :has(*))\n if (content.startsWith('$,')) {\n const selector = content.slice(2).trim(); // Remove '$,' prefix\n return createSupportsCondition('selector', selector, false, raw);\n }\n\n // Feature syntax: @supports(display: grid)\n return createSupportsCondition('feature', content, false, raw);\n }\n\n private parseOwnState(raw: string): ConditionNode {\n return this.parseInnerCondition(raw, 5, (inner) =>\n createOwnCondition(inner, false, raw),\n );\n }\n\n /**\n * Parse @(...) container query\n */\n private parseContainerQuery(raw: string): ConditionNode {\n const content = raw.slice(2, -1); // Remove '@(' and ')'\n if (!content.trim()) {\n return trueCondition();\n }\n\n // Check for named container: @(layout, w < 600px)\n // Use parentheses-aware comma search so inner commas (e.g., scroll-state(a, b)) are skipped\n const commaIdx = findTopLevelComma(content);\n let containerName: string | undefined;\n let condition: string;\n\n if (commaIdx !== -1) {\n containerName = content.slice(0, commaIdx).trim();\n condition = content.slice(commaIdx + 1).trim();\n } else {\n condition = content.trim();\n }\n\n // Check for style query shorthand: @($variant=primary)\n if (condition.startsWith('$')) {\n const styleQuery = condition.slice(1); // Remove '$'\n const eqIdx = styleQuery.indexOf('=');\n\n if (eqIdx === -1) {\n // Existence check: @($variant)\n return createContainerStyleCondition(\n styleQuery,\n undefined,\n containerName,\n false,\n raw,\n );\n }\n\n const property = styleQuery.slice(0, eqIdx).trim();\n let propertyValue = styleQuery.slice(eqIdx + 1).trim();\n\n // Remove quotes if present\n if (\n (propertyValue.startsWith('\"') && propertyValue.endsWith('\"')) ||\n (propertyValue.startsWith(\"'\") && propertyValue.endsWith(\"'\"))\n ) {\n propertyValue = propertyValue.slice(1, -1);\n }\n\n return createContainerStyleCondition(\n property,\n propertyValue,\n containerName,\n false,\n raw,\n );\n }\n\n // Check for function-like syntax: scroll-state(...), style(...), etc.\n // Passes the condition through to CSS verbatim.\n if (/^[a-zA-Z][\\w-]*\\s*\\(/.test(condition)) {\n return createContainerRawCondition(condition, containerName, false, raw);\n }\n\n // Dimension query\n let expandedCondition = expandDimensionShorthands(condition);\n expandedCondition = expandTastyUnits(expandedCondition);\n\n const { dimension, lowerBound, upperBound } =\n this.parseDimensionCondition(expandedCondition);\n\n if (!dimension) {\n // Fallback\n return createPseudoCondition(raw, false, raw);\n }\n\n return createContainerDimensionCondition(\n dimension as 'width' | 'height',\n lowerBound,\n upperBound,\n containerName,\n false,\n raw,\n );\n }\n\n /**\n * Parse predefined state (@mobile, @dark, etc.)\n */\n private parsePredefinedState(raw: string): ConditionNode {\n const ctx = this.options.context;\n if (!ctx) {\n // No context - can't resolve predefined states\n return createPseudoCondition(raw, false, raw);\n }\n\n const resolved = resolvePredefinedState(raw, ctx);\n if (!resolved) {\n // Undefined predefined state - treat as modifier\n return createModifierCondition(\n `data-${camelToKebab(raw.slice(1))}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n\n // Parse the resolved value recursively\n return parseStateKey(resolved, this.options);\n }\n\n /**\n * Parse value modifier (e.g., theme=danger, size^=sm)\n */\n private parseValueModifier(raw: string): ConditionNode {\n // Match operators: =, ^=, $=, *=\n const opMatch = raw.match(/^([a-z][a-z0-9-]*)(\\^=|\\$=|\\*=|=)(.+)$/i);\n if (!opMatch) {\n return createModifierCondition(\n `data-${camelToKebab(raw)}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n\n const [, key, operator, value] = opMatch;\n let cleanValue = value;\n\n // Remove quotes if present\n if (\n (cleanValue.startsWith('\"') && cleanValue.endsWith('\"')) ||\n (cleanValue.startsWith(\"'\") && cleanValue.endsWith(\"'\"))\n ) {\n cleanValue = cleanValue.slice(1, -1);\n }\n\n return createModifierCondition(\n `data-${camelToKebab(key)}`,\n cleanValue,\n operator as '=' | '^=' | '$=' | '*=',\n false,\n raw,\n );\n }\n\n /**\n * Parse boolean modifier (e.g., hovered, disabled)\n */\n private parseBooleanModifier(raw: string): ConditionNode {\n return createModifierCondition(\n `data-${camelToKebab(raw)}`,\n undefined,\n '=',\n false,\n raw,\n );\n }\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Parse a numeric value from a CSS value string\n */\nfunction parseNumericValue(value: string): number | null {\n const match = value.match(/^(\\d+(?:\\.\\d+)?)(px|em|rem|vh|vw|%)?$/);\n if (match) {\n return parseFloat(match[1]);\n }\n return null;\n}\n\n// ============================================================================\n// Main Export\n// ============================================================================\n\n/**\n * Parse a state key string into a ConditionNode\n */\nexport function parseStateKey(\n stateKey: string,\n options: ParseStateKeyOptions = {},\n): ConditionNode {\n // Handle empty/default state\n if (!stateKey || !stateKey.trim()) {\n return trueCondition();\n }\n\n const trimmed = stateKey.trim();\n\n // Build cache key including local predefined states (they affect parsing)\n // Global predefined states are set once at initialization and don't change\n const ctx = options.context;\n const localStatesKey =\n ctx && Object.keys(ctx.localPredefinedStates).length > 0\n ? JSON.stringify(ctx.localPredefinedStates)\n : '';\n const cacheKey = JSON.stringify([\n trimmed,\n options.isSubElement,\n localStatesKey,\n ]);\n\n // Check cache\n const cached = parseCache.get(cacheKey);\n if (cached) {\n return cached;\n }\n\n // Tokenize and parse\n const tokens = tokenize(trimmed);\n const parser = new Parser(tokens, options);\n const result = parser.parse();\n\n // Cache result\n parseCache.set(cacheKey, result);\n\n return result;\n}\n\n/**\n * Clear the parse cache (for testing)\n */\nexport function clearParseCache(): void {\n parseCache.clear();\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { Lru } from "../parser/lru.js";
|
|
2
|
+
import { falseCondition, getConditionUniqueId, trueCondition } from "./conditions.js";
|
|
3
|
+
|
|
4
|
+
//#region src/pipeline/simplify.ts
|
|
5
|
+
/**
|
|
6
|
+
* Condition Simplification Engine
|
|
7
|
+
*
|
|
8
|
+
* Simplifies condition trees by applying boolean algebra rules,
|
|
9
|
+
* detecting contradictions, merging ranges, and deduplicating terms.
|
|
10
|
+
*
|
|
11
|
+
* This is critical for:
|
|
12
|
+
* 1. Detecting invalid combinations (A & !A → FALSE)
|
|
13
|
+
* 2. Reducing CSS output size
|
|
14
|
+
* 3. Producing cleaner selectors
|
|
15
|
+
*/
|
|
16
|
+
const simplifyCache = new Lru(5e3);
|
|
17
|
+
/**
|
|
18
|
+
* Simplify a condition tree aggressively.
|
|
19
|
+
*
|
|
20
|
+
* This applies all possible simplification rules:
|
|
21
|
+
* - Boolean algebra (identity, annihilator, idempotent, absorption)
|
|
22
|
+
* - Contradiction detection (A & !A → FALSE)
|
|
23
|
+
* - Tautology detection (A | !A → TRUE)
|
|
24
|
+
* - Range intersection for numeric queries
|
|
25
|
+
* - Attribute value conflict detection
|
|
26
|
+
* - Deduplication and sorting
|
|
27
|
+
*/
|
|
28
|
+
function simplifyCondition(node) {
|
|
29
|
+
const key = getConditionUniqueId(node);
|
|
30
|
+
const cached = simplifyCache.get(key);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const result = simplifyInner(node);
|
|
33
|
+
simplifyCache.set(key, result);
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
function simplifyInner(node) {
|
|
37
|
+
if (node.kind === "true" || node.kind === "false") return node;
|
|
38
|
+
if (node.kind === "state") return node;
|
|
39
|
+
if (node.kind === "compound") {
|
|
40
|
+
const simplifiedChildren = node.children.map((c) => simplifyInner(c));
|
|
41
|
+
if (node.operator === "AND") return simplifyAnd(simplifiedChildren);
|
|
42
|
+
else return simplifyOr(simplifiedChildren);
|
|
43
|
+
}
|
|
44
|
+
return node;
|
|
45
|
+
}
|
|
46
|
+
function simplifyAnd(children) {
|
|
47
|
+
let terms = [];
|
|
48
|
+
for (const child of children) {
|
|
49
|
+
if (child.kind === "false") return falseCondition();
|
|
50
|
+
if (child.kind === "true") continue;
|
|
51
|
+
if (child.kind === "compound" && child.operator === "AND") terms.push(...child.children);
|
|
52
|
+
else terms.push(child);
|
|
53
|
+
}
|
|
54
|
+
if (terms.length === 0) return trueCondition();
|
|
55
|
+
if (terms.length === 1) return terms[0];
|
|
56
|
+
if (hasContradiction(terms)) return falseCondition();
|
|
57
|
+
if (hasRangeContradiction(terms)) return falseCondition();
|
|
58
|
+
if (hasAttributeConflict(terms)) return falseCondition();
|
|
59
|
+
if (hasContainerStyleConflict(terms)) return falseCondition();
|
|
60
|
+
terms = removeImpliedNegations(terms);
|
|
61
|
+
terms = deduplicateTerms(terms);
|
|
62
|
+
terms = mergeRanges(terms);
|
|
63
|
+
terms = sortTerms(terms);
|
|
64
|
+
terms = applyAbsorptionAnd(terms);
|
|
65
|
+
if (terms.length === 0) return trueCondition();
|
|
66
|
+
if (terms.length === 1) return terms[0];
|
|
67
|
+
return {
|
|
68
|
+
kind: "compound",
|
|
69
|
+
operator: "AND",
|
|
70
|
+
children: terms
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function simplifyOr(children) {
|
|
74
|
+
let terms = [];
|
|
75
|
+
for (const child of children) {
|
|
76
|
+
if (child.kind === "true") return trueCondition();
|
|
77
|
+
if (child.kind === "false") continue;
|
|
78
|
+
if (child.kind === "compound" && child.operator === "OR") terms.push(...child.children);
|
|
79
|
+
else terms.push(child);
|
|
80
|
+
}
|
|
81
|
+
if (terms.length === 0) return falseCondition();
|
|
82
|
+
if (terms.length === 1) return terms[0];
|
|
83
|
+
if (hasTautology(terms)) return trueCondition();
|
|
84
|
+
terms = deduplicateTerms(terms);
|
|
85
|
+
terms = sortTerms(terms);
|
|
86
|
+
terms = applyAbsorptionOr(terms);
|
|
87
|
+
if (terms.length === 0) return falseCondition();
|
|
88
|
+
if (terms.length === 1) return terms[0];
|
|
89
|
+
return {
|
|
90
|
+
kind: "compound",
|
|
91
|
+
operator: "OR",
|
|
92
|
+
children: terms
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Check if any pair of terms has complementary negation (A and !A).
|
|
97
|
+
* Used for both contradiction detection (in AND) and tautology detection (in OR),
|
|
98
|
+
* since the underlying check is identical: the context determines the semantics.
|
|
99
|
+
*/
|
|
100
|
+
function hasComplementaryPair(terms) {
|
|
101
|
+
const uniqueIds = /* @__PURE__ */ new Set();
|
|
102
|
+
for (const term of terms) {
|
|
103
|
+
if (term.kind !== "state") continue;
|
|
104
|
+
const id = term.uniqueId;
|
|
105
|
+
const negatedId = term.negated ? id.slice(1) : `!${id}`;
|
|
106
|
+
if (uniqueIds.has(negatedId)) return true;
|
|
107
|
+
uniqueIds.add(id);
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const hasContradiction = hasComplementaryPair;
|
|
112
|
+
const hasTautology = hasComplementaryPair;
|
|
113
|
+
/**
|
|
114
|
+
* Check for range contradictions in media/container queries
|
|
115
|
+
* e.g., @media(w < 400px) & @media(w > 800px) → FALSE
|
|
116
|
+
*
|
|
117
|
+
* Also handles negated conditions:
|
|
118
|
+
* - Single-bound negations are inverted (not (w < 600px) → w >= 600px)
|
|
119
|
+
* - Range negations create excluded ranges that are checked against positive bounds
|
|
120
|
+
*/
|
|
121
|
+
function hasRangeContradiction(terms) {
|
|
122
|
+
const mediaByDim = /* @__PURE__ */ new Map();
|
|
123
|
+
const containerByDim = /* @__PURE__ */ new Map();
|
|
124
|
+
for (const term of terms) {
|
|
125
|
+
if (term.kind !== "state") continue;
|
|
126
|
+
if (term.type === "media" && term.subtype === "dimension") {
|
|
127
|
+
const key = term.dimension || "width";
|
|
128
|
+
if (!mediaByDim.has(key)) mediaByDim.set(key, {
|
|
129
|
+
positive: [],
|
|
130
|
+
negated: []
|
|
131
|
+
});
|
|
132
|
+
const group = mediaByDim.get(key);
|
|
133
|
+
if (term.negated) group.negated.push(term);
|
|
134
|
+
else group.positive.push(term);
|
|
135
|
+
}
|
|
136
|
+
if (term.type === "container" && term.subtype === "dimension") {
|
|
137
|
+
const key = `${term.containerName || "_"}:${term.dimension || "width"}`;
|
|
138
|
+
if (!containerByDim.has(key)) containerByDim.set(key, {
|
|
139
|
+
positive: [],
|
|
140
|
+
negated: []
|
|
141
|
+
});
|
|
142
|
+
const group = containerByDim.get(key);
|
|
143
|
+
if (term.negated) group.negated.push(term);
|
|
144
|
+
else group.positive.push(term);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const group of mediaByDim.values()) if (rangesAreImpossibleWithNegations(group.positive, group.negated)) return true;
|
|
148
|
+
for (const group of containerByDim.values()) if (rangesAreImpossibleWithNegations(group.positive, group.negated)) return true;
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if conditions are impossible, including negated conditions.
|
|
153
|
+
*
|
|
154
|
+
* For negated single-bound conditions:
|
|
155
|
+
* not (w < 600px) → w >= 600px (inverted to lower bound)
|
|
156
|
+
* not (w >= 800px) → w < 800px (inverted to upper bound)
|
|
157
|
+
*
|
|
158
|
+
* For negated range conditions:
|
|
159
|
+
* not (400px <= w < 800px) → excludes [400, 800)
|
|
160
|
+
* If the effective bounds fall entirely within an excluded range, it's impossible.
|
|
161
|
+
*/
|
|
162
|
+
function rangesAreImpossibleWithNegations(positive, negated) {
|
|
163
|
+
const bounds = computeEffectiveBounds(positive);
|
|
164
|
+
const excludedRanges = [];
|
|
165
|
+
for (const cond of negated) {
|
|
166
|
+
const hasLower = cond.lowerBound?.valueNumeric != null;
|
|
167
|
+
const hasUpper = cond.upperBound?.valueNumeric != null;
|
|
168
|
+
if (hasLower && hasUpper) excludedRanges.push({
|
|
169
|
+
lower: cond.lowerBound.valueNumeric,
|
|
170
|
+
lowerInclusive: cond.lowerBound.inclusive,
|
|
171
|
+
upper: cond.upperBound.valueNumeric,
|
|
172
|
+
upperInclusive: cond.upperBound.inclusive
|
|
173
|
+
});
|
|
174
|
+
else if (hasUpper) {
|
|
175
|
+
const value = cond.upperBound.valueNumeric;
|
|
176
|
+
const inclusive = !cond.upperBound.inclusive;
|
|
177
|
+
if (bounds.lowerBound === null || value > bounds.lowerBound) {
|
|
178
|
+
bounds.lowerBound = value;
|
|
179
|
+
bounds.lowerInclusive = inclusive;
|
|
180
|
+
} else if (value === bounds.lowerBound && !inclusive) bounds.lowerInclusive = false;
|
|
181
|
+
} else if (hasLower) {
|
|
182
|
+
const value = cond.lowerBound.valueNumeric;
|
|
183
|
+
const inclusive = !cond.lowerBound.inclusive;
|
|
184
|
+
if (bounds.upperBound === null || value < bounds.upperBound) {
|
|
185
|
+
bounds.upperBound = value;
|
|
186
|
+
bounds.upperInclusive = inclusive;
|
|
187
|
+
} else if (value === bounds.upperBound && !inclusive) bounds.upperInclusive = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (bounds.lowerBound !== null && bounds.upperBound !== null) {
|
|
191
|
+
if (bounds.lowerBound > bounds.upperBound) return true;
|
|
192
|
+
if (bounds.lowerBound === bounds.upperBound && (!bounds.lowerInclusive || !bounds.upperInclusive)) return true;
|
|
193
|
+
}
|
|
194
|
+
if (bounds.lowerBound !== null && bounds.upperBound !== null && excludedRanges.length > 0) {
|
|
195
|
+
for (const excluded of excludedRanges) if (boundsWithinExcludedRange(bounds, excluded)) return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Compute effective bounds from positive (non-negated) conditions
|
|
201
|
+
*/
|
|
202
|
+
function computeEffectiveBounds(conditions) {
|
|
203
|
+
let lowerBound = null;
|
|
204
|
+
let lowerInclusive = false;
|
|
205
|
+
let upperBound = null;
|
|
206
|
+
let upperInclusive = false;
|
|
207
|
+
for (const cond of conditions) {
|
|
208
|
+
if (cond.lowerBound?.valueNumeric != null) {
|
|
209
|
+
const value = cond.lowerBound.valueNumeric;
|
|
210
|
+
const inclusive = cond.lowerBound.inclusive;
|
|
211
|
+
if (lowerBound === null || value > lowerBound) {
|
|
212
|
+
lowerBound = value;
|
|
213
|
+
lowerInclusive = inclusive;
|
|
214
|
+
} else if (value === lowerBound && !inclusive) lowerInclusive = false;
|
|
215
|
+
}
|
|
216
|
+
if (cond.upperBound?.valueNumeric != null) {
|
|
217
|
+
const value = cond.upperBound.valueNumeric;
|
|
218
|
+
const inclusive = cond.upperBound.inclusive;
|
|
219
|
+
if (upperBound === null || value < upperBound) {
|
|
220
|
+
upperBound = value;
|
|
221
|
+
upperInclusive = inclusive;
|
|
222
|
+
} else if (value === upperBound && !inclusive) upperInclusive = false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
lowerBound,
|
|
227
|
+
lowerInclusive,
|
|
228
|
+
upperBound,
|
|
229
|
+
upperInclusive
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Check if effective bounds fall entirely within an excluded range.
|
|
234
|
+
*
|
|
235
|
+
* For example:
|
|
236
|
+
* Effective: [400, 800)
|
|
237
|
+
* Excluded: [400, 800)
|
|
238
|
+
* → bounds fall entirely within excluded range → impossible
|
|
239
|
+
*/
|
|
240
|
+
function boundsWithinExcludedRange(bounds, excluded) {
|
|
241
|
+
if (bounds.lowerBound === null || bounds.upperBound === null) return false;
|
|
242
|
+
let lowerOk = false;
|
|
243
|
+
if (bounds.lowerBound > excluded.lower) lowerOk = true;
|
|
244
|
+
else if (bounds.lowerBound === excluded.lower) lowerOk = excluded.lowerInclusive || !bounds.lowerInclusive;
|
|
245
|
+
let upperOk = false;
|
|
246
|
+
if (bounds.upperBound < excluded.upper) upperOk = true;
|
|
247
|
+
else if (bounds.upperBound === excluded.upper) upperOk = excluded.upperInclusive || !bounds.upperInclusive;
|
|
248
|
+
return lowerOk && upperOk;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Check for attribute value conflicts
|
|
252
|
+
* e.g., [data-theme="dark"] & [data-theme="light"] → FALSE
|
|
253
|
+
* e.g., [data-theme="dark"] & ![data-theme] → FALSE
|
|
254
|
+
*/
|
|
255
|
+
/**
|
|
256
|
+
* Generic value-conflict checker for grouped conditions.
|
|
257
|
+
*
|
|
258
|
+
* Groups terms by a key, splits into positive/negated, then checks:
|
|
259
|
+
* 1. Multiple distinct positive values → conflict
|
|
260
|
+
* 2. Positive value + negated existence (value === undefined) → conflict
|
|
261
|
+
* 3. Positive value + negated same value → conflict
|
|
262
|
+
*/
|
|
263
|
+
function hasGroupedValueConflict(terms, match, groupKey, getValue) {
|
|
264
|
+
const groups = /* @__PURE__ */ new Map();
|
|
265
|
+
for (const term of terms) {
|
|
266
|
+
const matched = match(term);
|
|
267
|
+
if (!matched) continue;
|
|
268
|
+
const key = groupKey(matched);
|
|
269
|
+
let group = groups.get(key);
|
|
270
|
+
if (!group) {
|
|
271
|
+
group = {
|
|
272
|
+
positive: [],
|
|
273
|
+
negated: []
|
|
274
|
+
};
|
|
275
|
+
groups.set(key, group);
|
|
276
|
+
}
|
|
277
|
+
if (matched.negated) group.negated.push(matched);
|
|
278
|
+
else group.positive.push(matched);
|
|
279
|
+
}
|
|
280
|
+
for (const [, group] of groups) {
|
|
281
|
+
const positiveValues = group.positive.map(getValue).filter((v) => v !== void 0);
|
|
282
|
+
if (new Set(positiveValues).size > 1) return true;
|
|
283
|
+
const hasPositiveValue = positiveValues.length > 0;
|
|
284
|
+
const hasNegatedExistence = group.negated.some((t) => getValue(t) === void 0);
|
|
285
|
+
if (hasPositiveValue && hasNegatedExistence) return true;
|
|
286
|
+
for (const pos of group.positive) {
|
|
287
|
+
const posVal = getValue(pos);
|
|
288
|
+
if (posVal !== void 0) {
|
|
289
|
+
for (const neg of group.negated) if (getValue(neg) === posVal) return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
function hasAttributeConflict(terms) {
|
|
296
|
+
return hasGroupedValueConflict(terms, (t) => t.kind === "state" && t.type === "modifier" ? t : null, (t) => t.attribute, (t) => t.value);
|
|
297
|
+
}
|
|
298
|
+
function hasContainerStyleConflict(terms) {
|
|
299
|
+
return hasGroupedValueConflict(terms, (t) => t.kind === "state" && t.type === "container" && t.subtype === "style" ? t : null, (t) => `${t.containerName || "_"}:${t.property}`, (t) => t.propertyValue);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Remove negations that are implied by positive terms.
|
|
303
|
+
*
|
|
304
|
+
* Key optimizations:
|
|
305
|
+
* 1. style(--variant: danger) implies NOT style(--variant: success)
|
|
306
|
+
* → If we have style(--variant: danger) & not style(--variant: success),
|
|
307
|
+
* the negation is redundant and can be removed.
|
|
308
|
+
*
|
|
309
|
+
* 2. [data-theme="dark"] implies NOT [data-theme="light"]
|
|
310
|
+
* → Same logic for attribute selectors.
|
|
311
|
+
*
|
|
312
|
+
* This produces cleaner CSS:
|
|
313
|
+
* Before: @container style(--variant: danger) and (not style(--variant: success))
|
|
314
|
+
* After: @container style(--variant: danger)
|
|
315
|
+
*/
|
|
316
|
+
/**
|
|
317
|
+
* Collect positive values from terms and build a "is this negation implied?" check.
|
|
318
|
+
*
|
|
319
|
+
* A negation is implied (redundant) when a positive term for the same group
|
|
320
|
+
* already pins a specific value, making "NOT other-value" obvious.
|
|
321
|
+
* e.g. style(--variant: danger) implies NOT style(--variant: success).
|
|
322
|
+
*/
|
|
323
|
+
function buildImpliedNegationCheck(terms) {
|
|
324
|
+
const positiveValues = /* @__PURE__ */ new Map();
|
|
325
|
+
for (const term of terms) {
|
|
326
|
+
if (term.kind !== "state" || term.negated) continue;
|
|
327
|
+
if (term.type === "container" && term.subtype === "style") {
|
|
328
|
+
if (term.propertyValue !== void 0) positiveValues.set(`c:${term.containerName || "_"}:${term.property}`, term.propertyValue);
|
|
329
|
+
} else if (term.type === "modifier" && term.value !== void 0) positiveValues.set(`m:${term.attribute}`, term.value);
|
|
330
|
+
}
|
|
331
|
+
return (term) => {
|
|
332
|
+
if (term.kind !== "state" || !term.negated) return false;
|
|
333
|
+
if (term.type === "container" && term.subtype === "style") {
|
|
334
|
+
if (term.propertyValue === void 0) return false;
|
|
335
|
+
const pos = positiveValues.get(`c:${term.containerName || "_"}:${term.property}`);
|
|
336
|
+
return pos !== void 0 && term.propertyValue !== pos;
|
|
337
|
+
}
|
|
338
|
+
if (term.type === "modifier" && term.value !== void 0) {
|
|
339
|
+
const pos = positiveValues.get(`m:${term.attribute}`);
|
|
340
|
+
return pos !== void 0 && term.value !== pos;
|
|
341
|
+
}
|
|
342
|
+
return false;
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function removeImpliedNegations(terms) {
|
|
346
|
+
const isImplied = buildImpliedNegationCheck(terms);
|
|
347
|
+
return terms.filter((t) => !isImplied(t));
|
|
348
|
+
}
|
|
349
|
+
function deduplicateTerms(terms) {
|
|
350
|
+
const seen = /* @__PURE__ */ new Set();
|
|
351
|
+
const result = [];
|
|
352
|
+
for (const term of terms) {
|
|
353
|
+
const id = getConditionUniqueId(term);
|
|
354
|
+
if (!seen.has(id)) {
|
|
355
|
+
seen.add(id);
|
|
356
|
+
result.push(term);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Merge compatible range conditions
|
|
363
|
+
* e.g., @media(w >= 400px) & @media(w <= 800px) → @media(400px <= w <= 800px)
|
|
364
|
+
*/
|
|
365
|
+
function mergeRanges(terms) {
|
|
366
|
+
const mediaByDim = /* @__PURE__ */ new Map();
|
|
367
|
+
const containerByDim = /* @__PURE__ */ new Map();
|
|
368
|
+
terms.forEach((term, index) => {
|
|
369
|
+
if (term.kind !== "state") return;
|
|
370
|
+
if (term.type === "media" && term.subtype === "dimension" && !term.negated) {
|
|
371
|
+
const key = term.dimension || "width";
|
|
372
|
+
if (!mediaByDim.has(key)) mediaByDim.set(key, {
|
|
373
|
+
conditions: [],
|
|
374
|
+
indices: []
|
|
375
|
+
});
|
|
376
|
+
const group = mediaByDim.get(key);
|
|
377
|
+
group.conditions.push(term);
|
|
378
|
+
group.indices.push(index);
|
|
379
|
+
}
|
|
380
|
+
if (term.type === "container" && term.subtype === "dimension" && !term.negated) {
|
|
381
|
+
const key = `${term.containerName || "_"}:${term.dimension || "width"}`;
|
|
382
|
+
if (!containerByDim.has(key)) containerByDim.set(key, {
|
|
383
|
+
conditions: [],
|
|
384
|
+
indices: []
|
|
385
|
+
});
|
|
386
|
+
const group = containerByDim.get(key);
|
|
387
|
+
group.conditions.push(term);
|
|
388
|
+
group.indices.push(index);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
const indicesToRemove = /* @__PURE__ */ new Set();
|
|
392
|
+
const mergedTerms = [];
|
|
393
|
+
for (const [_dim, group] of mediaByDim) if (group.conditions.length > 1) {
|
|
394
|
+
const merged = mergeMediaRanges(group.conditions);
|
|
395
|
+
if (merged) {
|
|
396
|
+
group.indices.forEach((i) => indicesToRemove.add(i));
|
|
397
|
+
mergedTerms.push(merged);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
for (const [, group] of containerByDim) if (group.conditions.length > 1) {
|
|
401
|
+
const merged = mergeContainerRanges(group.conditions);
|
|
402
|
+
if (merged) {
|
|
403
|
+
group.indices.forEach((i) => indicesToRemove.add(i));
|
|
404
|
+
mergedTerms.push(merged);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const result = [];
|
|
408
|
+
terms.forEach((term, index) => {
|
|
409
|
+
if (!indicesToRemove.has(index)) result.push(term);
|
|
410
|
+
});
|
|
411
|
+
result.push(...mergedTerms);
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Tighten bounds by picking the most restrictive lower and upper bounds
|
|
416
|
+
* from a set of conditions that have lowerBound/upperBound fields.
|
|
417
|
+
*/
|
|
418
|
+
function tightenBounds(conditions) {
|
|
419
|
+
let lowerBound;
|
|
420
|
+
let upperBound;
|
|
421
|
+
for (const cond of conditions) {
|
|
422
|
+
if (cond.lowerBound) {
|
|
423
|
+
if (!lowerBound || (cond.lowerBound.valueNumeric ?? -Infinity) > (lowerBound.valueNumeric ?? -Infinity)) lowerBound = cond.lowerBound;
|
|
424
|
+
}
|
|
425
|
+
if (cond.upperBound) {
|
|
426
|
+
if (!upperBound || (cond.upperBound.valueNumeric ?? Infinity) < (upperBound.valueNumeric ?? Infinity)) upperBound = cond.upperBound;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
lowerBound,
|
|
431
|
+
upperBound
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function appendBoundsToUniqueId(parts, lowerBound, upperBound) {
|
|
435
|
+
if (lowerBound) {
|
|
436
|
+
parts.push(lowerBound.inclusive ? ">=" : ">");
|
|
437
|
+
parts.push(lowerBound.value);
|
|
438
|
+
}
|
|
439
|
+
if (upperBound) {
|
|
440
|
+
parts.push(upperBound.inclusive ? "<=" : "<");
|
|
441
|
+
parts.push(upperBound.value);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function mergeDimensionRanges(conditions, idPrefix) {
|
|
445
|
+
if (conditions.length === 0) return null;
|
|
446
|
+
const { lowerBound, upperBound } = tightenBounds(conditions);
|
|
447
|
+
const base = conditions[0];
|
|
448
|
+
const parts = [...idPrefix];
|
|
449
|
+
appendBoundsToUniqueId(parts, lowerBound, upperBound);
|
|
450
|
+
return {
|
|
451
|
+
...base,
|
|
452
|
+
negated: false,
|
|
453
|
+
raw: buildMergedRaw(base.dimension || "width", lowerBound, upperBound),
|
|
454
|
+
uniqueId: parts.join(":"),
|
|
455
|
+
lowerBound,
|
|
456
|
+
upperBound
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function mergeMediaRanges(conditions) {
|
|
460
|
+
return mergeDimensionRanges(conditions, [
|
|
461
|
+
"media",
|
|
462
|
+
"dim",
|
|
463
|
+
conditions[0]?.dimension ?? "width"
|
|
464
|
+
]);
|
|
465
|
+
}
|
|
466
|
+
function mergeContainerRanges(conditions) {
|
|
467
|
+
const base = conditions[0];
|
|
468
|
+
if (!base) return null;
|
|
469
|
+
return mergeDimensionRanges(conditions, [
|
|
470
|
+
"container",
|
|
471
|
+
"dim",
|
|
472
|
+
base.containerName || "_",
|
|
473
|
+
base.dimension ?? "width"
|
|
474
|
+
]);
|
|
475
|
+
}
|
|
476
|
+
function buildMergedRaw(dimension, lowerBound, upperBound) {
|
|
477
|
+
if (lowerBound && upperBound) {
|
|
478
|
+
const lowerOp = lowerBound.inclusive ? "<=" : "<";
|
|
479
|
+
const upperOp = upperBound.inclusive ? "<=" : "<";
|
|
480
|
+
return `@media(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;
|
|
481
|
+
} else if (upperBound) return `@media(${dimension} ${upperBound.inclusive ? "<=" : "<"} ${upperBound.value})`;
|
|
482
|
+
else if (lowerBound) return `@media(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
|
|
483
|
+
return "@media()";
|
|
484
|
+
}
|
|
485
|
+
function sortTerms(terms) {
|
|
486
|
+
const withIds = terms.map((t) => [getConditionUniqueId(t), t]);
|
|
487
|
+
withIds.sort((a, b) => a[0].localeCompare(b[0]));
|
|
488
|
+
return withIds.map(([, t]) => t);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Apply the absorption law: removes compound terms that are absorbed by
|
|
492
|
+
* a simple term already present.
|
|
493
|
+
*
|
|
494
|
+
* For AND context: A & (A | B) → A (absorbs OR compounds)
|
|
495
|
+
* For OR context: A | (A & B) → A (absorbs AND compounds)
|
|
496
|
+
*/
|
|
497
|
+
function applyAbsorption(terms, absorbedOperator) {
|
|
498
|
+
const simpleIds = /* @__PURE__ */ new Set();
|
|
499
|
+
for (const term of terms) if (term.kind !== "compound") simpleIds.add(getConditionUniqueId(term));
|
|
500
|
+
return terms.filter((term) => {
|
|
501
|
+
if (term.kind === "compound" && term.operator === absorbedOperator) {
|
|
502
|
+
for (const child of term.children) if (simpleIds.has(getConditionUniqueId(child))) return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function applyAbsorptionAnd(terms) {
|
|
508
|
+
return applyAbsorption(terms, "OR");
|
|
509
|
+
}
|
|
510
|
+
function applyAbsorptionOr(terms) {
|
|
511
|
+
return applyAbsorption(terms, "AND");
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
//#endregion
|
|
515
|
+
export { simplifyCondition };
|
|
516
|
+
//# sourceMappingURL=simplify.js.map
|