Skip to content

Commit 9591bdc

Browse files
committed
selector-unify() implementation trying to get the spirit of the reference implementation #310
1 parent ae7d9f6 commit 9591bdc

1 file changed

Lines changed: 230 additions & 0 deletions

File tree

src/Compiler.php

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6309,6 +6309,236 @@ protected function libSelectorParse($args)
63096309
return $this->formatOutputSelector($selectors);
63106310
}
63116311

6312+
protected static $libSelectorUnify = ['selectors1', 'selectors2'];
6313+
protected function libSelectorUnify($args)
6314+
{
6315+
list($selectors1, $selectors2) = $args;
6316+
$selectors1 = $this->getSelectorArg($selectors1);
6317+
$selectors2 = $this->getSelectorArg($selectors2);
6318+
6319+
if (! $selectors1 || ! $selectors2) {
6320+
$this->throwError("selector-unify() invalid arguments");
6321+
}
6322+
6323+
// only consider the first compound of each
6324+
$compound1 = reset($selectors1);
6325+
$compound2 = reset($selectors2);
6326+
6327+
// unify them and that's it
6328+
$unified = $this->unifyCompoundSelectors($compound1, $compound2);
6329+
6330+
return $this->formatOutputSelector($unified);
6331+
}
6332+
6333+
/**
6334+
* The selector-unify magic as its best
6335+
* (at least works as expected on test cases)
6336+
*
6337+
* @param array $compound1
6338+
* @param array $compound2
6339+
* @return array|mixed
6340+
*/
6341+
protected function unifyCompoundSelectors($compound1, $compound2)
6342+
{
6343+
6344+
if (!count($compound1)) {
6345+
return $compound2;
6346+
}
6347+
if (!count($compound2)) {
6348+
return $compound1;
6349+
}
6350+
6351+
// check that last part are compatible
6352+
$lastPart1 = array_pop($compound1);
6353+
$lastPart2 = array_pop($compound2);
6354+
$last = $this->mergeParts($lastPart1, $lastPart2);
6355+
if (!$last) {
6356+
return [[]];
6357+
}
6358+
$unifiedCompound = [$last];
6359+
$unifiedSelectors = [$unifiedCompound];
6360+
6361+
// do the rest
6362+
while (count($compound1) || count($compound2)) {
6363+
$part1 = end($compound1);
6364+
$part2 = end($compound2);
6365+
if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
6366+
list($compound2, $part2, $after2) = $match2;
6367+
if ($after2) {
6368+
$unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
6369+
}
6370+
$c = $this->mergeParts($part1, $part2);
6371+
$unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
6372+
$part1 = $part2 = null;
6373+
array_pop($compound1);
6374+
}
6375+
if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
6376+
list($compound1, $part1, $after1) = $match1;
6377+
if ($after1) {
6378+
$unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
6379+
}
6380+
$c = $this->mergeParts($part2, $part1);
6381+
$unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
6382+
$part1 = $part2 = null;
6383+
array_pop($compound2);
6384+
}
6385+
$new = [];
6386+
if ($part1 && $part2) {
6387+
array_pop($compound1);
6388+
array_pop($compound2);
6389+
$s = $this->prependSelectors($unifiedSelectors, [$part2]);
6390+
$new = array_merge($new, $this->prependSelectors($s, [$part1]));
6391+
$s = $this->prependSelectors($unifiedSelectors, [$part1]);
6392+
$new = array_merge($new, $this->prependSelectors($s, [$part2]));
6393+
} elseif ($part1) {
6394+
array_pop($compound1);
6395+
$new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
6396+
} elseif ($part2) {
6397+
array_pop($compound2);
6398+
$new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
6399+
}
6400+
if ($new) {
6401+
$unifiedSelectors = $new;
6402+
}
6403+
}
6404+
6405+
return $unifiedSelectors;
6406+
}
6407+
6408+
/**
6409+
* Prepend each selector from $selectors with $parts
6410+
* @param $selectors
6411+
* @param $parts
6412+
* @return array
6413+
*/
6414+
protected function prependSelectors($selectors, $parts)
6415+
{
6416+
$new = [];
6417+
foreach ($selectors as $compoundSelector) {
6418+
array_unshift($compoundSelector, $parts);
6419+
$new[] = $compoundSelector;
6420+
}
6421+
return $new;
6422+
}
6423+
6424+
/**
6425+
* Try to find a matching part in a compound:
6426+
* - with same html tag name
6427+
* - with some class or id or something in common
6428+
* @param array $part
6429+
* @param array $compound
6430+
* @return array|bool
6431+
*/
6432+
protected function matchPartInCompound($part, $compound)
6433+
{
6434+
$partTag = $this->findTagName($part);
6435+
$before = $compound;
6436+
$after = [];
6437+
// try to find a match by tag name first
6438+
while (count($before)) {
6439+
$p = array_pop($before);
6440+
if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
6441+
return [$before, $p, $after];
6442+
}
6443+
$after[] = $p;
6444+
}
6445+
// try again matching a non empty intersection and a compatible tagname
6446+
$before = $compound;
6447+
$after = [];
6448+
while (count($before)) {
6449+
$p = array_pop($before);
6450+
if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
6451+
if (count(array_intersect($part, $p))) {
6452+
return [$before, $p, $after];
6453+
}
6454+
}
6455+
$after[] = $p;
6456+
}
6457+
6458+
return false;
6459+
}
6460+
6461+
/**
6462+
* Merge two part list taking care that
6463+
* - the html tag is coming first - if any
6464+
* - the :something are coming last
6465+
*
6466+
* @param $parts1
6467+
* @param $parts2
6468+
* @return array
6469+
*/
6470+
protected function mergeParts($parts1, $parts2)
6471+
{
6472+
$tag1 = $this->findTagName($parts1);
6473+
$tag2 = $this->findTagName($parts2);
6474+
$tag = $this->checkCompatibleTags($tag1, $tag2);
6475+
// not compatible tags
6476+
if ($tag === false) {
6477+
return [];
6478+
}
6479+
6480+
if ($tag) {
6481+
if ($tag1) {
6482+
$parts1 = array_diff($parts1, [$tag1]);
6483+
}
6484+
if ($tag2) {
6485+
$parts2 = array_diff($parts2, [$tag2]);
6486+
}
6487+
}
6488+
6489+
$mergedParts = array_merge($parts1, $parts2);
6490+
$mergedOrderedParts = [];
6491+
foreach ($mergedParts as $part) {
6492+
if (strpos($part, ':') === 0) {
6493+
$mergedOrderedParts[] = $part;
6494+
}
6495+
}
6496+
$mergedParts = array_diff($mergedParts, $mergedOrderedParts);
6497+
$mergedParts = array_merge($mergedParts, $mergedOrderedParts);
6498+
if ($tag) {
6499+
array_unshift($mergedParts, $tag);
6500+
}
6501+
return $mergedParts;
6502+
}
6503+
6504+
/**
6505+
* Check the compatibility between two tag names:
6506+
* if both are defined they should be identical or one has to be '*'
6507+
*
6508+
* @param $tag1
6509+
* @param $tag2
6510+
* @return array|bool
6511+
*/
6512+
protected function checkCompatibleTags($tag1, $tag2)
6513+
{
6514+
$tags = [$tag1, $tag2];
6515+
$tags = array_unique($tags);
6516+
$tags = array_filter($tags);
6517+
if (count($tags)>1) {
6518+
$tags = array_diff($tags, ['*']);
6519+
}
6520+
// not compatible nodes
6521+
if (count($tags)>1) {
6522+
return false;
6523+
}
6524+
return $tags;
6525+
}
6526+
6527+
/**
6528+
* Find the html tag name in a selector parts list
6529+
* @param array $parts
6530+
* @return mixed|string
6531+
*/
6532+
protected function findTagName($parts)
6533+
{
6534+
foreach ($parts as $part) {
6535+
if (! preg_match('/^[\[.:#%_-]/', $part)) {
6536+
return $part;
6537+
}
6538+
}
6539+
return '';
6540+
}
6541+
63126542
protected static $libSimpleSelectors = ['selector'];
63136543
protected function libSimpleSelectors($args)
63146544
{

0 commit comments

Comments
 (0)