@tenphi/tasty 1.5.4 → 2.0.0

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.
Files changed (76) hide show
  1. package/README.md +20 -15
  2. package/dist/compute-styles.js +13 -26
  3. package/dist/compute-styles.js.map +1 -1
  4. package/dist/config.d.ts +39 -1
  5. package/dist/config.js +64 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/index.d.ts +2 -2
  8. package/dist/core/index.js +1 -1
  9. package/dist/debug.js +4 -4
  10. package/dist/debug.js.map +1 -1
  11. package/dist/hooks/useCounterStyle.js +2 -1
  12. package/dist/hooks/useCounterStyle.js.map +1 -1
  13. package/dist/hooks/useGlobalStyles.js +2 -2
  14. package/dist/hooks/useKeyframes.js +2 -1
  15. package/dist/hooks/useKeyframes.js.map +1 -1
  16. package/dist/hooks/useRawCSS.js +1 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +1 -1
  19. package/dist/injector/index.js +1 -1
  20. package/dist/injector/index.js.map +1 -1
  21. package/dist/injector/injector.d.ts +5 -0
  22. package/dist/injector/injector.js +93 -6
  23. package/dist/injector/injector.js.map +1 -1
  24. package/dist/injector/sheet-manager.js +3 -2
  25. package/dist/injector/sheet-manager.js.map +1 -1
  26. package/dist/injector/types.d.ts +9 -2
  27. package/dist/pipeline/exclusive.js +57 -2
  28. package/dist/pipeline/exclusive.js.map +1 -1
  29. package/dist/pipeline/index.js +2 -2
  30. package/dist/pipeline/index.js.map +1 -1
  31. package/dist/pipeline/materialize.js +56 -2
  32. package/dist/pipeline/materialize.js.map +1 -1
  33. package/dist/pipeline/simplify.js +180 -5
  34. package/dist/pipeline/simplify.js.map +1 -1
  35. package/dist/plugins/types.d.ts +12 -1
  36. package/dist/rsc-cache.js +2 -4
  37. package/dist/rsc-cache.js.map +1 -1
  38. package/dist/ssr/astro-client.js +5 -10
  39. package/dist/ssr/astro-client.js.map +1 -1
  40. package/dist/ssr/astro.d.ts +4 -2
  41. package/dist/ssr/astro.js +2 -2
  42. package/dist/ssr/astro.js.map +1 -1
  43. package/dist/ssr/collector.d.ts +9 -13
  44. package/dist/ssr/collector.js +32 -16
  45. package/dist/ssr/collector.js.map +1 -1
  46. package/dist/ssr/hydrate.d.ts +20 -13
  47. package/dist/ssr/hydrate.js +24 -28
  48. package/dist/ssr/hydrate.js.map +1 -1
  49. package/dist/ssr/index.d.ts +3 -3
  50. package/dist/ssr/index.js +2 -2
  51. package/dist/ssr/index.js.map +1 -1
  52. package/dist/ssr/next.d.ts +4 -3
  53. package/dist/ssr/next.js +5 -5
  54. package/dist/ssr/next.js.map +1 -1
  55. package/dist/tasty.d.ts +1 -1
  56. package/dist/tasty.js +9 -4
  57. package/dist/tasty.js.map +1 -1
  58. package/dist/utils/typography.d.ts +21 -10
  59. package/dist/utils/typography.js +1 -1
  60. package/dist/utils/typography.js.map +1 -1
  61. package/dist/zero/babel.d.ts +7 -108
  62. package/dist/zero/babel.js +36 -12
  63. package/dist/zero/babel.js.map +1 -1
  64. package/docs/README.md +2 -2
  65. package/docs/adoption.md +5 -3
  66. package/docs/comparison.md +24 -25
  67. package/docs/configuration.md +69 -1
  68. package/docs/design-system.md +22 -10
  69. package/docs/dsl.md +3 -3
  70. package/docs/getting-started.md +10 -10
  71. package/docs/injector.md +2 -2
  72. package/docs/methodology.md +2 -2
  73. package/docs/{runtime.md → react-api.md} +5 -1
  74. package/docs/ssr.md +14 -7
  75. package/docs/tasty-static.md +14 -2
  76. package/package.json +5 -5
@@ -1,5 +1,5 @@
1
1
  import { Lru } from "../parser/lru.js";
2
- import { falseCondition, getConditionUniqueId, trueCondition } from "./conditions.js";
2
+ import { and, falseCondition, getConditionUniqueId, not, or, trueCondition } from "./conditions.js";
3
3
  //#region src/pipeline/simplify.ts
