%PDF- %PDF-
Direktori : /home/q/g/b/qgbqkvz/www/wp-content/plugins/wp-scss/scssphp/src/Extend/ |
Current File : /home/q/g/b/qgbqkvz/www/wp-content/plugins/wp-scss/scssphp/src/Extend/ExtendUtil.php |
<?php /** * SCSSPHP * * @copyright 2012-2020 Leaf Corcoran * * @license http://opensource.org/licenses/MIT MIT * * @link http://scssphp.github.io/scssphp */ namespace ScssPhp\ScssPhp\Extend; use ScssPhp\ScssPhp\Ast\Selector\Combinator; use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector; use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent; use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector; use ScssPhp\ScssPhp\Ast\Selector\IDSelector; use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector; use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector; use ScssPhp\ScssPhp\Ast\Selector\QualifiedName; use ScssPhp\ScssPhp\Ast\Selector\SelectorList; use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector; use ScssPhp\ScssPhp\Ast\Selector\TypeSelector; use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector; use ScssPhp\ScssPhp\Util\EquatableUtil; use ScssPhp\ScssPhp\Util\ListUtil; /** * @internal */ final class ExtendUtil { /** * Pseudo-selectors that can only meaningfully appear in the first component of * a complex selector. */ private const ROOTISH_PSEUDO_CLASSES = ['root', 'scope', 'host', 'host-context']; /** * Returns the contents of a {@see SelectorList} that matches only elements that are * matched by every complex selector in $complexes. * * If no such list can be produced, returns `null`. * * @param list<ComplexSelector> $complexes * * @return list<ComplexSelector>|null */ public static function unifyComplex(array $complexes): ?array { if (\count($complexes) === 1) { return $complexes; } $unifiedBase = null; $leadingCombinator = null; $trailingCombinator = null; foreach ($complexes as $complex) { if ($complex->isUseless()) { return null; } if (\count($complex->getComponents()) === 1 && \count($complex->getLeadingCombinators()) !== 0) { $newLeadingCombinator = \count($complex->getLeadingCombinators()) === 1 ? $complex->getLeadingCombinators()[0] : null; if ($leadingCombinator !== null && $newLeadingCombinator !== $leadingCombinator) { return null; } $leadingCombinator = $newLeadingCombinator; } $base = $complex->getLastComponent(); if (\count($base->getCombinators()) !== 0) { $newTrailingCombinator = \count($base->getCombinators()) === 1 ? $base->getCombinators()[0] : null; if ($trailingCombinator !== null && $newTrailingCombinator !== $trailingCombinator) { return null; } $trailingCombinator = $newTrailingCombinator; } if ($unifiedBase === null) { $unifiedBase = $base->getSelector()->getComponents(); } else { foreach ($base->getSelector()->getComponents() as $simple) { $unifiedBase = $simple->unify($unifiedBase); if ($unifiedBase === null) { return null; } } } } $withoutBases = []; $hasLineBreak = false; foreach ($complexes as $complex) { if (\count($complex->getComponents()) > 1) { $withoutBases[] = new ComplexSelector($complex->getLeadingCombinators(), array_slice($complex->getComponents(), 0, \count($complex->getComponents()) - 1), $complex->getLineBreak()); } if ($complex->getLineBreak()) { $hasLineBreak = true; } } \assert($unifiedBase !== null); $base = new ComplexSelector( $leadingCombinator === null ? [] : [$leadingCombinator], [new ComplexSelectorComponent(new CompoundSelector($unifiedBase), $trailingCombinator === null ? [] : [$trailingCombinator])], $hasLineBreak ); return self::weave($withoutBases === [] ? [$base] : array_merge(ListUtil::exceptLast($withoutBases), [ListUtil::last($withoutBases)->concatenate($base)])); } /** * Returns a {@see CompoundSelector} that matches only elements that are matched by * both $compound1 and $compound2. * * If no such selector can be produced, returns `null`. * * @param list<SimpleSelector> $compound1 * @param list<SimpleSelector> $compound2 * * @return CompoundSelector|null */ public static function unifyCompound(array $compound1, array $compound2): ?CompoundSelector { $result = $compound2; foreach ($compound1 as $simple) { $unified = $simple->unify($result); if ($unified === null) { return null; } $result = $unified; } return new CompoundSelector($result); } /** * Returns a {@see SimpleSelector} that matches only elements that are matched by * both $selector1 and $selector2, which must both be either * {@see UniversalSelector}s or {@see TypeSelector}s. * * If no such selector can be produced, returns `null`. */ public static function unifyUniversalAndElement(SimpleSelector $selector1, SimpleSelector $selector2): ?SimpleSelector { $name1 = null; if ($selector1 instanceof UniversalSelector) { $namespace1 = $selector1->getNamespace(); } elseif ($selector1 instanceof TypeSelector) { $namespace1 = $selector1->getName()->getNamespace(); $name1 = $selector1->getName()->getName(); } else { throw new \InvalidArgumentException('selector1 must be a UniversalSelector or a TypeSelector'); } $name2 = null; if ($selector2 instanceof UniversalSelector) { $namespace2 = $selector2->getNamespace(); } elseif ($selector2 instanceof TypeSelector) { $namespace2 = $selector2->getName()->getNamespace(); $name2 = $selector2->getName()->getName(); } else { throw new \InvalidArgumentException('selector2 must be a UniversalSelector or a TypeSelector'); } if ($namespace1 === $namespace2 || $namespace2 === '*') { $namespace = $namespace1; } elseif ($namespace1 === '*') { $namespace = $namespace2; } else { return null; } if ($name1 === $name2 || $name2 === null) { $name = $name1; } elseif ($name1 === null) { $name = $name2; } else { return null; } if ($name === null) { return new UniversalSelector($namespace); } return new TypeSelector(new QualifiedName($name, $namespace)); } /** * Expands "parenthesized selectors" in $complexes. * * That is, if we have `.A .B {@extend .C}` and `.D .C {...}`, this * conceptually expands into `.D .C, .D (.A .B)`, and this function translates * `.D (.A .B)` into `.D .A .B, .A .D .B`. For thoroughness, `.A.D .B` would * also be required, but including merged selectors results in exponential * output for very little gain. * * The selector `.D (.A .B)` is represented as the list `[.D, .A .B]`. * * If $forceLineBreak is `true`, this will mark all returned complex selectors * as having line breaks. * * @param list<ComplexSelector> $complexes * * @return list<ComplexSelector> */ public static function weave(array $complexes, bool $forceLineBreak = false): array { if (\count($complexes) === 1) { $complex = $complexes[0]; if (!$forceLineBreak || $complex->getLineBreak()) { return $complexes; } return [ new ComplexSelector($complex->getLeadingCombinators(), $complex->getComponents(), true), ]; } $prefixes = [$complexes[0]]; foreach (array_slice($complexes, 1) as $complex) { $target = ListUtil::last($complex->getComponents()); if (\count($complex->getComponents()) === 1) { foreach ($prefixes as $i => $prefix) { $prefixes[$i] = $prefix->concatenate($complex, $forceLineBreak); } continue; } $newPrefixes = []; foreach ($prefixes as $prefix) { foreach (self::weaveParents($prefix, $complex) ?? [] as $parentPrefix) { $newPrefixes[] = $parentPrefix->withAdditionalComponent($target, $forceLineBreak); } } $prefixes = $newPrefixes; } return $prefixes; } /** * Interweaves $prefix's components with $base's components _other than * the last_. * * Returns all possible orderings of the selectors in the inputs (including * using unification) that maintain the relative ordering of the input. For * example, given `.foo .bar` and `.baz .bang div`, this would return `.foo * .bar .baz .bang div`, `.foo .bar.baz .bang div`, `.foo .baz .bar .bang div`, * `.foo .baz .bar.bang div`, `.foo .baz .bang .bar div`, and so on until `.baz * .bang .foo .bar div`. * * Semantically, for selectors `P` and `C`, this returns all selectors `PC_i` * such that the union over all `i` of elements matched by `PC_i` is identical * to the intersection of all elements matched by `C` and all descendants of * elements matched by `P`. Some `PC_i` are elided to reduce the size of the * output. * * Returns `null` if this intersection is empty. * * @param ComplexSelector $prefix * @param ComplexSelector $base * * @return list<ComplexSelector>|null */ private static function weaveParents(ComplexSelector $prefix, ComplexSelector $base): ?array { $leadingCombinators = self::mergeLeadingCombinators($prefix->getLeadingCombinators(), $base->getLeadingCombinators()); if ($leadingCombinators === null) { return null; } // Make queues of _only_ the parent selectors. The prefix only contains // parents, but the complex selector has a target that we don't want to weave // in. $queue1 = $prefix->getComponents(); $queue2 = ListUtil::exceptLast($base->getComponents()); $finalCombinators = self::mergeTrailingCombinators($queue1, $queue2); if ($finalCombinators === null) { return null; } // Make sure all selectors that are required to be at the root are unified // with one another. $rootish1 = self::firstIfRootish($queue1); $rootish2 = self::firstIfRootish($queue2); if ($rootish1 !== null && $rootish2 !== null) { $rootish = self::unifyCompound($rootish1->getSelector()->getComponents(), $rootish2->getSelector()->getComponents()); if ($rootish === null) { return null; } array_unshift($queue1, new ComplexSelectorComponent($rootish, $rootish1->getCombinators())); array_unshift($queue2, new ComplexSelectorComponent($rootish, $rootish2->getCombinators())); } elseif ($rootish1 !== null || $rootish2 !== null) { // If there's only one rootish selector, it should only appear in the first // position of the resulting selector. We can ensure that happens by adding // it to the beginning of _both_ queues. $rootish = $rootish1 ?? $rootish2; array_unshift($queue1, $rootish); array_unshift($queue2, $rootish); } $groups1 = self::groupSelectors($queue1); $groups2 = self::groupSelectors($queue2); /** @phpstan-var list<list<ComplexSelectorComponent>> $lcs */ $lcs = ListUtil::longestCommonSubsequence($groups2, $groups1, function ($group1, $group2) { if (EquatableUtil::listEquals($group1, $group2)) { return $group1; } if (self::complexIsParentSuperselector($group1, $group2)) { return $group2; } if (self::complexIsParentSuperselector($group2, $group1)) { return $group1; } if (!self::mustUnify($group1, $group2)) { return null; } $unified = self::unifyComplex([new ComplexSelector([], $group1), new ComplexSelector([], $group2)]); if ($unified === null) { return null; } if (\count($unified) > 1) { return null; } return $unified[0]->getComponents(); }); $choices = []; foreach ($lcs as $group) { $newChoice = []; /** @var list<list<list<ComplexSelectorComponent>>> $chunks */ $chunks = self::chunks($groups1, $groups2, function ($sequence) use ($group) { return self::complexIsParentSuperselector($sequence[0], $group); }); foreach ($chunks as $chunk) { $flattened = []; foreach ($chunk as $chunkGroup) { $flattened = array_merge($flattened, $chunkGroup); } $newChoice[] = $flattened; } /** @var list<list<ComplexSelectorComponent>> $groups1 */ /** @var list<list<ComplexSelectorComponent>> $groups2 */ $choices[] = $newChoice; $choices[] = [$group]; array_shift($groups1); array_shift($groups2); } $newChoice = []; /** @var list<list<list<ComplexSelectorComponent>>> $chunks */ $chunks = self::chunks($groups1, $groups2, function ($sequence) { return count($sequence) === 0; }); foreach ($chunks as $chunk) { $flattened = []; foreach ($chunk as $chunkGroup) { $flattened = array_merge($flattened, $chunkGroup); } $newChoice[] = $flattened; } $choices[] = $newChoice; foreach ($finalCombinators as $finalCombinator) { $choices[] = $finalCombinator; } $choices = array_filter($choices, function ($choice) { return $choice !== []; }); $paths = self::paths($choices); return array_map(function (array $path) use ($leadingCombinators, $prefix, $base) { $result = []; foreach ($path as $group) { $result = array_merge($result, $group); } return new ComplexSelector($leadingCombinators, $result, $prefix->getLineBreak() || $base->getLineBreak()); }, $paths); } /** * If the first element of $queue has a `:root` selector, removes and returns * that element. * * @param list<ComplexSelectorComponent> $queue * * @return ComplexSelectorComponent|null */ private static function firstIfRootish(array &$queue): ?ComplexSelectorComponent { if (empty($queue)) { return null; } $first = $queue[0]; foreach ($first->getSelector()->getComponents() as $simple) { if ($simple instanceof PseudoSelector && $simple->isClass() && \in_array($simple->getNormalizedName(), self::ROOTISH_PSEUDO_CLASSES, true)) { array_shift($queue); return $first; } } return null; } /** * Returns a leading combinator list that's compatible with both $combinators1 * and $combinators2. * * Returns `null` if the combinator lists can't be unified. * * @param list<string>|null $combinators1 * @param list<string>|null $combinators2 * * @return list<string>|null * * @phpstan-param list<Combinator::*> $combinators1 * @phpstan-param list<Combinator::*> $combinators2 * * @phpstan-return list<Combinator::*>|null */ private static function mergeLeadingCombinators(?array $combinators1, ?array $combinators2): ?array { if ($combinators1 === null) { return null; } if ($combinators2 === null) { return null; } if (\count($combinators1) > 1) { return null; } if (\count($combinators2) > 1) { return null; } if (\count($combinators1) === 0) { return $combinators2; } if (\count($combinators2) === 0) { return $combinators1; } return $combinators1 === $combinators2 ? $combinators1 : null; } /** * Extracts trailing {@see ComplexSelectorComponent}s with trailing combinators from * $components1 and $components2 and merges them together into a single list. * * Each element in the returned list is a set of choices for a particular * position in a complex selector. Each choice is the contents of a complex * selector, which is to say a list of complex selector components. The union * of each path through these choices will match the full set of necessary * elements. * * If there are no combinators to be merged, returns an empty list. If the * sequences can't be merged, returns `null`. * * @param list<ComplexSelectorComponent> $components1 * @param list<ComplexSelectorComponent> $components2 * @param list<list<list<ComplexSelectorComponent>>> $result * * @return list<list<list<ComplexSelectorComponent>>>|null */ private static function mergeTrailingCombinators(array &$components1, array &$components2, array $result = []): ?array { $combinators1 = \count($components1) === 0 ? [] : ListUtil::last($components1)->getCombinators(); $combinators2 = \count($components2) === 0 ? [] : ListUtil::last($components2)->getCombinators(); if (\count($combinators1) === 0 && \count($combinators2) === 0) { return $result; } if (count($combinators1) > 1 || count($combinators2) > 1) { return null; } // This code looks complicated, but it's actually just a bunch of special // cases for interactions between different combinators. $combinator1 = $combinators1[0] ?? null; $combinator2 = $combinators2[0] ?? null; if ($combinator1 !== null && $combinator2 !== null) { $component1 = array_pop($components1); assert($component1 instanceof ComplexSelectorComponent); $component2 = array_pop($components2); assert($component2 instanceof ComplexSelectorComponent); if ($combinator1 === Combinator::FOLLOWING_SIBLING && $combinator2 === Combinator::FOLLOWING_SIBLING) { if ($component1->getSelector()->isSuperselector($component2->getSelector())) { array_unshift($result, [[$component2]]); } elseif ($component2->getSelector()->isSuperselector($component1->getSelector())) { array_unshift($result, [[$component1]]); } else { $choices = [ [$component1, $component2], [$component2, $component1], ]; $unified = self::unifyCompound($component1->getSelector()->getComponents(), $component2->getSelector()->getComponents()); if ($unified !== null) { $choices[] = [new ComplexSelectorComponent($unified, [Combinator::FOLLOWING_SIBLING])]; } array_unshift($result, $choices); } } elseif (($combinator1 === Combinator::FOLLOWING_SIBLING && $combinator2 === Combinator::NEXT_SIBLING) || ($combinator1 === Combinator::NEXT_SIBLING && $combinator2 === Combinator::FOLLOWING_SIBLING)) { $followingSiblingComponent = $combinator1 === Combinator::FOLLOWING_SIBLING ? $component1 : $component2; $nextSiblingComponent = $combinator1 === Combinator::FOLLOWING_SIBLING ? $component2 : $component1; if ($followingSiblingComponent->getSelector()->isSuperselector($nextSiblingComponent->getSelector())) { array_unshift($result, [[$nextSiblingComponent]]); } else { $unified = self::unifyCompound($component1->getSelector()->getComponents(), $component2->getSelector()->getComponents()); $choices = [ [$followingSiblingComponent, $nextSiblingComponent], ]; if ($unified !== null) { $choices[] = [new ComplexSelectorComponent($unified, [Combinator::NEXT_SIBLING])]; } array_unshift($result, $choices); } } elseif ($combinator1 === Combinator::CHILD && ($combinator2 === Combinator::NEXT_SIBLING || $combinator2 === Combinator::FOLLOWING_SIBLING)) { array_unshift($result, [[$component2]]); $components1[] = $component1; } elseif ($combinator2 === Combinator::CHILD && ($combinator1 === Combinator::NEXT_SIBLING || $combinator1 === Combinator::FOLLOWING_SIBLING)) { array_unshift($result, [[$component1]]); $components2[] = $component2; } elseif ($combinator1 === $combinator2) { $unified = self::unifyCompound($component1->getSelector()->getComponents(), $component2->getSelector()->getComponents()); if ($unified === null) { return null; } array_unshift($result, [[new ComplexSelectorComponent($unified, [$combinator1])]]); } else { return null; } return self::mergeTrailingCombinators($components1, $components2, $result); } if ($combinator1 !== null) { $component1 = array_pop($components1); \assert($component1 instanceof ComplexSelectorComponent); if ($combinator1 === Combinator::CHILD && \count($components2) > 0 && ListUtil::last($components2)->getSelector()->isSuperselector($component1->getSelector())) { array_pop($components2); } array_unshift($result, [[$component1]]); return self::mergeTrailingCombinators($components1, $components2, $result); } $component2 = array_pop($components2); \assert($component2 instanceof ComplexSelectorComponent); assert($combinator2 !== null); if ($combinator2 === Combinator::CHILD && \count($components1) > 0 && ListUtil::last($components1)->getSelector()->isSuperselector($component2->getSelector())) { array_pop($components1); } array_unshift($result, [[$component2]]); return self::mergeTrailingCombinators($components2, $components1, $result); } /** * Returns whether $complex1 and $complex2 need to be unified to produce a * valid combined selector. * * This is necessary when both selectors contain the same unique simple * selector, such as an ID. * * @param list<ComplexSelectorComponent> $complex1 * @param list<ComplexSelectorComponent> $complex2 * * @return bool */ private static function mustUnify(array $complex1, array $complex2): bool { $uniqueSelectors = []; foreach ($complex1 as $component) { foreach ($component->getSelector()->getComponents() as $simple) { if (self::isUnique($simple)) { $uniqueSelectors[] = $simple; } } } if (\count($uniqueSelectors) === 0) { return false; } foreach ($complex2 as $component) { foreach ($component->getSelector()->getComponents() as $simple) { if (self::isUnique($simple) && EquatableUtil::listContains($uniqueSelectors, $simple)) { return true; } } } return false; } /** * Returns whether a {@see CompoundSelector} may contain only one simple selector of * the same type as $simple. */ private static function isUnique(SimpleSelector $simple): bool { return $simple instanceof IDSelector || ($simple instanceof PseudoSelector && $simple->isElement()); } /** * Returns all orderings of initial subsequences of $queue1 and $queue2. * * The $done callback is used to determine the extent of the initial * subsequences. It's called with each queue until it returns `true`. * * This destructively removes the initial subsequences of $queue1 and * $queue2. * * For example, given `(A B C | D E)` and `(1 2 | 3 4 5)` (with `|` denoting * the boundary of the initial subsequence), this would return `[(A B C 1 2), * (1 2 A B C)]`. The queues would then contain `(D E)` and `(3 4 5)`. * * @template T * * @param list<T> $queue1 * @param list<T> $queue2 * @param callable(list<T>): bool $done * * @return list<list<T>> */ private static function chunks(array &$queue1, array &$queue2, callable $done): array { $chunk1 = []; while (!$done($queue1)) { $element = array_shift($queue1); if ($element === null) { throw new \LogicException('Cannot remove an element from an empty queue'); } $chunk1[] = $element; } $chunk2 = []; while (!$done($queue2)) { $element = array_shift($queue2); if ($element === null) { throw new \LogicException('Cannot remove an element from an empty queue'); } $chunk2[] = $element; } if (empty($chunk1) && empty($chunk2)) { return []; } if (empty($chunk1)) { return [$chunk2]; } if (empty($chunk2)) { return [$chunk1]; } return [ array_merge($chunk1, $chunk2), array_merge($chunk2, $chunk1), ]; } /** * Returns a list of all possible paths through the given lists. * * For example, given `[[1, 2], [3, 4], [5]]`, this returns: * * ``` * [[1, 3, 5], * [2, 3, 5], * [1, 4, 5], * [2, 4, 5]] * ``` * * @template T * * @param array<list<T>> $choices * * @return list<list<T>> */ public static function paths(array $choices): array { return array_reduce($choices, function (array $paths, array $choice) { $newPaths = []; foreach ($choice as $option) { foreach ($paths as $path) { $path[] = $option; $newPaths[] = $path; } } return $newPaths; }, [[]]); } /** * Returns $complex, grouped into the longest possible sub-lists such that * {@see ComplexSelectorComponent}s without combinators only appear at the end of * sub-lists. * * For example, `(A B > C D + E ~ G)` is grouped into * `[(A) (B > C) (D + E ~ G)]`. * * @param iterable<ComplexSelectorComponent> $complex * * @return list<list<ComplexSelectorComponent>> */ private static function groupSelectors(iterable $complex): array { $groups = []; $group = []; foreach ($complex as $component) { $group[] = $component; if (\count($component->getCombinators()) === 0) { $groups[] = $group; $group = []; } } if ($group !== []) { $groups[] = $group; } return $groups; } /** * Returns whether $list1 is a superselector of $list2. * * That is, whether $list1 matches every element that $list2 matches, as well * as possibly additional elements. * * @param list<ComplexSelector> $list1 * @param list<ComplexSelector> $list2 * * @return bool */ public static function listIsSuperselector(array $list1, array $list2): bool { foreach ($list2 as $complex1) { foreach ($list1 as $complex2) { if ($complex2->isSuperselector($complex1)) { continue 2; } } return false; } return true; } /** * Like {@see complexIsSuperselector}, but compares $complex1 and $complex2 as * though they shared an implicit base {@see SimpleSelector}. * * For example, `B` is not normally a superselector of `B A`, since it doesn't * match elements that match `A`. However, it *is* a parent superselector, * since `B X` is a superselector of `B A X`. * * @param list<ComplexSelectorComponent> $complex1 * @param list<ComplexSelectorComponent> $complex2 * * @return bool */ private static function complexIsParentSuperselector(array $complex1, array $complex2): bool { if (\count($complex1) > \count($complex2)) { return false; } $base = new ComplexSelectorComponent(new CompoundSelector([new PlaceholderSelector('<temp>')]), []); $complex1[] = $base; $complex2[] = $base; return self::complexIsSuperselector($complex1, $complex2); } /** * Returns whether $complex1 is a superselector of $complex2. * * That is, whether $complex1 matches every element that $complex2 matches, as well * as possibly additional elements. * * @param list<ComplexSelectorComponent> $complex1 * @param list<ComplexSelectorComponent> $complex2 * * @return bool */ public static function complexIsSuperselector(array $complex1, array $complex2): bool { // Selectors with trailing operators are neither superselectors nor // subselectors. if (\count(ListUtil::last($complex1)->getCombinators()) !== 0) { return false; } if (\count(ListUtil::last($complex2)->getCombinators()) !== 0) { return false; } $i1 = 0; $i2 = 0; while (true) { $remaining1 = \count($complex1) - $i1; $remaining2 = \count($complex2) - $i2; if ($remaining1 === 0 || $remaining2 === 0) { return false; } // More complex selectors are never superselectors of less complex ones. if ($remaining1 > $remaining2) { return false; } $component1 = $complex1[$i1]; if (\count($component1->getCombinators()) > 1) { return false; } if ($remaining1 === 1) { $parents = array_slice($complex2, $i2, -1); foreach ($parents as $parent) { if (\count($parent->getCombinators()) > 1) { return false; } } return self::compoundIsSuperselector($component1->getSelector(), ListUtil::last($complex2)->getSelector(), $parents); } // Find the first index $endOfSubselector in $complex2 such that // `complex2.sublist(i2, endOfSubselector + 1)` is a subselector of // `$component1->getSelector()`. $endOfSubselector = $i2; $parents = null; while (true) { $component2 = $complex2[$endOfSubselector]; if (\count($component2->getCombinators()) > 1) { return false; } if (self::compoundIsSuperselector($component1->getSelector(), $component2->getSelector(), $parents)) { break; } $endOfSubselector++; if ($endOfSubselector === \count($complex2) - 1) { // Stop before the superselector would encompass all of $complex2 // because we know $complex1 has more than one element, and consuming // all of $complex2 wouldn't leave anything for the rest of $complex1 // to match. return false; } $parents[] = $component2; } $component2 = $complex2[$endOfSubselector]; $combinator1 = $component1->getCombinators()[0] ?? null; $combinator2 = $component2->getCombinators()[0] ?? null; if (!self::isSupercombinator($combinator1, $combinator2)) { return false; } $i1++; $i2 = $endOfSubselector + 1; if (\count($complex1) - $i1 === 1) { if ($combinator1 === Combinator::FOLLOWING_SIBLING) { // The selector `.foo ~ .bar` is only a superselector of selectors that // *exclusively* contain subcombinators of `~`. for ($index = $i2; $index < \count($complex2) - 1; $index++) { $component = $complex2[$index]; if (!self::isSupercombinator($combinator1, $component->getCombinators()[0] ?? null)) { return false; } } } elseif ($combinator1 !== null) { // `.foo > .bar` and `.foo + bar` aren't superselectors of any selectors // with more than one combinator. if (\count($complex2) - $i2 > 1) { return false; } } } } } /** * Returns whether $combinator1 is a supercombinator of $combinator2. * * That is, whether `X $combinator1 Y` is a superselector of `X $combinator2 Y`. * * @phpstan-param Combinator::*|null $combinator1 * @phpstan-param Combinator::*|null $combinator2 */ private static function isSupercombinator(?string $combinator1, ?string $combinator2): bool { return $combinator1 === $combinator2 || ($combinator1 === null && $combinator2 === Combinator::CHILD) || ($combinator1 === Combinator::FOLLOWING_SIBLING && $combinator2 === Combinator::NEXT_SIBLING); } /** * Returns whether $compound1 is a superselector of $compound2. * * That is, whether $compound1 matches every element that $compound2 matches, as well * as possibly additional elements. * * If $parents is passed, it represents the parents of $compound2. This is * relevant for pseudo selectors with selector arguments, where we may need to * know if the parent selectors in the selector argument match $parents. * * @param CompoundSelector $compound1 * @param CompoundSelector $compound2 * @param list<ComplexSelectorComponent>|null $parents * * @return bool */ public static function compoundIsSuperselector(CompoundSelector $compound1, CompoundSelector $compound2, ?array $parents = null): bool { // Pseudo elements effectively change the target of a compound selector rather // than narrowing the set of elements to which it applies like other // selectors. As such, if either selector has a pseudo element, they both must // have the _same_ pseudo element. // // In addition, order matters when pseudo-elements are involved. The selectors // before them must $tuple1 = self::findPseudoElementIndexed($compound1); $tuple2 = self::findPseudoElementIndexed($compound2); if ($tuple1 !== null && $tuple2 !== null) { return $tuple1[0]->isSuperselector($tuple2[0]) && self::compoundComponentsIsSuperselector( array_slice($compound1->getComponents(), 0, $tuple1[1]), array_slice($compound2->getComponents(), 0, $tuple2[1]), $parents ) && self::compoundComponentsIsSuperselector( array_slice($compound1->getComponents(), $tuple1[1] + 1), array_slice($compound2->getComponents(), $tuple2[1] + 1), $parents ); } elseif ($tuple1 !== null || $tuple2 !== null) { return false; } // Every selector in `$compound1->getComponents()` must have a matching selector in // `$compound2->getComponents()`. foreach ($compound1->getComponents() as $simple1) { if ($simple1 instanceof PseudoSelector && $simple1->getSelector() !== null) { if (!self::selectorPseudoIsSuperselector($simple1, $compound2, $parents)) { return false; } } else { foreach ($compound2->getComponents() as $simple2) { if ($simple1->isSuperselector($simple2)) { continue 2; } } return false; } } return true; } /** * If $compound contains a pseudo-element, returns it and its index in * `$compound->getComponents()`. * * @return array{PseudoSelector, int}|null */ private static function findPseudoElementIndexed(CompoundSelector $compound): ?array { foreach ($compound->getComponents() as $i => $simple) { if ($simple instanceof PseudoSelector && $simple->isElement()) { return [$simple, $i]; } } return null; } /** * Like {@see compoundIsSuperselector} but operates on the underlying lists of * simple selectors. * * @param list<SimpleSelector> $compound1 * @param list<SimpleSelector> $compound2 * @param list<ComplexSelectorComponent>|null $parents * * @return bool */ private static function compoundComponentsIsSuperselector(array $compound1, array $compound2, ?array $parents = null): bool { if (\count($compound1) === 0) { return true; } if (\count($compound2) === 0) { $compound2 = [new UniversalSelector('*')]; } return self::compoundIsSuperselector(new CompoundSelector($compound1), new CompoundSelector($compound2), $parents); } /** * Returns whether $pseudo1 is a superselector of $compound2. * * That is, whether $pseudo1 matches every element that $compound2 matches, as well * as possibly additional elements. * * This assumes that $pseudo1's `selector` argument is not `null`. * * If $parents is passed, it represents the parents of $compound2. This is * relevant for pseudo selectors with selector arguments, where we may need to * know if the parent selectors in the selector argument match $parents. * * @param list<ComplexSelectorComponent>|null $parents */ private static function selectorPseudoIsSuperselector(PseudoSelector $pseudo1, CompoundSelector $compound2, ?array $parents): bool { $selector1 = $pseudo1->getSelector(); if ($selector1 === null) { throw new \InvalidArgumentException("Selector $pseudo1 must have a selector argument."); } switch ($pseudo1->getNormalizedName()) { case 'is': case 'matches': case 'any': case 'where': $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName()); foreach ($selectors as $selector2) { if ($selector1->isSuperselector($selector2)) { return true; } } $componentWithParents = $parents; $componentWithParents[] = new ComplexSelectorComponent($compound2, []); foreach ($selector1->getComponents() as $complex1) { if (\count($complex1->getLeadingCombinators()) === 0 && self::complexIsSuperselector($complex1->getComponents(), $componentWithParents)) { return true; } } return false; case 'has': case 'host': case 'host-context': $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName()); foreach ($selectors as $selector2) { if ($selector1->isSuperselector($selector2)) { return true; } } return false; case 'slotted': $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName(), false); foreach ($selectors as $selector2) { if ($selector1->isSuperselector($selector2)) { return true; } } return false; case 'not': foreach ($selector1->getComponents() as $complex) { if ($complex->isBogus()) { return false; } foreach ($compound2->getComponents() as $simple2) { if ($simple2 instanceof TypeSelector) { foreach ($complex->getLastComponent()->getSelector()->getComponents() as $simple1) { if ($simple1 instanceof TypeSelector && !$simple1->equals($simple2)) { continue 3; } } } elseif ($simple2 instanceof IDSelector) { foreach ($complex->getLastComponent()->getSelector()->getComponents() as $simple1) { if ($simple1 instanceof IDSelector && !$simple1->equals($simple2)) { continue 3; } } } elseif ($simple2 instanceof PseudoSelector && $simple2->getName() === $pseudo1->getName()) { $selector2 = $simple2->getSelector(); if ($selector2 === null) { continue; } if (self::listIsSuperselector($selector2->getComponents(), [$complex])) { continue 2; } } } return false; } return true; case 'current': $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName()); foreach ($selectors as $selector2) { if ($selector1->equals($selector2)) { return true; } } return false; case 'nth-child': case 'nth-last-child': foreach ($compound2->getComponents() as $pseudo2) { if (!$pseudo2 instanceof PseudoSelector) { continue; } if ($pseudo2->getName() !== $pseudo1->getName()) { continue; } if ($pseudo2->getArgument() !== $pseudo1->getArgument()) { continue; } $selector2 = $pseudo2->getSelector(); if ($selector2 === null) { continue; } if ($selector1->isSuperselector($selector2)) { return true; } } return false; default: throw new \LogicException('unreachache'); } } /** * Returns all the selector arguments of pseudo selectors in $compound with * the given $name. * * @return SelectorList[] */ private static function selectorPseudoArgs(CompoundSelector $compound, string $name, bool $isClass = true): array { $selectors = []; foreach ($compound->getComponents() as $simple) { if (!$simple instanceof PseudoSelector) { continue; } if ($simple->isClass() !== $isClass || $simple->getName() !== $name) { continue; } if ($simple->getSelector() === null) { continue; } $selectors[] = $simple->getSelector(); } return $selectors; } }