<?php
/**
 * CssSelector
 *
 * Allow to navigate a DOM with CSS selector.
 *
 * based on getElementsBySelector version 0.4 - Simon Willison, 2003-03-25
 * http://simon.incutio.com/archive/2003/03/25/getElementsBySelector
 *
 * derived from sfDomCssSelector Id 3053 (Symfony version 1.0.2) - Fabien Potencier, 2006-12-16
 * http://www.symfony-project.com/api/symfony/util/sfDomCssSelector.html
 *
 * @author Perrick Penet <perrick@noparking.net>
 *
 * @param DomDocument $dom
 */
class CssSelector
{
    protected $dom;

    public function __construct($dom)
    {
        $this->dom = $dom;
    }

    public function getTexts($selector)
    {
        $texts = array();
        foreach ($this->getElements($selector) as $element) {
            $texts[] = $element->nodeValue;
        }

        return $texts;
    }

    public function getElements($selector)
    {
        $all_nodes = array();
        foreach ($this->tokenize_selectors($selector) as $selector) {
            $nodes = array($this->dom);
            foreach ($this->tokenize($selector) as $token) {
                $combinator = $token['combinator'];
                $token      = trim($token['name']);
                $pos        = strpos($token, '#');
                if (false !== $pos && preg_match('/^[A-Za-z0-9]*$/', substr($token, 0, $pos))) {
          $tagName           = substr($token, 0, $pos);
                    $id      = substr($token, $pos + 1);
                    $xpath   = new DomXPath($this->dom);
                    $element = $xpath->query(sprintf("//*[@id = '%s']", $id))->item(0);
                    if (!$element || ($tagName && strtolower($element->nodeName) != $tagName)) {
            return array();
                    }

          $nodes = array($element);

                    continue; // Skip to next token
                }

                $pos = strpos($token, '.');
                if (false !== $pos && preg_match('/^[A-Za-z0-9]*$/', substr($token, 0, $pos))) {
          $tagName = substr($token, 0, $pos);
                    if (!$tagName) {
                        $tagName = '*';
                    }
                    $className = substr($token, $pos + 1);

          $founds          = $this->getElementsByTagName($nodes, $tagName, $combinator);
                    $nodes = array();
                    foreach ($founds as $found) {
                        if (preg_match('/\b' . $className . '\b/', $found->getAttribute('class'))) {
                            $nodes[] = $found;
                        }
                    }

                    continue; // Skip to next token
                }

        if (preg_match('/^(\w*)(\[.+\])$/', $token, $matches)) {
            $tagName = $matches[1] ? $matches[1] : '*';
            preg_match_all('/
            \[
              (\w+)                 # attribute
              ([=~\|\^\$\*]?)       # modifier (optional)
              =?                    # equal (optional)
              (
                "([^"]*)"           # quoted value (optional)
                |
                ([^\]]*)            # non quoted value (optional)
              )
            \]
          /x', $matches[2], $matches, PREG_SET_ORDER);

          $founds  = $this->getElementsByTagName($nodes, $tagName, $combinator);
            $nodes = array();
            foreach ($founds as $found) {
                $ok = false;
                foreach ($matches as $match) {
                    $attrName     = $match[1];
                    $attrOperator = $match[2];
                    $attrValue    = $match[4];

                    switch ($attrOperator) {
                case '=': // Equality
                  $ok = $found->getAttribute($attrName) == $attrValue;
                  break;
                case '~': // Match one of space seperated words
                  $ok = preg_match('/\b' . preg_quote($attrValue, '/') . '\b/', $found->getAttribute($attrName));
                  break;
                case '|': // Match start with value followed by optional hyphen
                  $ok = preg_match('/^' . preg_quote($attrValue, '/') . '-?/', $found->getAttribute($attrName));
                  break;
                case '^': // Match starts with value
                  $ok = 0 === strpos($found->getAttribute($attrName), strval($attrValue));
                  break;
                case '$': // Match ends with value
                  $ok = $attrValue == substr($found->getAttribute($attrName), -strlen($attrValue));
                  break;
                case '*': // Match ends with value
                  $ok = false !== strpos($found->getAttribute($attrName), strval($attrValue));
                  break;
                default :
                  $ok = $found->hasAttribute($attrName);
              }

                    if (false == $ok) {
                        break;
                    }
                }

                if ($ok) {
                    $nodes[] = $found;
                }
            }

            continue; // Skip to next token
        }

                if (preg_match('/^(\w*)(:first-child)$/', $token, $matches)) {
                    $token      = $matches[1] ? $matches[1] : '*';
                    $combinator = $matches[2] ? $matches[2] : '';
                }

        $nodes = $this->getElementsByTagName($nodes, $token, $combinator);
            }

            foreach ($nodes as $node) {
                if (!$node->getAttribute('sf_matched')) {
                    $node->setAttribute('sf_matched', true);
                    $all_nodes[] = $node;
                }
            }
        }

        foreach ($all_nodes as $node) {
            $node->removeAttribute('sf_matched');
        }

        return $all_nodes;
    }

    protected function getElementsByTagName($nodes, $tagName, $combinator = ' ')
    {
        $founds = array();
        foreach ($nodes as $node) {
            switch ($combinator) {
        case ' ':
          foreach ($node->getElementsByTagName($tagName) as $element) {
              $founds[] = $element;
          }
          break;
        case '>':
          foreach ($node->childNodes as $element) {
              if ($tagName == $element->nodeName) {
                  $founds[] = $element;
              }
          }
          break;
        case '+':
            $element = $node->nextSibling;
            if (isset($element->nodeName) and $element->nodeName == '#text') {
                $element = $element->nextSibling;
            }
            if ($element && $tagName == $element->nodeName) {
                $founds[] = $element;
            }
            break;
        case ':first-child':
          foreach ($node->getElementsByTagName($tagName) as $element) {
              if (count($founds) == 0) {
                  $founds[] = $element;
              }
          }
          break;
      }
        }

        return $founds;
    }

    protected function tokenize_selectors($selector)
    {
    $tokens     = array();
        $quoted = false;
        $token  = '';
        for ($i = 0, $max = strlen($selector); $i < $max; $i++) {
            if (',' == $selector[$i] && !$quoted) {
                $tokens[] = trim($token);
                $token    = '';
            } elseif ('"' == $selector[$i]) {
                $token .= $selector[$i];
                $quoted = $quoted ? false : true;
            } else {
                $token .= $selector[$i];
            }
        }
        if ($token) {
            $tokens[] = trim($token);
        }

        return $tokens;
    }

    protected function tokenize($selector)
    {
    $tokens          = array();
        $combinators = array(' ', '>', '+');
        $quoted      = false;
        $token       = array('combinator' => ' ', 'name' => '');
        for ($i = 0, $max = strlen($selector); $i < $max; $i++) {
            if (in_array($selector[$i], $combinators) && !$quoted) {
        $combinator = $selector[$i];
                while (in_array($selector[$i + 1], $combinators)) {
                    if (' ' != $selector[++$i]) {
                        $combinator = $selector[$i];
                    }
                }

                $tokens[] = $token;
                $token    = array('combinator' => $combinator, 'name' => '');
            } elseif ('"' == $selector[$i]) {
                $token['name'] .= $selector[$i];
                $quoted = $quoted ? false : true;
            } else {
                $token['name'] .= $selector[$i];
            }
        }
        if ($token['name']) {
            $tokens[] = $token;
        }

        return $tokens;
    }
}