4
4
  /**
5
5
  * Condition Simplification Engine
@@ -61,6 +61,7 @@ function simplifyAnd(children) {
61
61
  terms = mergeRanges(terms);
62
62
  terms = sortTerms(terms);
63
63
  terms = applyAbsorptionAnd(terms);
64
+ terms = applyConsensusAnd(terms);
64
65
  if (terms.length === 0) return trueCondition();
65
66
  if (terms.length === 1) return terms[0];
66
67
  return {
@@ -83,6 +84,7 @@ function simplifyOr(children) {
83
84
  terms = deduplicateTerms(terms);
84
85
  terms = sortTerms(terms);
85
86
  terms = applyAbsorptionOr(terms);
87
+ terms = applyComplementaryFactoring(terms);
86
88
  if (terms.length === 0) return falseCondition();
87
89
  if (terms.length === 1) return terms[0];
88
90
  return {
@@ -481,6 +483,83 @@ function buildMergedRaw(dimension, lowerBound, upperBound) {
481
483
  else if (lowerBound) return `@media(${dimension} ${lowerBound.inclusive ? ">=" : ">"} ${lowerBound.value})`;
482
484
  return "@media()";
483
485
  }
486
+ /**
487
+ * Apply complementary factoring: (A & B) | (A & !B) → A
488
+ *
489
+ * Finds pairs of AND compounds that share all children except one,
490
+ * where the differing child is complementary (X vs !X).
491
+ * Replaces the pair with the common terms.
492
+ *
493
+ * This is applied iteratively until no more reductions are possible,
494
+ * since factoring can expose further simplification opportunities.
495
+ */
496
+ function applyComplementaryFactoring(terms) {
497
+ let changed = true;
498
+ while (changed) {
499
+ changed = false;
500
+ for (let i = 0; i < terms.length; i++) {
501
+ const a = terms[i];
502
+ if (a.kind !== "compound" || a.operator !== "AND") continue;
503
+ for (let j = i + 1; j < terms.length; j++) {
504
+ const b = terms[j];
505
+ if (b.kind !== "compound" || b.operator !== "AND") continue;
506
+ const factored = tryFactorPair(a.children, b.children);
507
+ if (factored) {
508
+ const replacement = simplifyInner(factored);
509
+ terms = [
510
+ ...terms.slice(0, i),
511
+ ...terms.slice(i + 1, j),
512
+ ...terms.slice(j + 1),
513
+ replacement
514
+ ];
515
+ changed = true;
516
+ break;
517
+ }
518
+ }
519
+ if (changed) break;
520
+ }
521
+ }
522
+ return terms;
523
+ }
524
+ /**
525
+ * Try to factor two AND children lists.
526
+ *
527
+ * Extracts the common children (by uniqueId). If the remaining
528
+ * (non-common) parts of each side OR to TRUE, the common part alone
529
+ * is sufficient: `(common & restA) | (common & restB) → common`
530
+ * when `restA | restB → TRUE`.
531
+ *
532
+ * Also handles the simpler case where exactly one child is
533
+ * complementary: `[A, B, C]` and `[A, !B, C]` → `AND(A, C)`.
534
+ */
535
+ function tryFactorPair(aChildren, bChildren) {
536
+ const aIds = aChildren.map((c) => getConditionUniqueId(c));
537
+ const bIds = bChildren.map((c) => getConditionUniqueId(c));
538
+ const bIdSet = new Set(bIds);
539
+ const aIdSet = new Set(aIds);
540
+ const commonIndicesA = [];
541
+ const restIndicesA = [];
542
+ for (let i = 0; i < aIds.length; i++) if (bIdSet.has(aIds[i])) commonIndicesA.push(i);
543
+ else restIndicesA.push(i);
544
+ const restIndicesB = [];
545
+ for (let i = 0; i < bIds.length; i++) if (!aIdSet.has(bIds[i])) restIndicesB.push(i);
546
+ if (commonIndicesA.length === 0) return null;
547
+ if (restIndicesA.length === 0 && restIndicesB.length === 0) return null;
548
+ const restA = restIndicesA.length === 0 ? trueCondition() : restIndicesA.length === 1 ? aChildren[restIndicesA[0]] : and(...restIndicesA.map((i) => aChildren[i]));
549
+ const restB = restIndicesB.length === 0 ? trueCondition() : restIndicesB.length === 1 ? bChildren[restIndicesB[0]] : and(...restIndicesB.map((i) => bChildren[i]));
550
+ if (simplifyInner({
551
+ kind: "compound",
552
+ operator: "OR",
553
+ children: [restA, restB]
554
+ }).kind !== "true") {
555
+ const simplifiedRestB = simplifyInner(restB);
556
+ if (getConditionUniqueId(simplifyInner(not(restA))) !== getConditionUniqueId(simplifiedRestB)) return null;
557
+ }
558
+ const common = commonIndicesA.map((i) => aChildren[i]);
559
+ if (common.length === 0) return trueCondition();
560
+ if (common.length === 1) return common[0];
561
+ return and(...common);
562
+ }
484
563
  function sortTerms(terms) {
485
564
  const withIds = terms.map((t) => [getConditionUniqueId(t), t]);
486
565
  withIds.sort((a, b) => a[0].localeCompare(b[0]));
@@ -488,17 +567,38 @@ function sortTerms(terms) {
488
567
  }
489
568
  /**
490
569
  * Apply the absorption law: removes compound terms that are absorbed by
491
- * a simple term already present.
570
+ * another term already present (simple or compound).
492
571
  *
493
572
  * For AND context: A & (A | B) → A (absorbs OR compounds)
494
573
  * For OR context: A | (A & B) → A (absorbs AND compounds)
574
+ *
575
+ * After flattening, a compound A = OR(X, Y) becomes [X, Y, ...] in the
576
+ * outer OR. A child AND(A, B) = AND(OR(X, Y), B) still references the
577
+ * original un-flattened compound. We reconstruct possible compound
578
+ * absorbers from the flattened terms so absorption works across nesting.
495
579
  */
496
580
  function applyAbsorption(terms, absorbedOperator) {
497
- const simpleIds = /* @__PURE__ */ new Set();
498
- for (const term of terms) if (term.kind !== "compound") simpleIds.add(getConditionUniqueId(term));
581
+ const absorberIds = /* @__PURE__ */ new Set();
582
+ for (const term of terms) absorberIds.add(getConditionUniqueId(term));
583
+ let changed = true;
584
+ while (changed) {
585
+ changed = false;
586
+ for (const term of terms) {
587
+ if (term.kind !== "compound" || term.operator !== absorbedOperator) continue;
588
+ for (const child of term.children) {
589
+ if (child.kind !== "compound") continue;
590
+ const childId = getConditionUniqueId(child);
591
+ if (absorberIds.has(childId)) continue;
592
+ if (child.children.every((c) => absorberIds.has(getConditionUniqueId(c)))) {
593
+ absorberIds.add(childId);
594
+ changed = true;
595
+ }
596
+ }
597
+ }
598
+ }
499
599
  return terms.filter((term) => {
500
600
  if (term.kind === "compound" && term.operator === absorbedOperator) {
501
- for (const child of term.children) if (simpleIds.has(getConditionUniqueId(child))) return false;
601
+ for (const child of term.children) if (absorberIds.has(getConditionUniqueId(child))) return false;
502
602
  }
503
603
  return true;
504
604
  });
@@ -509,6 +609,81 @@ function applyAbsorptionAnd(terms) {
509
609
  function applyAbsorptionOr(terms) {
510
610
  return applyAbsorption(terms, "AND");
511
611
  }
612
+ /**
613
+ * Apply the consensus/resolution rule for AND:
614
+ * (A | B) & (A | !B) → A
615
+ *
616
+ * This is the dual of complementary factoring in OR context:
617
+ * (A & B) | (A & !B) → A
618
+ *
619
+ * Extracts common children from two OR terms. If the remaining
620
+ * parts of each side AND to FALSE, the common part alone is
621
+ * sufficient.
622
+ */
623
+ function applyConsensusAnd(terms) {
624
+ let changed = true;
625
+ while (changed) {
626
+ changed = false;
627
+ for (let i = 0; i < terms.length; i++) {
628
+ const a = terms[i];
629
+ if (a.kind !== "compound" || a.operator !== "OR") continue;
630
+ for (let j = i + 1; j < terms.length; j++) {
631
+ const b = terms[j];
632
+ if (b.kind !== "compound" || b.operator !== "OR") continue;
633
+ const resolved = tryResolvePair(a.children, b.children);
634
+ if (resolved) {
635
+ const replacement = simplifyInner(resolved);
636
+ terms = [
637
+ ...terms.slice(0, i),
638
+ ...terms.slice(i + 1, j),
639
+ ...terms.slice(j + 1),
640
+ replacement
641
+ ];
642
+ changed = true;
643
+ break;
644
+ }
645
+ }
646
+ if (changed) break;
647
+ }
648
+ }
649
+ return terms;
650
+ }
651
+ /**
652
+ * Try to resolve two OR children lists.
653
+ *
654
+ * Extracts common children (by uniqueId). If the remaining
655
+ * (non-common) parts AND to FALSE, the common part alone
656
+ * is sufficient: `(common | restA) & (common | restB) → common`
657
+ * when `restA & restB → FALSE`.
658
+ */
659
+ function tryResolvePair(aChildren, bChildren) {
660
+ const aIds = aChildren.map((c) => getConditionUniqueId(c));
661
+ const bIds = bChildren.map((c) => getConditionUniqueId(c));
662
+ const bIdSet = new Set(bIds);
663
+ const aIdSet = new Set(aIds);
664
+ const commonIndicesA = [];
665
+ const restIndicesA = [];
666
+ for (let i = 0; i < aIds.length; i++) if (bIdSet.has(aIds[i])) commonIndicesA.push(i);
667
+ else restIndicesA.push(i);
668
+ const restIndicesB = [];
669
+ for (let i = 0; i < bIds.length; i++) if (!aIdSet.has(bIds[i])) restIndicesB.push(i);
670
+ if (commonIndicesA.length === 0) return null;
671
+ if (restIndicesA.length === 0 && restIndicesB.length === 0) return null;
672
+ const restA = restIndicesA.length === 0 ? falseCondition() : restIndicesA.length === 1 ? aChildren[restIndicesA[0]] : or(...restIndicesA.map((i) => aChildren[i]));
673
+ const restB = restIndicesB.length === 0 ? falseCondition() : restIndicesB.length === 1 ? bChildren[restIndicesB[0]] : or(...restIndicesB.map((i) => bChildren[i]));
674
+ if (simplifyInner({
675
+ kind: "compound",
676
+ operator: "AND",
677
+ children: [restA, restB]
678
+ }).kind !== "false") {
679
+ const simplifiedRestB = simplifyInner(restB);
680
+ if (getConditionUniqueId(simplifyInner(not(restA))) !== getConditionUniqueId(simplifiedRestB)) return null;
681
+ }
682
+ const common = commonIndicesA.map((i) => aChildren[i]);
683
+ if (common.length === 0) return falseCondition();
684
+ if (common.length === 1) return common[0];
685
+ return or(...common);
686
+ }
512
687
  //#endregion
513
688
  export { simplifyCondition };
514
689
 
@@ -1 +1 @@
1
- {"version":3,"file":"simplify.js","names":[],"sources":["../../src/pipeline/simplify.ts"],"sourcesContent":["/**\n * Condition Simplification Engine\n *\n * Simplifies condition trees by applying boolean algebra rules,\n * detecting contradictions, merging ranges, and deduplicating terms.\n *\n * This is critical for:\n * 1. Detecting invalid combinations (A & !A → FALSE)\n * 2. Reducing CSS output size\n * 3. Producing cleaner selectors\n */\n\nimport { Lru } from '../parser/lru';\n\nimport type {\n ConditionNode,\n ContainerCondition,\n MediaCondition,\n ModifierCondition,\n NumericBound,\n} from './conditions';\nimport {\n falseCondition,\n getConditionUniqueId,\n trueCondition,\n} from './conditions';\n\n// ============================================================================\n// Caching\n// ============================================================================\n\nconst simplifyCache = new Lru<string, ConditionNode>(5000);\n\n// ============================================================================\n// Main Simplify Function\n// ============================================================================\n\n/**\n * Simplify a condition tree aggressively.\n *\n * This applies all possible simplification rules:\n * - Boolean algebra (identity, annihilator, idempotent, absorption)\n * - Contradiction detection (A & !A → FALSE)\n * - Tautology detection (A | !A → TRUE)\n * - Range intersection for numeric queries\n * - Attribute value conflict detection\n * - Deduplication and sorting\n */\nexport function simplifyCondition(node: ConditionNode): ConditionNode {\n // Check cache\n const key = getConditionUniqueId(node);\n const cached = simplifyCache.get(key);\n if (cached) {\n return cached;\n }\n\n const result = simplifyInner(node);\n\n // Cache result\n simplifyCache.set(key, result);\n\n return result;\n}\n\n/**\n * Clear the simplify cache (for testing)\n */\nexport function clearSimplifyCache(): void {\n simplifyCache.clear();\n}\n\n// ============================================================================\n// Inner Simplification\n// ============================================================================\n\nfunction simplifyInner(node: ConditionNode): ConditionNode {\n // Base cases\n if (node.kind === 'true' || node.kind === 'false') {\n return node;\n }\n\n // State conditions - return as-is (they're already leaf nodes)\n if (node.kind === 'state') {\n return node;\n }\n\n // Compound conditions - recursively simplify\n if (node.kind === 'compound') {\n // First, recursively simplify all children\n const simplifiedChildren = node.children.map((c) => simplifyInner(c));\n\n // Then apply compound-specific simplifications\n if (node.operator === 'AND') {\n return simplifyAnd(simplifiedChildren);\n } else {\n return simplifyOr(simplifiedChildren);\n }\n }\n\n return node;\n}\n\n// ============================================================================\n// AND Simplification\n// ============================================================================\n\nfunction simplifyAnd(children: ConditionNode[]): ConditionNode {\n let terms: ConditionNode[] = [];\n\n // Flatten nested ANDs and handle TRUE/FALSE\n for (const child of children) {\n if (child.kind === 'false') {\n // AND with FALSE → FALSE\n return falseCondition();\n }\n if (child.kind === 'true') {\n // AND with TRUE → skip (identity)\n continue;\n }\n if (child.kind === 'compound' && child.operator === 'AND') {\n // Flatten nested AND\n terms.push(...child.children);\n } else {\n terms.push(child);\n }\n }\n\n // Empty → TRUE\n if (terms.length === 0) {\n return trueCondition();\n }\n\n // Single term → return it\n if (terms.length === 1) {\n return terms[0];\n }\n\n // Check for contradictions\n if (hasContradiction(terms)) {\n return falseCondition();\n }\n\n // Check for range contradictions in media/container queries\n if (hasRangeContradiction(terms)) {\n return falseCondition();\n }\n\n // Check for attribute value conflicts\n if (hasAttributeConflict(terms)) {\n return falseCondition();\n }\n\n // Check for container style query conflicts\n if (hasContainerStyleConflict(terms)) {\n return falseCondition();\n }\n\n // Remove redundant negations implied by positive terms\n // e.g., style(--variant: danger) implies NOT style(--variant: success)\n // and style(--variant: danger) implies style(--variant) (existence)\n terms = removeImpliedNegations(terms);\n\n // Deduplicate (by uniqueId)\n terms = deduplicateTerms(terms);\n\n // Try to merge numeric ranges\n terms = mergeRanges(terms);\n\n // Sort for canonical form\n terms = sortTerms(terms);\n\n // Apply absorption: A & (A | B) → A\n terms = applyAbsorptionAnd(terms);\n\n if (terms.length === 0) {\n return trueCondition();\n }\n if (terms.length === 1) {\n return terms[0];\n }\n\n return {\n kind: 'compound',\n operator: 'AND',\n children: terms,\n };\n}\n\n// ============================================================================\n// OR Simplification\n// ============================================================================\n\nfunction simplifyOr(children: ConditionNode[]): ConditionNode {\n let terms: ConditionNode[] = [];\n\n // Flatten nested ORs and handle TRUE/FALSE\n for (const child of children) {\n if (child.kind === 'true') {\n // OR with TRUE → TRUE\n return trueCondition();\n }\n if (child.kind === 'false') {\n // OR with FALSE → skip (identity)\n continue;\n }\n if (child.kind === 'compound' && child.operator === 'OR') {\n // Flatten nested OR\n terms.push(...child.children);\n } else {\n terms.push(child);\n }\n }\n\n // Empty → FALSE\n if (terms.length === 0) {\n return falseCondition();\n }\n\n // Single term → return it\n if (terms.length === 1) {\n return terms[0];\n }\n\n // Check for tautologies (A | !A)\n if (hasTautology(terms)) {\n return trueCondition();\n }\n\n // Deduplicate\n terms = deduplicateTerms(terms);\n\n // Sort for canonical form\n terms = sortTerms(terms);\n\n // Apply absorption: A | (A & B) → A\n terms = applyAbsorptionOr(terms);\n\n if (terms.length === 0) {\n return falseCondition();\n }\n if (terms.length === 1) {\n return terms[0];\n }\n\n return {\n kind: 'compound',\n operator: 'OR',\n children: terms,\n };\n}\n\n// ============================================================================\n// Contradiction Detection\n// ============================================================================\n\n/**\n * Check if any pair of terms has complementary negation (A and !A).\n * Used for both contradiction detection (in AND) and tautology detection (in OR),\n * since the underlying check is identical: the context determines the semantics.\n */\nfunction hasComplementaryPair(terms: ConditionNode[]): boolean {\n const uniqueIds = new Set<string>();\n\n for (const term of terms) {\n if (term.kind !== 'state') continue;\n\n const id = term.uniqueId;\n const negatedId = term.negated ? id.slice(1) : `!${id}`;\n\n if (uniqueIds.has(negatedId)) {\n return true;\n }\n uniqueIds.add(id);\n }\n\n return false;\n}\n\nconst hasContradiction = hasComplementaryPair;\nconst hasTautology = hasComplementaryPair;\n\n// ============================================================================\n// Range Contradiction Detection\n// ============================================================================\n\n/**\n * Effective bounds computed from conditions (including negated single-bound conditions)\n */\ninterface EffectiveBounds {\n lowerBound: number | null;\n lowerInclusive: boolean;\n upperBound: number | null;\n upperInclusive: boolean;\n}\n\n/**\n * Excluded range from a negated range condition\n */\ninterface ExcludedRange {\n lower: number;\n lowerInclusive: boolean;\n upper: number;\n upperInclusive: boolean;\n}\n\n/**\n * Check for range contradictions in media/container queries\n * e.g., @media(w < 400px) & @media(w > 800px) → FALSE\n *\n * Also handles negated conditions:\n * - Single-bound negations are inverted (not (w < 600px) → w >= 600px)\n * - Range negations create excluded ranges that are checked against positive bounds\n */\nfunction hasRangeContradiction(terms: ConditionNode[]): boolean {\n // Group by dimension, separating positive and negated conditions\n const mediaByDim = new Map<\n string,\n { positive: MediaCondition[]; negated: MediaCondition[] }\n >();\n const containerByDim = new Map<\n string,\n { positive: ContainerCondition[]; negated: ContainerCondition[] }\n >();\n\n for (const term of terms) {\n if (term.kind !== 'state') continue;\n\n if (term.type === 'media' && term.subtype === 'dimension') {\n const key = term.dimension || 'width';\n if (!mediaByDim.has(key)) {\n mediaByDim.set(key, { positive: [], negated: [] });\n }\n const group = mediaByDim.get(key)!;\n if (term.negated) {\n group.negated.push(term);\n } else {\n group.positive.push(term);\n }\n }\n\n if (term.type === 'container' && term.subtype === 'dimension') {\n const key = `${term.containerName || '_'}:${term.dimension || 'width'}`;\n if (!containerByDim.has(key)) {\n containerByDim.set(key, { positive: [], negated: [] });\n }\n const group = containerByDim.get(key)!;\n if (term.negated) {\n group.negated.push(term);\n } else {\n group.positive.push(term);\n }\n }\n }\n\n // Check each dimension group for impossible ranges\n for (const group of mediaByDim.values()) {\n if (rangesAreImpossibleWithNegations(group.positive, group.negated)) {\n return true;\n }\n }\n\n for (const group of containerByDim.values()) {\n if (rangesAreImpossibleWithNegations(group.positive, group.negated)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if conditions are impossible, including negated conditions.\n *\n * For negated single-bound conditions:\n * not (w < 600px) → w >= 600px (inverted to lower bound)\n * not (w >= 800px) → w < 800px (inverted to upper bound)\n *\n * For negated range conditions:\n * not (400px <= w < 800px) → excludes [400, 800)\n * If the effective bounds fall entirely within an excluded range, it's impossible.\n */\nfunction rangesAreImpossibleWithNegations(\n positive: (MediaCondition | ContainerCondition)[],\n negated: (MediaCondition | ContainerCondition)[],\n): boolean {\n // Start with bounds from positive conditions\n const bounds = computeEffectiveBounds(positive);\n\n // Apply inverted bounds from single-bound negated conditions\n // and collect excluded ranges from range negated conditions\n const excludedRanges: ExcludedRange[] = [];\n\n for (const cond of negated) {\n const hasLower = cond.lowerBound?.valueNumeric != null;\n const hasUpper = cond.upperBound?.valueNumeric != null;\n\n if (hasLower && hasUpper) {\n // Range negation: not (lower <= w < upper) excludes [lower, upper)\n excludedRanges.push({\n lower: cond.lowerBound!.valueNumeric!,\n lowerInclusive: cond.lowerBound!.inclusive,\n upper: cond.upperBound!.valueNumeric!,\n upperInclusive: cond.upperBound!.inclusive,\n });\n } else if (hasUpper) {\n // not (w < upper) → w >= upper (becomes lower bound)\n // not (w <= upper) → w > upper (becomes lower bound, exclusive)\n const value = cond.upperBound!.valueNumeric!;\n const inclusive = !cond.upperBound!.inclusive; // flip inclusivity\n\n if (bounds.lowerBound === null || value > bounds.lowerBound) {\n bounds.lowerBound = value;\n bounds.lowerInclusive = inclusive;\n } else if (value === bounds.lowerBound && !inclusive) {\n bounds.lowerInclusive = false;\n }\n } else if (hasLower) {\n // not (w >= lower) → w < lower (becomes upper bound)\n // not (w > lower) → w <= lower (becomes upper bound, inclusive)\n const value = cond.lowerBound!.valueNumeric!;\n const inclusive = !cond.lowerBound!.inclusive; // flip inclusivity\n\n if (bounds.upperBound === null || value < bounds.upperBound) {\n bounds.upperBound = value;\n bounds.upperInclusive = inclusive;\n } else if (value === bounds.upperBound && !inclusive) {\n bounds.upperInclusive = false;\n }\n }\n }\n\n // Check if effective bounds are impossible on their own\n if (bounds.lowerBound !== null && bounds.upperBound !== null) {\n if (bounds.lowerBound > bounds.upperBound) {\n return true;\n }\n if (\n bounds.lowerBound === bounds.upperBound &&\n (!bounds.lowerInclusive || !bounds.upperInclusive)\n ) {\n return true;\n }\n }\n\n // Check if effective bounds fall entirely within any excluded range\n if (\n bounds.lowerBound !== null &&\n bounds.upperBound !== null &&\n excludedRanges.length > 0\n ) {\n for (const excluded of excludedRanges) {\n if (boundsWithinExcludedRange(bounds, excluded)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Compute effective bounds from positive (non-negated) conditions\n */\nfunction computeEffectiveBounds(\n conditions: (MediaCondition | ContainerCondition)[],\n): EffectiveBounds {\n let lowerBound: number | null = null;\n let lowerInclusive = false;\n let upperBound: number | null = null;\n let upperInclusive = false;\n\n for (const cond of conditions) {\n if (cond.lowerBound?.valueNumeric != null) {\n const value = cond.lowerBound.valueNumeric;\n const inclusive = cond.lowerBound.inclusive;\n\n if (lowerBound === null || value > lowerBound) {\n lowerBound = value;\n lowerInclusive = inclusive;\n } else if (value === lowerBound && !inclusive) {\n lowerInclusive = false;\n }\n }\n\n if (cond.upperBound?.valueNumeric != null) {\n const value = cond.upperBound.valueNumeric;\n const inclusive = cond.upperBound.inclusive;\n\n if (upperBound === null || value < upperBound) {\n upperBound = value;\n upperInclusive = inclusive;\n } else if (value === upperBound && !inclusive) {\n upperInclusive = false;\n }\n }\n }\n\n return { lowerBound, lowerInclusive, upperBound, upperInclusive };\n}\n\n/**\n * Check if effective bounds fall entirely within an excluded range.\n *\n * For example:\n * Effective: [400, 800)\n * Excluded: [400, 800)\n * → bounds fall entirely within excluded range → impossible\n */\nfunction boundsWithinExcludedRange(\n bounds: EffectiveBounds,\n excluded: ExcludedRange,\n): boolean {\n if (bounds.lowerBound === null || bounds.upperBound === null) {\n return false;\n }\n\n // Check if bounds.lower >= excluded.lower\n let lowerOk = false;\n if (bounds.lowerBound > excluded.lower) {\n lowerOk = true;\n } else if (bounds.lowerBound === excluded.lower) {\n // If excluded includes lower, and bounds includes or excludes lower, it's within\n // If excluded excludes lower, bounds must also exclude it to be within\n lowerOk = excluded.lowerInclusive || !bounds.lowerInclusive;\n }\n\n // Check if bounds.upper <= excluded.upper\n let upperOk = false;\n if (bounds.upperBound < excluded.upper) {\n upperOk = true;\n } else if (bounds.upperBound === excluded.upper) {\n // If excluded includes upper, and bounds includes or excludes upper, it's within\n // If excluded excludes upper, bounds must also exclude it to be within\n upperOk = excluded.upperInclusive || !bounds.upperInclusive;\n }\n\n return lowerOk && upperOk;\n}\n\n// ============================================================================\n// Attribute Conflict Detection\n// ============================================================================\n\n/**\n * Check for attribute value conflicts\n * e.g., [data-theme=\"dark\"] & [data-theme=\"light\"] → FALSE\n * e.g., [data-theme=\"dark\"] & ![data-theme] → FALSE\n */\n/**\n * Generic value-conflict checker for grouped conditions.\n *\n * Groups terms by a key, splits into positive/negated, then checks:\n * 1. Multiple distinct positive values → conflict\n * 2. Positive value + negated existence (value === undefined) → conflict\n * 3. Positive value + negated same value → conflict\n */\nfunction hasGroupedValueConflict<T extends { negated: boolean }>(\n terms: ConditionNode[],\n match: (term: ConditionNode) => T | null,\n groupKey: (term: T) => string,\n getValue: (term: T) => string | undefined,\n): boolean {\n const groups = new Map<string, { positive: T[]; negated: T[] }>();\n\n for (const term of terms) {\n const matched = match(term);\n if (!matched) continue;\n\n const key = groupKey(matched);\n let group = groups.get(key);\n if (!group) {\n group = { positive: [], negated: [] };\n groups.set(key, group);\n }\n\n if (matched.negated) {\n group.negated.push(matched);\n } else {\n group.positive.push(matched);\n }\n }\n\n for (const [, group] of groups) {\n const positiveValues = group.positive\n .map(getValue)\n .filter((v) => v !== undefined);\n if (new Set(positiveValues).size > 1) return true;\n\n const hasPositiveValue = positiveValues.length > 0;\n const hasNegatedExistence = group.negated.some(\n (t) => getValue(t) === undefined,\n );\n if (hasPositiveValue && hasNegatedExistence) return true;\n\n for (const pos of group.positive) {\n const posVal = getValue(pos);\n if (posVal !== undefined) {\n for (const neg of group.negated) {\n if (getValue(neg) === posVal) return true;\n }\n }\n }\n }\n\n return false;\n}\n\nfunction hasAttributeConflict(terms: ConditionNode[]): boolean {\n return hasGroupedValueConflict<ModifierCondition>(\n terms,\n (t) => (t.kind === 'state' && t.type === 'modifier' ? t : null),\n (t) => t.attribute,\n (t) => t.value,\n );\n}\n\nfunction hasContainerStyleConflict(terms: ConditionNode[]): boolean {\n return hasGroupedValueConflict<ContainerCondition>(\n terms,\n (t) =>\n t.kind === 'state' && t.type === 'container' && t.subtype === 'style'\n ? t\n : null,\n (t) => `${t.containerName || '_'}:${t.property}`,\n (t) => t.propertyValue,\n );\n}\n\n// ============================================================================\n// Implied Negation Removal\n// ============================================================================\n\n/**\n * Remove negations that are implied by positive terms.\n *\n * Key optimizations:\n * 1. style(--variant: danger) implies NOT style(--variant: success)\n * → If we have style(--variant: danger) & not style(--variant: success),\n * the negation is redundant and can be removed.\n *\n * 2. [data-theme=\"dark\"] implies NOT [data-theme=\"light\"]\n * → Same logic for attribute selectors.\n *\n * This produces cleaner CSS:\n * Before: @container style(--variant: danger) and (not style(--variant: success))\n * After: @container style(--variant: danger)\n */\n/**\n * Collect positive values from terms and build a \"is this negation implied?\" check.\n *\n * A negation is implied (redundant) when a positive term for the same group\n * already pins a specific value, making \"NOT other-value\" obvious.\n * e.g. style(--variant: danger) implies NOT style(--variant: success).\n */\nfunction buildImpliedNegationCheck(\n terms: ConditionNode[],\n): (term: ConditionNode) => boolean {\n const positiveValues = new Map<string, string>();\n\n for (const term of terms) {\n if (term.kind !== 'state' || term.negated) continue;\n\n if (term.type === 'container' && term.subtype === 'style') {\n if (term.propertyValue !== undefined) {\n positiveValues.set(\n `c:${term.containerName || '_'}:${term.property}`,\n term.propertyValue,\n );\n }\n } else if (term.type === 'modifier' && term.value !== undefined) {\n positiveValues.set(`m:${term.attribute}`, term.value);\n }\n }\n\n return (term: ConditionNode): boolean => {\n if (term.kind !== 'state' || !term.negated) return false;\n\n if (term.type === 'container' && term.subtype === 'style') {\n if (term.propertyValue === undefined) return false;\n const pos = positiveValues.get(\n `c:${term.containerName || '_'}:${term.property}`,\n );\n return pos !== undefined && term.propertyValue !== pos;\n }\n\n if (term.type === 'modifier' && term.value !== undefined) {\n const pos = positiveValues.get(`m:${term.attribute}`);\n return pos !== undefined && term.value !== pos;\n }\n\n return false;\n };\n}\n\nfunction removeImpliedNegations(terms: ConditionNode[]): ConditionNode[] {\n const isImplied = buildImpliedNegationCheck(terms);\n return terms.filter((t) => !isImplied(t));\n}\n\n// ============================================================================\n// Deduplication\n// ============================================================================\n\nfunction deduplicateTerms(terms: ConditionNode[]): ConditionNode[] {\n const seen = new Set<string>();\n const result: ConditionNode[] = [];\n\n for (const term of terms) {\n const id = getConditionUniqueId(term);\n if (!seen.has(id)) {\n seen.add(id);\n result.push(term);\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Range Merging\n// ============================================================================\n\n/**\n * Merge compatible range conditions\n * e.g., @media(w >= 400px) & @media(w <= 800px) → @media(400px <= w <= 800px)\n */\nfunction mergeRanges(terms: ConditionNode[]): ConditionNode[] {\n // Group media conditions by dimension\n const mediaByDim = new Map<\n string,\n { conditions: MediaCondition[]; indices: number[] }\n >();\n const containerByDim = new Map<\n string,\n { conditions: ContainerCondition[]; indices: number[] }\n >();\n\n terms.forEach((term, index) => {\n if (term.kind !== 'state') return;\n\n if (\n term.type === 'media' &&\n term.subtype === 'dimension' &&\n !term.negated\n ) {\n const key = term.dimension || 'width';\n if (!mediaByDim.has(key)) {\n mediaByDim.set(key, { conditions: [], indices: [] });\n }\n const group = mediaByDim.get(key)!;\n group.conditions.push(term);\n group.indices.push(index);\n }\n\n if (\n term.type === 'container' &&\n term.subtype === 'dimension' &&\n !term.negated\n ) {\n const key = `${term.containerName || '_'}:${term.dimension || 'width'}`;\n if (!containerByDim.has(key)) {\n containerByDim.set(key, { conditions: [], indices: [] });\n }\n const group = containerByDim.get(key)!;\n group.conditions.push(term);\n group.indices.push(index);\n }\n });\n\n // Track indices to remove\n const indicesToRemove = new Set<number>();\n const mergedTerms: ConditionNode[] = [];\n\n // Merge media conditions\n for (const [_dim, group] of mediaByDim) {\n if (group.conditions.length > 1) {\n const merged = mergeMediaRanges(group.conditions);\n if (merged) {\n group.indices.forEach((i) => indicesToRemove.add(i));\n mergedTerms.push(merged);\n }\n }\n }\n\n // Merge container conditions\n for (const [, group] of containerByDim) {\n if (group.conditions.length > 1) {\n const merged = mergeContainerRanges(group.conditions);\n if (merged) {\n group.indices.forEach((i) => indicesToRemove.add(i));\n mergedTerms.push(merged);\n }\n }\n }\n\n // Build result\n const result: ConditionNode[] = [];\n terms.forEach((term, index) => {\n if (!indicesToRemove.has(index)) {\n result.push(term);\n }\n });\n result.push(...mergedTerms);\n\n return result;\n}\n\n/**\n * Tighten bounds by picking the most restrictive lower and upper bounds\n * from a set of conditions that have lowerBound/upperBound fields.\n */\nfunction tightenBounds(\n conditions: { lowerBound?: NumericBound; upperBound?: NumericBound }[],\n): { lowerBound?: NumericBound; upperBound?: NumericBound } {\n let lowerBound: NumericBound | undefined;\n let upperBound: NumericBound | undefined;\n\n for (const cond of conditions) {\n if (cond.lowerBound) {\n if (\n !lowerBound ||\n (cond.lowerBound.valueNumeric ?? -Infinity) >\n (lowerBound.valueNumeric ?? -Infinity)\n ) {\n lowerBound = cond.lowerBound;\n }\n }\n if (cond.upperBound) {\n if (\n !upperBound ||\n (cond.upperBound.valueNumeric ?? Infinity) <\n (upperBound.valueNumeric ?? Infinity)\n ) {\n upperBound = cond.upperBound;\n }\n }\n }\n\n return { lowerBound, upperBound };\n}\n\nfunction appendBoundsToUniqueId(\n parts: string[],\n lowerBound?: NumericBound,\n upperBound?: NumericBound,\n): void {\n if (lowerBound) {\n parts.push(lowerBound.inclusive ? '>=' : '>');\n parts.push(lowerBound.value);\n }\n if (upperBound) {\n parts.push(upperBound.inclusive ? '<=' : '<');\n parts.push(upperBound.value);\n }\n}\n\nfunction mergeDimensionRanges<T extends MediaCondition | ContainerCondition>(\n conditions: T[],\n idPrefix: string[],\n): T | null {\n if (conditions.length === 0) return null;\n\n const { lowerBound, upperBound } = tightenBounds(conditions);\n const base = conditions[0];\n\n const parts = [...idPrefix];\n appendBoundsToUniqueId(parts, lowerBound, upperBound);\n\n return {\n ...base,\n negated: false,\n raw: buildMergedRaw(base.dimension || 'width', lowerBound, upperBound),\n uniqueId: parts.join(':'),\n lowerBound,\n upperBound,\n };\n}\n\nfunction mergeMediaRanges(conditions: MediaCondition[]): MediaCondition | null {\n const dim = conditions[0]?.dimension ?? 'width';\n return mergeDimensionRanges(conditions, ['media', 'dim', dim]);\n}\n\nfunction mergeContainerRanges(\n conditions: ContainerCondition[],\n): ContainerCondition | null {\n const base = conditions[0];\n if (!base) return null;\n const name = base.containerName || '_';\n const dim = base.dimension ?? 'width';\n return mergeDimensionRanges(conditions, ['container', 'dim', name, dim]);\n}\n\nfunction buildMergedRaw(\n dimension: string,\n lowerBound?: NumericBound,\n upperBound?: NumericBound,\n): string {\n if (lowerBound && upperBound) {\n const lowerOp = lowerBound.inclusive ? '<=' : '<';\n const upperOp = upperBound.inclusive ? '<=' : '<';\n return `@media(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;\n } else if (upperBound) {\n const op = upperBound.inclusive ? '<=' : '<';\n return `@media(${dimension} ${op} ${upperBound.value})`;\n } else if (lowerBound) {\n const op = lowerBound.inclusive ? '>=' : '>';\n return `@media(${dimension} ${op} ${lowerBound.value})`;\n }\n return '@media()';\n}\n\n// ============================================================================\n// Sorting\n// ============================================================================\n\nfunction sortTerms(terms: ConditionNode[]): ConditionNode[] {\n const withIds = terms.map((t) => [getConditionUniqueId(t), t] as const);\n withIds.sort((a, b) => a[0].localeCompare(b[0]));\n return withIds.map(([, t]) => t);\n}\n\n// ============================================================================\n// Absorption\n// ============================================================================\n\n/**\n * Apply the absorption law: removes compound terms that are absorbed by\n * a simple term already present.\n *\n * For AND context: A & (A | B) → A (absorbs OR compounds)\n * For OR context: A | (A & B) → A (absorbs AND compounds)\n */\nfunction applyAbsorption(\n terms: ConditionNode[],\n absorbedOperator: 'OR' | 'AND',\n): ConditionNode[] {\n const simpleIds = new Set<string>();\n for (const term of terms) {\n if (term.kind !== 'compound') {\n simpleIds.add(getConditionUniqueId(term));\n }\n }\n\n return terms.filter((term) => {\n if (term.kind === 'compound' && term.operator === absorbedOperator) {\n for (const child of term.children) {\n if (simpleIds.has(getConditionUniqueId(child))) {\n return false;\n }\n }\n }\n return true;\n });\n}\n\nfunction applyAbsorptionAnd(terms: ConditionNode[]): ConditionNode[] {\n return applyAbsorption(terms, 'OR');\n}\n\nfunction applyAbsorptionOr(terms: ConditionNode[]): ConditionNode[] {\n return applyAbsorption(terms, 'AND');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"simplify.js","names":[],"sources":["../../src/pipeline/simplify.ts"],"sourcesContent":["/**\n * Condition Simplification Engine\n *\n * Simplifies condition trees by applying boolean algebra rules,\n * detecting contradictions, merging ranges, and deduplicating terms.\n *\n * This is critical for:\n * 1. Detecting invalid combinations (A & !A → FALSE)\n * 2. Reducing CSS output size\n * 3. Producing cleaner selectors\n */\n\nimport { Lru } from '../parser/lru';\n\nimport type {\n ConditionNode,\n ContainerCondition,\n MediaCondition,\n ModifierCondition,\n NumericBound,\n} from './conditions';\nimport {\n and,\n falseCondition,\n getConditionUniqueId,\n not,\n or,\n trueCondition,\n} from './conditions';\n\n// ============================================================================\n// Caching\n// ============================================================================\n\nconst simplifyCache = new Lru<string, ConditionNode>(5000);\n\n// ============================================================================\n// Main Simplify Function\n// ============================================================================\n\n/**\n * Simplify a condition tree aggressively.\n *\n * This applies all possible simplification rules:\n * - Boolean algebra (identity, annihilator, idempotent, absorption)\n * - Contradiction detection (A & !A → FALSE)\n * - Tautology detection (A | !A → TRUE)\n * - Range intersection for numeric queries\n * - Attribute value conflict detection\n * - Deduplication and sorting\n */\nexport function simplifyCondition(node: ConditionNode): ConditionNode {\n // Check cache\n const key = getConditionUniqueId(node);\n const cached = simplifyCache.get(key);\n if (cached) {\n return cached;\n }\n\n const result = simplifyInner(node);\n\n // Cache result\n simplifyCache.set(key, result);\n\n return result;\n}\n\n/**\n * Clear the simplify cache (for testing)\n */\nexport function clearSimplifyCache(): void {\n simplifyCache.clear();\n}\n\n// ============================================================================\n// Inner Simplification\n// ============================================================================\n\nfunction simplifyInner(node: ConditionNode): ConditionNode {\n // Base cases\n if (node.kind === 'true' || node.kind === 'false') {\n return node;\n }\n\n // State conditions - return as-is (they're already leaf nodes)\n if (node.kind === 'state') {\n return node;\n }\n\n // Compound conditions - recursively simplify\n if (node.kind === 'compound') {\n // First, recursively simplify all children\n const simplifiedChildren = node.children.map((c) => simplifyInner(c));\n\n // Then apply compound-specific simplifications\n if (node.operator === 'AND') {\n return simplifyAnd(simplifiedChildren);\n } else {\n return simplifyOr(simplifiedChildren);\n }\n }\n\n return node;\n}\n\n// ============================================================================\n// AND Simplification\n// ============================================================================\n\nfunction simplifyAnd(children: ConditionNode[]): ConditionNode {\n let terms: ConditionNode[] = [];\n\n // Flatten nested ANDs and handle TRUE/FALSE\n for (const child of children) {\n if (child.kind === 'false') {\n // AND with FALSE → FALSE\n return falseCondition();\n }\n if (child.kind === 'true') {\n // AND with TRUE → skip (identity)\n continue;\n }\n if (child.kind === 'compound' && child.operator === 'AND') {\n // Flatten nested AND\n terms.push(...child.children);\n } else {\n terms.push(child);\n }\n }\n\n // Empty → TRUE\n if (terms.length === 0) {\n return trueCondition();\n }\n\n // Single term → return it\n if (terms.length === 1) {\n return terms[0];\n }\n\n // Check for contradictions\n if (hasContradiction(terms)) {\n return falseCondition();\n }\n\n // Check for range contradictions in media/container queries\n if (hasRangeContradiction(terms)) {\n return falseCondition();\n }\n\n // Check for attribute value conflicts\n if (hasAttributeConflict(terms)) {\n return falseCondition();\n }\n\n // Check for container style query conflicts\n if (hasContainerStyleConflict(terms)) {\n return falseCondition();\n }\n\n // Remove redundant negations implied by positive terms\n // e.g., style(--variant: danger) implies NOT style(--variant: success)\n // and style(--variant: danger) implies style(--variant) (existence)\n terms = removeImpliedNegations(terms);\n\n // Deduplicate (by uniqueId)\n terms = deduplicateTerms(terms);\n\n // Try to merge numeric ranges\n terms = mergeRanges(terms);\n\n // Sort for canonical form\n terms = sortTerms(terms);\n\n // Apply absorption: A & (A | B) → A\n terms = applyAbsorptionAnd(terms);\n\n // Apply consensus/resolution: (A | B) & (A | !B) → A\n terms = applyConsensusAnd(terms);\n\n if (terms.length === 0) {\n return trueCondition();\n }\n if (terms.length === 1) {\n return terms[0];\n }\n\n return {\n kind: 'compound',\n operator: 'AND',\n children: terms,\n };\n}\n\n// ============================================================================\n// OR Simplification\n// ============================================================================\n\nfunction simplifyOr(children: ConditionNode[]): ConditionNode {\n let terms: ConditionNode[] = [];\n\n // Flatten nested ORs and handle TRUE/FALSE\n for (const child of children) {\n if (child.kind === 'true') {\n // OR with TRUE → TRUE\n return trueCondition();\n }\n if (child.kind === 'false') {\n // OR with FALSE → skip (identity)\n continue;\n }\n if (child.kind === 'compound' && child.operator === 'OR') {\n // Flatten nested OR\n terms.push(...child.children);\n } else {\n terms.push(child);\n }\n }\n\n // Empty → FALSE\n if (terms.length === 0) {\n return falseCondition();\n }\n\n // Single term → return it\n if (terms.length === 1) {\n return terms[0];\n }\n\n // Check for tautologies (A | !A)\n if (hasTautology(terms)) {\n return trueCondition();\n }\n\n // Deduplicate\n terms = deduplicateTerms(terms);\n\n // Sort for canonical form\n terms = sortTerms(terms);\n\n // Apply absorption: A | (A & B) → A\n terms = applyAbsorptionOr(terms);\n\n // Apply complementary factoring: (A & B) | (A & !B) → A\n terms = applyComplementaryFactoring(terms);\n\n if (terms.length === 0) {\n return falseCondition();\n }\n if (terms.length === 1) {\n return terms[0];\n }\n\n return {\n kind: 'compound',\n operator: 'OR',\n children: terms,\n };\n}\n\n// ============================================================================\n// Contradiction Detection\n// ============================================================================\n\n/**\n * Check if any pair of terms has complementary negation (A and !A).\n * Used for both contradiction detection (in AND) and tautology detection (in OR),\n * since the underlying check is identical: the context determines the semantics.\n */\nfunction hasComplementaryPair(terms: ConditionNode[]): boolean {\n const uniqueIds = new Set<string>();\n\n for (const term of terms) {\n if (term.kind !== 'state') continue;\n\n const id = term.uniqueId;\n const negatedId = term.negated ? id.slice(1) : `!${id}`;\n\n if (uniqueIds.has(negatedId)) {\n return true;\n }\n uniqueIds.add(id);\n }\n\n return false;\n}\n\nconst hasContradiction = hasComplementaryPair;\nconst hasTautology = hasComplementaryPair;\n\n// ============================================================================\n// Range Contradiction Detection\n// ============================================================================\n\n/**\n * Effective bounds computed from conditions (including negated single-bound conditions)\n */\ninterface EffectiveBounds {\n lowerBound: number | null;\n lowerInclusive: boolean;\n upperBound: number | null;\n upperInclusive: boolean;\n}\n\n/**\n * Excluded range from a negated range condition\n */\ninterface ExcludedRange {\n lower: number;\n lowerInclusive: boolean;\n upper: number;\n upperInclusive: boolean;\n}\n\n/**\n * Check for range contradictions in media/container queries\n * e.g., @media(w < 400px) & @media(w > 800px) → FALSE\n *\n * Also handles negated conditions:\n * - Single-bound negations are inverted (not (w < 600px) → w >= 600px)\n * - Range negations create excluded ranges that are checked against positive bounds\n */\nfunction hasRangeContradiction(terms: ConditionNode[]): boolean {\n // Group by dimension, separating positive and negated conditions\n const mediaByDim = new Map<\n string,\n { positive: MediaCondition[]; negated: MediaCondition[] }\n >();\n const containerByDim = new Map<\n string,\n { positive: ContainerCondition[]; negated: ContainerCondition[] }\n >();\n\n for (const term of terms) {\n if (term.kind !== 'state') continue;\n\n if (term.type === 'media' && term.subtype === 'dimension') {\n const key = term.dimension || 'width';\n if (!mediaByDim.has(key)) {\n mediaByDim.set(key, { positive: [], negated: [] });\n }\n const group = mediaByDim.get(key)!;\n if (term.negated) {\n group.negated.push(term);\n } else {\n group.positive.push(term);\n }\n }\n\n if (term.type === 'container' && term.subtype === 'dimension') {\n const key = `${term.containerName || '_'}:${term.dimension || 'width'}`;\n if (!containerByDim.has(key)) {\n containerByDim.set(key, { positive: [], negated: [] });\n }\n const group = containerByDim.get(key)!;\n if (term.negated) {\n group.negated.push(term);\n } else {\n group.positive.push(term);\n }\n }\n }\n\n // Check each dimension group for impossible ranges\n for (const group of mediaByDim.values()) {\n if (rangesAreImpossibleWithNegations(group.positive, group.negated)) {\n return true;\n }\n }\n\n for (const group of containerByDim.values()) {\n if (rangesAreImpossibleWithNegations(group.positive, group.negated)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Check if conditions are impossible, including negated conditions.\n *\n * For negated single-bound conditions:\n * not (w < 600px) → w >= 600px (inverted to lower bound)\n * not (w >= 800px) → w < 800px (inverted to upper bound)\n *\n * For negated range conditions:\n * not (400px <= w < 800px) → excludes [400, 800)\n * If the effective bounds fall entirely within an excluded range, it's impossible.\n */\nfunction rangesAreImpossibleWithNegations(\n positive: (MediaCondition | ContainerCondition)[],\n negated: (MediaCondition | ContainerCondition)[],\n): boolean {\n // Start with bounds from positive conditions\n const bounds = computeEffectiveBounds(positive);\n\n // Apply inverted bounds from single-bound negated conditions\n // and collect excluded ranges from range negated conditions\n const excludedRanges: ExcludedRange[] = [];\n\n for (const cond of negated) {\n const hasLower = cond.lowerBound?.valueNumeric != null;\n const hasUpper = cond.upperBound?.valueNumeric != null;\n\n if (hasLower && hasUpper) {\n // Range negation: not (lower <= w < upper) excludes [lower, upper)\n excludedRanges.push({\n lower: cond.lowerBound!.valueNumeric!,\n lowerInclusive: cond.lowerBound!.inclusive,\n upper: cond.upperBound!.valueNumeric!,\n upperInclusive: cond.upperBound!.inclusive,\n });\n } else if (hasUpper) {\n // not (w < upper) → w >= upper (becomes lower bound)\n // not (w <= upper) → w > upper (becomes lower bound, exclusive)\n const value = cond.upperBound!.valueNumeric!;\n const inclusive = !cond.upperBound!.inclusive; // flip inclusivity\n\n if (bounds.lowerBound === null || value > bounds.lowerBound) {\n bounds.lowerBound = value;\n bounds.lowerInclusive = inclusive;\n } else if (value === bounds.lowerBound && !inclusive) {\n bounds.lowerInclusive = false;\n }\n } else if (hasLower) {\n // not (w >= lower) → w < lower (becomes upper bound)\n // not (w > lower) → w <= lower (becomes upper bound, inclusive)\n const value = cond.lowerBound!.valueNumeric!;\n const inclusive = !cond.lowerBound!.inclusive; // flip inclusivity\n\n if (bounds.upperBound === null || value < bounds.upperBound) {\n bounds.upperBound = value;\n bounds.upperInclusive = inclusive;\n } else if (value === bounds.upperBound && !inclusive) {\n bounds.upperInclusive = false;\n }\n }\n }\n\n // Check if effective bounds are impossible on their own\n if (bounds.lowerBound !== null && bounds.upperBound !== null) {\n if (bounds.lowerBound > bounds.upperBound) {\n return true;\n }\n if (\n bounds.lowerBound === bounds.upperBound &&\n (!bounds.lowerInclusive || !bounds.upperInclusive)\n ) {\n return true;\n }\n }\n\n // Check if effective bounds fall entirely within any excluded range\n if (\n bounds.lowerBound !== null &&\n bounds.upperBound !== null &&\n excludedRanges.length > 0\n ) {\n for (const excluded of excludedRanges) {\n if (boundsWithinExcludedRange(bounds, excluded)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Compute effective bounds from positive (non-negated) conditions\n */\nfunction computeEffectiveBounds(\n conditions: (MediaCondition | ContainerCondition)[],\n): EffectiveBounds {\n let lowerBound: number | null = null;\n let lowerInclusive = false;\n let upperBound: number | null = null;\n let upperInclusive = false;\n\n for (const cond of conditions) {\n if (cond.lowerBound?.valueNumeric != null) {\n const value = cond.lowerBound.valueNumeric;\n const inclusive = cond.lowerBound.inclusive;\n\n if (lowerBound === null || value > lowerBound) {\n lowerBound = value;\n lowerInclusive = inclusive;\n } else if (value === lowerBound && !inclusive) {\n lowerInclusive = false;\n }\n }\n\n if (cond.upperBound?.valueNumeric != null) {\n const value = cond.upperBound.valueNumeric;\n const inclusive = cond.upperBound.inclusive;\n\n if (upperBound === null || value < upperBound) {\n upperBound = value;\n upperInclusive = inclusive;\n } else if (value === upperBound && !inclusive) {\n upperInclusive = false;\n }\n }\n }\n\n return { lowerBound, lowerInclusive, upperBound, upperInclusive };\n}\n\n/**\n * Check if effective bounds fall entirely within an excluded range.\n *\n * For example:\n * Effective: [400, 800)\n * Excluded: [400, 800)\n * → bounds fall entirely within excluded range → impossible\n */\nfunction boundsWithinExcludedRange(\n bounds: EffectiveBounds,\n excluded: ExcludedRange,\n): boolean {\n if (bounds.lowerBound === null || bounds.upperBound === null) {\n return false;\n }\n\n // Check if bounds.lower >= excluded.lower\n let lowerOk = false;\n if (bounds.lowerBound > excluded.lower) {\n lowerOk = true;\n } else if (bounds.lowerBound === excluded.lower) {\n // If excluded includes lower, and bounds includes or excludes lower, it's within\n // If excluded excludes lower, bounds must also exclude it to be within\n lowerOk = excluded.lowerInclusive || !bounds.lowerInclusive;\n }\n\n // Check if bounds.upper <= excluded.upper\n let upperOk = false;\n if (bounds.upperBound < excluded.upper) {\n upperOk = true;\n } else if (bounds.upperBound === excluded.upper) {\n // If excluded includes upper, and bounds includes or excludes upper, it's within\n // If excluded excludes upper, bounds must also exclude it to be within\n upperOk = excluded.upperInclusive || !bounds.upperInclusive;\n }\n\n return lowerOk && upperOk;\n}\n\n// ============================================================================\n// Attribute Conflict Detection\n// ============================================================================\n\n/**\n * Check for attribute value conflicts\n * e.g., [data-theme=\"dark\"] & [data-theme=\"light\"] → FALSE\n * e.g., [data-theme=\"dark\"] & ![data-theme] → FALSE\n */\n/**\n * Generic value-conflict checker for grouped conditions.\n *\n * Groups terms by a key, splits into positive/negated, then checks:\n * 1. Multiple distinct positive values → conflict\n * 2. Positive value + negated existence (value === undefined) → conflict\n * 3. Positive value + negated same value → conflict\n */\nfunction hasGroupedValueConflict<T extends { negated: boolean }>(\n terms: ConditionNode[],\n match: (term: ConditionNode) => T | null,\n groupKey: (term: T) => string,\n getValue: (term: T) => string | undefined,\n): boolean {\n const groups = new Map<string, { positive: T[]; negated: T[] }>();\n\n for (const term of terms) {\n const matched = match(term);\n if (!matched) continue;\n\n const key = groupKey(matched);\n let group = groups.get(key);\n if (!group) {\n group = { positive: [], negated: [] };\n groups.set(key, group);\n }\n\n if (matched.negated) {\n group.negated.push(matched);\n } else {\n group.positive.push(matched);\n }\n }\n\n for (const [, group] of groups) {\n const positiveValues = group.positive\n .map(getValue)\n .filter((v) => v !== undefined);\n if (new Set(positiveValues).size > 1) return true;\n\n const hasPositiveValue = positiveValues.length > 0;\n const hasNegatedExistence = group.negated.some(\n (t) => getValue(t) === undefined,\n );\n if (hasPositiveValue && hasNegatedExistence) return true;\n\n for (const pos of group.positive) {\n const posVal = getValue(pos);\n if (posVal !== undefined) {\n for (const neg of group.negated) {\n if (getValue(neg) === posVal) return true;\n }\n }\n }\n }\n\n return false;\n}\n\nfunction hasAttributeConflict(terms: ConditionNode[]): boolean {\n return hasGroupedValueConflict<ModifierCondition>(\n terms,\n (t) => (t.kind === 'state' && t.type === 'modifier' ? t : null),\n (t) => t.attribute,\n (t) => t.value,\n );\n}\n\nfunction hasContainerStyleConflict(terms: ConditionNode[]): boolean {\n return hasGroupedValueConflict<ContainerCondition>(\n terms,\n (t) =>\n t.kind === 'state' && t.type === 'container' && t.subtype === 'style'\n ? t\n : null,\n (t) => `${t.containerName || '_'}:${t.property}`,\n (t) => t.propertyValue,\n );\n}\n\n// ============================================================================\n// Implied Negation Removal\n// ============================================================================\n\n/**\n * Remove negations that are implied by positive terms.\n *\n * Key optimizations:\n * 1. style(--variant: danger) implies NOT style(--variant: success)\n * → If we have style(--variant: danger) & not style(--variant: success),\n * the negation is redundant and can be removed.\n *\n * 2. [data-theme=\"dark\"] implies NOT [data-theme=\"light\"]\n * → Same logic for attribute selectors.\n *\n * This produces cleaner CSS:\n * Before: @container style(--variant: danger) and (not style(--variant: success))\n * After: @container style(--variant: danger)\n */\n/**\n * Collect positive values from terms and build a \"is this negation implied?\" check.\n *\n * A negation is implied (redundant) when a positive term for the same group\n * already pins a specific value, making \"NOT other-value\" obvious.\n * e.g. style(--variant: danger) implies NOT style(--variant: success).\n */\nfunction buildImpliedNegationCheck(\n terms: ConditionNode[],\n): (term: ConditionNode) => boolean {\n const positiveValues = new Map<string, string>();\n\n for (const term of terms) {\n if (term.kind !== 'state' || term.negated) continue;\n\n if (term.type === 'container' && term.subtype === 'style') {\n if (term.propertyValue !== undefined) {\n positiveValues.set(\n `c:${term.containerName || '_'}:${term.property}`,\n term.propertyValue,\n );\n }\n } else if (term.type === 'modifier' && term.value !== undefined) {\n positiveValues.set(`m:${term.attribute}`, term.value);\n }\n }\n\n return (term: ConditionNode): boolean => {\n if (term.kind !== 'state' || !term.negated) return false;\n\n if (term.type === 'container' && term.subtype === 'style') {\n if (term.propertyValue === undefined) return false;\n const pos = positiveValues.get(\n `c:${term.containerName || '_'}:${term.property}`,\n );\n return pos !== undefined && term.propertyValue !== pos;\n }\n\n if (term.type === 'modifier' && term.value !== undefined) {\n const pos = positiveValues.get(`m:${term.attribute}`);\n return pos !== undefined && term.value !== pos;\n }\n\n return false;\n };\n}\n\nfunction removeImpliedNegations(terms: ConditionNode[]): ConditionNode[] {\n const isImplied = buildImpliedNegationCheck(terms);\n return terms.filter((t) => !isImplied(t));\n}\n\n// ============================================================================\n// Deduplication\n// ============================================================================\n\nfunction deduplicateTerms(terms: ConditionNode[]): ConditionNode[] {\n const seen = new Set<string>();\n const result: ConditionNode[] = [];\n\n for (const term of terms) {\n const id = getConditionUniqueId(term);\n if (!seen.has(id)) {\n seen.add(id);\n result.push(term);\n }\n }\n\n return result;\n}\n\n// ============================================================================\n// Range Merging\n// ============================================================================\n\n/**\n * Merge compatible range conditions\n * e.g., @media(w >= 400px) & @media(w <= 800px) → @media(400px <= w <= 800px)\n */\nfunction mergeRanges(terms: ConditionNode[]): ConditionNode[] {\n // Group media conditions by dimension\n const mediaByDim = new Map<\n string,\n { conditions: MediaCondition[]; indices: number[] }\n >();\n const containerByDim = new Map<\n string,\n { conditions: ContainerCondition[]; indices: number[] }\n >();\n\n terms.forEach((term, index) => {\n if (term.kind !== 'state') return;\n\n if (\n term.type === 'media' &&\n term.subtype === 'dimension' &&\n !term.negated\n ) {\n const key = term.dimension || 'width';\n if (!mediaByDim.has(key)) {\n mediaByDim.set(key, { conditions: [], indices: [] });\n }\n const group = mediaByDim.get(key)!;\n group.conditions.push(term);\n group.indices.push(index);\n }\n\n if (\n term.type === 'container' &&\n term.subtype === 'dimension' &&\n !term.negated\n ) {\n const key = `${term.containerName || '_'}:${term.dimension || 'width'}`;\n if (!containerByDim.has(key)) {\n containerByDim.set(key, { conditions: [], indices: [] });\n }\n const group = containerByDim.get(key)!;\n group.conditions.push(term);\n group.indices.push(index);\n }\n });\n\n // Track indices to remove\n const indicesToRemove = new Set<number>();\n const mergedTerms: ConditionNode[] = [];\n\n // Merge media conditions\n for (const [_dim, group] of mediaByDim) {\n if (group.conditions.length > 1) {\n const merged = mergeMediaRanges(group.conditions);\n if (merged) {\n group.indices.forEach((i) => indicesToRemove.add(i));\n mergedTerms.push(merged);\n }\n }\n }\n\n // Merge container conditions\n for (const [, group] of containerByDim) {\n if (group.conditions.length > 1) {\n const merged = mergeContainerRanges(group.conditions);\n if (merged) {\n group.indices.forEach((i) => indicesToRemove.add(i));\n mergedTerms.push(merged);\n }\n }\n }\n\n // Build result\n const result: ConditionNode[] = [];\n terms.forEach((term, index) => {\n if (!indicesToRemove.has(index)) {\n result.push(term);\n }\n });\n result.push(...mergedTerms);\n\n return result;\n}\n\n/**\n * Tighten bounds by picking the most restrictive lower and upper bounds\n * from a set of conditions that have lowerBound/upperBound fields.\n */\nfunction tightenBounds(\n conditions: { lowerBound?: NumericBound; upperBound?: NumericBound }[],\n): { lowerBound?: NumericBound; upperBound?: NumericBound } {\n let lowerBound: NumericBound | undefined;\n let upperBound: NumericBound | undefined;\n\n for (const cond of conditions) {\n if (cond.lowerBound) {\n if (\n !lowerBound ||\n (cond.lowerBound.valueNumeric ?? -Infinity) >\n (lowerBound.valueNumeric ?? -Infinity)\n ) {\n lowerBound = cond.lowerBound;\n }\n }\n if (cond.upperBound) {\n if (\n !upperBound ||\n (cond.upperBound.valueNumeric ?? Infinity) <\n (upperBound.valueNumeric ?? Infinity)\n ) {\n upperBound = cond.upperBound;\n }\n }\n }\n\n return { lowerBound, upperBound };\n}\n\nfunction appendBoundsToUniqueId(\n parts: string[],\n lowerBound?: NumericBound,\n upperBound?: NumericBound,\n): void {\n if (lowerBound) {\n parts.push(lowerBound.inclusive ? '>=' : '>');\n parts.push(lowerBound.value);\n }\n if (upperBound) {\n parts.push(upperBound.inclusive ? '<=' : '<');\n parts.push(upperBound.value);\n }\n}\n\nfunction mergeDimensionRanges<T extends MediaCondition | ContainerCondition>(\n conditions: T[],\n idPrefix: string[],\n): T | null {\n if (conditions.length === 0) return null;\n\n const { lowerBound, upperBound } = tightenBounds(conditions);\n const base = conditions[0];\n\n const parts = [...idPrefix];\n appendBoundsToUniqueId(parts, lowerBound, upperBound);\n\n return {\n ...base,\n negated: false,\n raw: buildMergedRaw(base.dimension || 'width', lowerBound, upperBound),\n uniqueId: parts.join(':'),\n lowerBound,\n upperBound,\n };\n}\n\nfunction mergeMediaRanges(conditions: MediaCondition[]): MediaCondition | null {\n const dim = conditions[0]?.dimension ?? 'width';\n return mergeDimensionRanges(conditions, ['media', 'dim', dim]);\n}\n\nfunction mergeContainerRanges(\n conditions: ContainerCondition[],\n): ContainerCondition | null {\n const base = conditions[0];\n if (!base) return null;\n const name = base.containerName || '_';\n const dim = base.dimension ?? 'width';\n return mergeDimensionRanges(conditions, ['container', 'dim', name, dim]);\n}\n\nfunction buildMergedRaw(\n dimension: string,\n lowerBound?: NumericBound,\n upperBound?: NumericBound,\n): string {\n if (lowerBound && upperBound) {\n const lowerOp = lowerBound.inclusive ? '<=' : '<';\n const upperOp = upperBound.inclusive ? '<=' : '<';\n return `@media(${lowerBound.value} ${lowerOp} ${dimension} ${upperOp} ${upperBound.value})`;\n } else if (upperBound) {\n const op = upperBound.inclusive ? '<=' : '<';\n return `@media(${dimension} ${op} ${upperBound.value})`;\n } else if (lowerBound) {\n const op = lowerBound.inclusive ? '>=' : '>';\n return `@media(${dimension} ${op} ${lowerBound.value})`;\n }\n return '@media()';\n}\n\n// ============================================================================\n// Complementary Factoring\n// ============================================================================\n\n/**\n * Apply complementary factoring: (A & B) | (A & !B) → A\n *\n * Finds pairs of AND compounds that share all children except one,\n * where the differing child is complementary (X vs !X).\n * Replaces the pair with the common terms.\n *\n * This is applied iteratively until no more reductions are possible,\n * since factoring can expose further simplification opportunities.\n */\nfunction applyComplementaryFactoring(terms: ConditionNode[]): ConditionNode[] {\n let changed = true;\n\n while (changed) {\n changed = false;\n\n for (let i = 0; i < terms.length; i++) {\n const a = terms[i];\n if (a.kind !== 'compound' || a.operator !== 'AND') continue;\n\n for (let j = i + 1; j < terms.length; j++) {\n const b = terms[j];\n if (b.kind !== 'compound' || b.operator !== 'AND') continue;\n\n const factored = tryFactorPair(a.children, b.children);\n if (factored) {\n const replacement = simplifyInner(factored);\n terms = [\n ...terms.slice(0, i),\n ...terms.slice(i + 1, j),\n ...terms.slice(j + 1),\n replacement,\n ];\n changed = true;\n break;\n }\n }\n\n if (changed) break;\n }\n }\n\n return terms;\n}\n\n/**\n * Try to factor two AND children lists.\n *\n * Extracts the common children (by uniqueId). If the remaining\n * (non-common) parts of each side OR to TRUE, the common part alone\n * is sufficient: `(common & restA) | (common & restB) → common`\n * when `restA | restB → TRUE`.\n *\n * Also handles the simpler case where exactly one child is\n * complementary: `[A, B, C]` and `[A, !B, C]` → `AND(A, C)`.\n */\nfunction tryFactorPair(\n aChildren: ConditionNode[],\n bChildren: ConditionNode[],\n): ConditionNode | null {\n const aIds = aChildren.map((c) => getConditionUniqueId(c));\n const bIds = bChildren.map((c) => getConditionUniqueId(c));\n\n const bIdSet = new Set(bIds);\n const aIdSet = new Set(aIds);\n\n // Extract common children (present in both by uniqueId)\n const commonIndicesA: number[] = [];\n const restIndicesA: number[] = [];\n for (let i = 0; i < aIds.length; i++) {\n if (bIdSet.has(aIds[i])) {\n commonIndicesA.push(i);\n } else {\n restIndicesA.push(i);\n }\n }\n\n const restIndicesB: number[] = [];\n for (let i = 0; i < bIds.length; i++) {\n if (!aIdSet.has(bIds[i])) {\n restIndicesB.push(i);\n }\n }\n\n // Must have at least one common child and differing parts on both sides\n if (commonIndicesA.length === 0) return null;\n if (restIndicesA.length === 0 && restIndicesB.length === 0) return null;\n\n // Build the \"rest\" conditions for each side\n const restA =\n restIndicesA.length === 0\n ? trueCondition()\n : restIndicesA.length === 1\n ? aChildren[restIndicesA[0]]\n : and(...restIndicesA.map((i) => aChildren[i]));\n\n const restB =\n restIndicesB.length === 0\n ? trueCondition()\n : restIndicesB.length === 1\n ? bChildren[restIndicesB[0]]\n : and(...restIndicesB.map((i) => bChildren[i]));\n\n // Check if restA | restB simplifies to TRUE\n const combined = simplifyInner({\n kind: 'compound',\n operator: 'OR',\n children: [restA, restB],\n });\n\n if (combined.kind !== 'true') {\n // Direct complement check for compound conditions.\n // hasTautology only detects leaf-level A/!A pairs, so compound\n // complements like @hc / !@hc (which expand via De Morgan into\n // structurally different trees) are missed. Compare the simplified\n // negation of one rest against the simplified other rest instead.\n const simplifiedRestB = simplifyInner(restB);\n const negRestA = simplifyInner(not(restA));\n\n if (\n getConditionUniqueId(negRestA) !== getConditionUniqueId(simplifiedRestB)\n ) {\n return null;\n }\n }\n\n // restA | restB = TRUE → (common & restA) | (common & restB) = common\n const common = commonIndicesA.map((i) => aChildren[i]);\n\n if (common.length === 0) {\n return trueCondition();\n }\n if (common.length === 1) {\n return common[0];\n }\n return and(...common);\n}\n\n// ============================================================================\n// Sorting\n// ============================================================================\n\nfunction sortTerms(terms: ConditionNode[]): ConditionNode[] {\n const withIds = terms.map((t) => [getConditionUniqueId(t), t] as const);\n withIds.sort((a, b) => a[0].localeCompare(b[0]));\n return withIds.map(([, t]) => t);\n}\n\n// ============================================================================\n// Absorption\n// ============================================================================\n\n/**\n * Apply the absorption law: removes compound terms that are absorbed by\n * another term already present (simple or compound).\n *\n * For AND context: A & (A | B) → A (absorbs OR compounds)\n * For OR context: A | (A & B) → A (absorbs AND compounds)\n *\n * After flattening, a compound A = OR(X, Y) becomes [X, Y, ...] in the\n * outer OR. A child AND(A, B) = AND(OR(X, Y), B) still references the\n * original un-flattened compound. We reconstruct possible compound\n * absorbers from the flattened terms so absorption works across nesting.\n */\nfunction applyAbsorption(\n terms: ConditionNode[],\n absorbedOperator: 'OR' | 'AND',\n): ConditionNode[] {\n // Collect IDs of ALL top-level terms as potential absorbers.\n const absorberIds = new Set<string>();\n for (const term of terms) {\n absorberIds.add(getConditionUniqueId(term));\n }\n\n // Reconstruct compound absorbers: if all children of a compound node\n // (found inside an absorbable term) are themselves absorbers, then\n // that compound is also an absorber. This handles the case where\n // A = OR(X, Y) was flattened into [X, Y, ...] at the top level,\n // but appears un-flattened as a child of AND(A, B).\n let changed = true;\n while (changed) {\n changed = false;\n for (const term of terms) {\n if (term.kind !== 'compound' || term.operator !== absorbedOperator) {\n continue;\n }\n for (const child of term.children) {\n if (child.kind !== 'compound') continue;\n const childId = getConditionUniqueId(child);\n if (absorberIds.has(childId)) continue;\n if (\n child.children.every((c) => absorberIds.has(getConditionUniqueId(c)))\n ) {\n absorberIds.add(childId);\n changed = true;\n }\n }\n }\n }\n\n return terms.filter((term) => {\n if (term.kind === 'compound' && term.operator === absorbedOperator) {\n for (const child of term.children) {\n if (absorberIds.has(getConditionUniqueId(child))) {\n return false;\n }\n }\n }\n return true;\n });\n}\n\nfunction applyAbsorptionAnd(terms: ConditionNode[]): ConditionNode[] {\n return applyAbsorption(terms, 'OR');\n}\n\nfunction applyAbsorptionOr(terms: ConditionNode[]): ConditionNode[] {\n return applyAbsorption(terms, 'AND');\n}\n\n// ============================================================================\n// Consensus / Resolution (AND dual of complementary factoring)\n// ============================================================================\n\n/**\n * Apply the consensus/resolution rule for AND:\n * (A | B) & (A | !B) → A\n *\n * This is the dual of complementary factoring in OR context:\n * (A & B) | (A & !B) → A\n *\n * Extracts common children from two OR terms. If the remaining\n * parts of each side AND to FALSE, the common part alone is\n * sufficient.\n */\nfunction applyConsensusAnd(terms: ConditionNode[]): ConditionNode[] {\n let changed = true;\n\n while (changed) {\n changed = false;\n\n for (let i = 0; i < terms.length; i++) {\n const a = terms[i];\n if (a.kind !== 'compound' || a.operator !== 'OR') continue;\n\n for (let j = i + 1; j < terms.length; j++) {\n const b = terms[j];\n if (b.kind !== 'compound' || b.operator !== 'OR') continue;\n\n const resolved = tryResolvePair(a.children, b.children);\n if (resolved) {\n const replacement = simplifyInner(resolved);\n terms = [\n ...terms.slice(0, i),\n ...terms.slice(i + 1, j),\n ...terms.slice(j + 1),\n replacement,\n ];\n changed = true;\n break;\n }\n }\n\n if (changed) break;\n }\n }\n\n return terms;\n}\n\n/**\n * Try to resolve two OR children lists.\n *\n * Extracts common children (by uniqueId). If the remaining\n * (non-common) parts AND to FALSE, the common part alone\n * is sufficient: `(common | restA) & (common | restB) → common`\n * when `restA & restB → FALSE`.\n */\nfunction tryResolvePair(\n aChildren: ConditionNode[],\n bChildren: ConditionNode[],\n): ConditionNode | null {\n const aIds = aChildren.map((c) => getConditionUniqueId(c));\n const bIds = bChildren.map((c) => getConditionUniqueId(c));\n\n const bIdSet = new Set(bIds);\n const aIdSet = new Set(aIds);\n\n const commonIndicesA: number[] = [];\n const restIndicesA: number[] = [];\n for (let i = 0; i < aIds.length; i++) {\n if (bIdSet.has(aIds[i])) {\n commonIndicesA.push(i);\n } else {\n restIndicesA.push(i);\n }\n }\n\n const restIndicesB: number[] = [];\n for (let i = 0; i < bIds.length; i++) {\n if (!aIdSet.has(bIds[i])) {\n restIndicesB.push(i);\n }\n }\n\n if (commonIndicesA.length === 0) return null;\n if (restIndicesA.length === 0 && restIndicesB.length === 0) return null;\n\n const restA =\n restIndicesA.length === 0\n ? falseCondition()\n : restIndicesA.length === 1\n ? aChildren[restIndicesA[0]]\n : or(...restIndicesA.map((i) => aChildren[i]));\n\n const restB =\n restIndicesB.length === 0\n ? falseCondition()\n : restIndicesB.length === 1\n ? bChildren[restIndicesB[0]]\n : or(...restIndicesB.map((i) => bChildren[i]));\n\n // Check if restA & restB simplifies to FALSE\n const combined = simplifyInner({\n kind: 'compound',\n operator: 'AND',\n children: [restA, restB],\n });\n\n if (combined.kind !== 'false') {\n // Direct complement check for compound conditions\n const simplifiedRestB = simplifyInner(restB);\n const negRestA = simplifyInner(not(restA));\n\n if (\n getConditionUniqueId(negRestA) !== getConditionUniqueId(simplifiedRestB)\n ) {\n return null;\n }\n }\n\n // restA & restB = FALSE → (common | restA) & (common | restB) = common\n const common = commonIndicesA.map((i) => aChildren[i]);\n\n if (common.length === 0) {\n return falseCondition();\n }\n if (common.length === 1) {\n return common[0];\n }\n return or(...common);\n}\n"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { StyleDetails, UnitHandler } from "../parser/types.js";
2
2
  import { StyleHandlerDefinition } from "../utils/styles.js";
3
- import { ConfigTokens, RecipeStyles } from "../styles/types.js";
3
+ import { ConfigTokens, RecipeStyles, Styles } from "../styles/types.js";
4
+ import { TypographyPreset } from "../utils/typography.js";
4
5
 
5
6
  //#region src/plugins/types.d.ts
6
7
  /**
@@ -50,6 +51,16 @@ interface TastyPlugin {
50
51
  * ```
51
52
  */
52
53
  recipes?: Record<string, RecipeStyles>;
54
+ /**
55
+ * Typography presets — shorthand for `generateTypographyTokens()`.
56
+ * Generated tokens are merged under explicit `tokens` (tokens win on conflict).
57
+ */
58
+ presets?: Record<string, TypographyPreset>;
59
+ /**
60
+ * Global Tasty styles keyed by CSS selector.
61
+ * Supports the full Tasty style syntax.
62
+ */
63
+ globalStyles?: Record<string, Styles>;
53
64
  }
54
65
  /**
55
66
  * A factory function that creates a TastyPlugin.
package/dist/rsc-cache.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { hashString } from "./utils/hash.js";
1
2
  import { getRegisteredSSRCollector } from "./ssr/ssr-collector-ref.js";
2
3
  import { cache } from "react";
3
4
  //#region src/rsc-cache.ts
@@ -17,11 +18,8 @@ import { cache } from "react";
17
18
  */
18
19
  const getRSCCache = cache(() => ({
19
20
  cacheKeyToClassName: /* @__PURE__ */ new Map(),
20
- classCounter: 0,
21
21
  emittedKeys: /* @__PURE__ */ new Set(),
22
22
  internalsEmitted: false,
23
- keyframesCounter: 0,
24
- counterStyleCounter: 0,
25
23
  pendingCSS: [],
26
24
  generatedNames: /* @__PURE__ */ new Map()
27
25
  }));
@@ -31,7 +29,7 @@ function rscAllocateClassName(rscCache, cacheKey) {
31
29
  className: existing,
32
30
  isNew: false
33
31
  };
34
- const className = `r${rscCache.classCounter++}`;
32
+ const className = `t${hashString(cacheKey)}`;
35
33
  rscCache.cacheKeyToClassName.set(cacheKey, className);
36
34
  return {
37
35
  className,
@@ -1 +1 @@
1
- {"version":3,"file":"rsc-cache.js","names":[],"sources":["../src/rsc-cache.ts"],"sourcesContent":["/**\n * Shared RSC (React Server Components) inline style cache.\n *\n * Uses React.cache for per-request memoization in Server Components.\n * Both computeStyles() and standalone style functions (useGlobalStyles,\n * useRawCSS, useKeyframes, useProperty, useFontFace, useCounterStyle)\n * share this cache so that CSS accumulated by standalone functions is\n * flushed into inline <style> tags by the next tasty() component.\n */\n\nimport { cache } from 'react';\n\nimport type { ServerStyleCollector } from './ssr/collector';\nimport { getRegisteredSSRCollector } from './ssr/ssr-collector-ref';\n\nexport interface RSCStyleCache {\n cacheKeyToClassName: Map<string, string>;\n classCounter: number;\n emittedKeys: Set<string>;\n internalsEmitted: boolean;\n keyframesCounter: number;\n counterStyleCounter: number;\n pendingCSS: string[];\n /** Maps dedup key -> generated name for keyframes and counter-styles in RSC mode. */\n generatedNames: Map<string, string>;\n}\n\n/**\n * Per-request RSC style cache using React.cache.\n * React.cache provides per-request memoization in Server Components,\n * so each request gets its own isolated cache.\n */\nexport const getRSCCache = cache(\n (): RSCStyleCache => ({\n cacheKeyToClassName: new Map(),\n classCounter: 0,\n emittedKeys: new Set(),\n internalsEmitted: false,\n keyframesCounter: 0,\n counterStyleCounter: 0,\n pendingCSS: [],\n generatedNames: new Map(),\n }),\n);\n\nexport function rscAllocateClassName(\n rscCache: RSCStyleCache,\n cacheKey: string,\n): { className: string; isNew: boolean } {\n const existing = rscCache.cacheKeyToClassName.get(cacheKey);\n if (existing) return { className: existing, isNew: false };\n\n // Use 'r' prefix to avoid collisions with SSR collector's 't' prefix\n const className = `r${rscCache.classCounter++}`;\n rscCache.cacheKeyToClassName.set(cacheKey, className);\n return { className, isNew: true };\n}\n\n/**\n * Flush any pending CSS accumulated by standalone functions.\n * Returns the CSS string and clears the buffer.\n */\nexport function flushPendingCSS(rscCache: RSCStyleCache): string {\n if (rscCache.pendingCSS.length === 0) return '';\n const css = rscCache.pendingCSS.join('\\n');\n rscCache.pendingCSS.length = 0;\n return css;\n}\n\n/**\n * Push CSS into the RSC pending buffer with dedup via emittedKeys.\n * Returns true if the CSS was added, false if it was already emitted.\n */\nexport function pushRSCCSS(\n rscCache: RSCStyleCache,\n key: string,\n css: string,\n): boolean {\n if (rscCache.emittedKeys.has(key)) return false;\n rscCache.emittedKeys.add(key);\n rscCache.pendingCSS.push(css);\n return true;\n}\n\nexport type StyleTarget =\n | { mode: 'ssr'; collector: ServerStyleCollector }\n | { mode: 'rsc'; cache: RSCStyleCache }\n | { mode: 'client' };\n\n/**\n * Determine the current style injection target.\n * Centralizes the three-way detection (SSR collector / RSC cache / client DOM)\n * used by all style functions.\n */\nexport function getStyleTarget(): StyleTarget {\n const collector = getRegisteredSSRCollector();\n if (collector) return { mode: 'ssr', collector };\n if (typeof document === 'undefined')\n return { mode: 'rsc', cache: getRSCCache() };\n return { mode: 'client' };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,MAAa,cAAc,aACH;CACpB,qCAAqB,IAAI,KAAK;CAC9B,cAAc;CACd,6BAAa,IAAI,KAAK;CACtB,kBAAkB;CAClB,kBAAkB;CAClB,qBAAqB;CACrB,YAAY,EAAE;CACd,gCAAgB,IAAI,KAAK;CAC1B,EACF;AAED,SAAgB,qBACd,UACA,UACuC;CACvC,MAAM,WAAW,SAAS,oBAAoB,IAAI,SAAS;AAC3D,KAAI,SAAU,QAAO;EAAE,WAAW;EAAU,OAAO;EAAO;CAG1D,MAAM,YAAY,IAAI,SAAS;AAC/B,UAAS,oBAAoB,IAAI,UAAU,UAAU;AACrD,QAAO;EAAE;EAAW,OAAO;EAAM;;;;;;AAOnC,SAAgB,gBAAgB,UAAiC;AAC/D,KAAI,SAAS,WAAW,WAAW,EAAG,QAAO;CAC7C,MAAM,MAAM,SAAS,WAAW,KAAK,KAAK;AAC1C,UAAS,WAAW,SAAS;AAC7B,QAAO;;;;;;AAOT,SAAgB,WACd,UACA,KACA,KACS;AACT,KAAI,SAAS,YAAY,IAAI,IAAI,CAAE,QAAO;AAC1C,UAAS,YAAY,IAAI,IAAI;AAC7B,UAAS,WAAW,KAAK,IAAI;AAC7B,QAAO;;;;;;;AAaT,SAAgB,iBAA8B;CAC5C,MAAM,YAAY,2BAA2B;AAC7C,KAAI,UAAW,QAAO;EAAE,MAAM;EAAO;EAAW;AAChD,KAAI,OAAO,aAAa,YACtB,QAAO;EAAE,MAAM;EAAO,OAAO,aAAa;EAAE;AAC9C,QAAO,EAAE,MAAM,UAAU"}
1
+ {"version":3,"file":"rsc-cache.js","names":[],"sources":["../src/rsc-cache.ts"],"sourcesContent":["/**\n * Shared RSC (React Server Components) inline style cache.\n *\n * Uses React.cache for per-request memoization in Server Components.\n * Both computeStyles() and standalone style functions (useGlobalStyles,\n * useRawCSS, useKeyframes, useProperty, useFontFace, useCounterStyle)\n * share this cache so that CSS accumulated by standalone functions is\n * flushed into inline <style> tags by the next tasty() component.\n */\n\nimport { cache } from 'react';\n\nimport type { ServerStyleCollector } from './ssr/collector';\nimport { getRegisteredSSRCollector } from './ssr/ssr-collector-ref';\nimport { hashString } from './utils/hash';\n\nexport interface RSCStyleCache {\n cacheKeyToClassName: Map<string, string>;\n emittedKeys: Set<string>;\n internalsEmitted: boolean;\n pendingCSS: string[];\n /** Maps dedup key -> generated name for keyframes and counter-styles in RSC mode. */\n generatedNames: Map<string, string>;\n}\n\n/**\n * Per-request RSC style cache using React.cache.\n * React.cache provides per-request memoization in Server Components,\n * so each request gets its own isolated cache.\n */\nexport const getRSCCache = cache(\n (): RSCStyleCache => ({\n cacheKeyToClassName: new Map(),\n emittedKeys: new Set(),\n internalsEmitted: false,\n pendingCSS: [],\n generatedNames: new Map(),\n }),\n);\n\nexport function rscAllocateClassName(\n rscCache: RSCStyleCache,\n cacheKey: string,\n): { className: string; isNew: boolean } {\n const existing = rscCache.cacheKeyToClassName.get(cacheKey);\n if (existing) return { className: existing, isNew: false };\n\n // Content-hash ensures stable names across all environments (RSC, SSR, client),\n // enabling cross-environment dedup and preventing class collisions.\n const className = `t${hashString(cacheKey)}`;\n rscCache.cacheKeyToClassName.set(cacheKey, className);\n return { className, isNew: true };\n}\n\n/**\n * Flush any pending CSS accumulated by standalone functions.\n * Returns the CSS string and clears the buffer.\n */\nexport function flushPendingCSS(rscCache: RSCStyleCache): string {\n if (rscCache.pendingCSS.length === 0) return '';\n const css = rscCache.pendingCSS.join('\\n');\n rscCache.pendingCSS.length = 0;\n return css;\n}\n\n/**\n * Push CSS into the RSC pending buffer with dedup via emittedKeys.\n * Returns true if the CSS was added, false if it was already emitted.\n */\nexport function pushRSCCSS(\n rscCache: RSCStyleCache,\n key: string,\n css: string,\n): boolean {\n if (rscCache.emittedKeys.has(key)) return false;\n rscCache.emittedKeys.add(key);\n rscCache.pendingCSS.push(css);\n return true;\n}\n\nexport type StyleTarget =\n | { mode: 'ssr'; collector: ServerStyleCollector }\n | { mode: 'rsc'; cache: RSCStyleCache }\n | { mode: 'client' };\n\n/**\n * Determine the current style injection target.\n * Centralizes the three-way detection (SSR collector / RSC cache / client DOM)\n * used by all style functions.\n */\nexport function getStyleTarget(): StyleTarget {\n const collector = getRegisteredSSRCollector();\n if (collector) return { mode: 'ssr', collector };\n if (typeof document === 'undefined')\n return { mode: 'rsc', cache: getRSCCache() };\n return { mode: 'client' };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA8BA,MAAa,cAAc,aACH;CACpB,qCAAqB,IAAI,KAAK;CAC9B,6BAAa,IAAI,KAAK;CACtB,kBAAkB;CAClB,YAAY,EAAE;CACd,gCAAgB,IAAI,KAAK;CAC1B,EACF;AAED,SAAgB,qBACd,UACA,UACuC;CACvC,MAAM,WAAW,SAAS,oBAAoB,IAAI,SAAS;AAC3D,KAAI,SAAU,QAAO;EAAE,WAAW;EAAU,OAAO;EAAO;CAI1D,MAAM,YAAY,IAAI,WAAW,SAAS;AAC1C,UAAS,oBAAoB,IAAI,UAAU,UAAU;AACrD,QAAO;EAAE;EAAW,OAAO;EAAM;;;;;;AAOnC,SAAgB,gBAAgB,UAAiC;AAC/D,KAAI,SAAS,WAAW,WAAW,EAAG,QAAO;CAC7C,MAAM,MAAM,SAAS,WAAW,KAAK,KAAK;AAC1C,UAAS,WAAW,SAAS;AAC7B,QAAO;;;;;;AAOT,SAAgB,WACd,UACA,KACA,KACS;AACT,KAAI,SAAS,YAAY,IAAI,IAAI,CAAE,QAAO;AAC1C,UAAS,YAAY,IAAI,IAAI;AAC7B,UAAS,WAAW,KAAK,IAAI;AAC7B,QAAO;;;;;;;AAaT,SAAgB,iBAA8B;CAC5C,MAAM,YAAY,2BAA2B;AAC7C,KAAI,UAAW,QAAO;EAAE,MAAM;EAAO;EAAW;AAChD,KAAI,OAAO,aAAa,YACtB,QAAO;EAAE,MAAM;EAAO,OAAO,aAAa;EAAE;AAC9C,QAAO,EAAE,MAAM,UAAU"}
@@ -1,11 +1,11 @@
1
- import { hydrateTastyCache } from "./hydrate.js";
1
+ import { hydrateTastyClasses } from "./hydrate.js";
2
2
  //#region src/ssr/astro-client.ts
3
3
  /**
4
4
  * Client-side cache hydration for Astro islands.
5
5
  *
6
- * Reads the SSR cache state from <script data-tasty-cache> and
7
- * pre-populates the injector so island hydration skips the style
8
- * pipeline entirely.
6
+ * Reads the class name list from `window.__TASTY__` (populated by
7
+ * inline scripts emitted during SSR) and pre-populates the injector
8
+ * so island hydration skips the style pipeline entirely.
9
9
  *
10
10
  * This module is browser-safe — it does NOT import node:async_hooks.
11
11
  *
@@ -13,12 +13,7 @@ import { hydrateTastyCache } from "./hydrate.js";
13
13
  * - Automatically injected by tastyIntegration() via injectScript('before-hydration')
14
14
  * - Can be imported manually: `import '@tenphi/tasty/ssr/astro-client'`
15
15
  */
16
- if (typeof window !== "undefined") {
17
- const script = document.querySelector("script[data-tasty-cache]");
18
- if (script) try {
19
- hydrateTastyCache(JSON.parse(script.textContent));
20
- } catch {}
21
- }
16
+ if (typeof window !== "undefined") hydrateTastyClasses();
22
17
  //#endregion
23
18
 
24
19
  //# sourceMappingURL=astro-client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"astro-client.js","names":[],"sources":["../../src/ssr/astro-client.ts"],"sourcesContent":["/**\n * Client-side cache hydration for Astro islands.\n *\n * Reads the SSR cache state from <script data-tasty-cache> and\n * pre-populates the injector so island hydration skips the style\n * pipeline entirely.\n *\n * This module is browser-safe — it does NOT import node:async_hooks.\n *\n * Usage:\n * - Automatically injected by tastyIntegration() via injectScript('before-hydration')\n * - Can be imported manually: `import '@tenphi/tasty/ssr/astro-client'`\n */\n\nimport { hydrateTastyCache } from './hydrate';\n\nif (typeof window !== 'undefined') {\n const script = document.querySelector('script[data-tasty-cache]');\n if (script) {\n try {\n const state = JSON.parse(script.textContent!);\n hydrateTastyCache(state);\n } catch {\n // Ignore malformed cache state\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAI,OAAO,WAAW,aAAa;CACjC,MAAM,SAAS,SAAS,cAAc,2BAA2B;AACjE,KAAI,OACF,KAAI;AAEF,oBADc,KAAK,MAAM,OAAO,YAAa,CACrB;SAClB"}
1
+ {"version":3,"file":"astro-client.js","names":[],"sources":["../../src/ssr/astro-client.ts"],"sourcesContent":["/**\n * Client-side cache hydration for Astro islands.\n *\n * Reads the class name list from `window.__TASTY__` (populated by\n * inline scripts emitted during SSR) and pre-populates the injector\n * so island hydration skips the style pipeline entirely.\n *\n * This module is browser-safe — it does NOT import node:async_hooks.\n *\n * Usage:\n * - Automatically injected by tastyIntegration() via injectScript('before-hydration')\n * - Can be imported manually: `import '@tenphi/tasty/ssr/astro-client'`\n */\n\nimport { hydrateTastyClasses } from './hydrate';\n\nif (typeof window !== 'undefined') {\n hydrateTastyClasses();\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,IAAI,OAAO,WAAW,YACpB,sBAAqB"}
@@ -10,8 +10,10 @@
10
10
  */
11
11
  interface TastyMiddlewareOptions {
12
12
  /**
13
- * Whether to embed the cache state script for client hydration.
14
- * Set to false to skip cache transfer. Default: true.
13
+ * Whether to embed the class-list script for client hydration.
14
+ * Set to false to skip class transfer (e.g. for CSP restrictions).
15
+ * Without it, client components may re-inject CSS that already exists
16
+ * in server-rendered `<style>` tags. Default: true.
15
17
  */
16
18
  transferCache?: boolean;
17
19
  }
package/dist/ssr/astro.js CHANGED
@@ -79,8 +79,8 @@ function tastyMiddleware(options) {
79
79
  const styleTag = `<style data-tasty-ssr${nonceAttr}>${css}</style>`;
80
80
  let cacheTag = "";
81
81
  if (transferCache) {
82
- const cacheState = collector.getCacheState();
83
- if (Object.keys(cacheState.entries).length > 0) cacheTag = `<script data-tasty-cache type="application/json"${nonceAttr}>${JSON.stringify(cacheState)}<\/script>`;
82
+ const classNames = collector.getRenderedClassNames();
83
+ if (classNames.length > 0) cacheTag = `<script${nonceAttr}>(window.__TASTY__=window.__TASTY__||[]).push(${classNames.map((n) => `"${n}"`).join(",")})<\/script>`;
84
84
  }
85
85
  const injection = styleTag + cacheTag;
86
86
  const idx = html.indexOf("</head>");