@@ -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