PHP Classes
elePHPant
Icontem

File: classes/css-parser/css-parser.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Gonzalo Chumillas  >  PHP CSS Parser  >  classes/css-parser/css-parser.php  >  Download  
File: classes/css-parser/css-parser.php
Role: Class source
Content type: text/plain
Description: CSSParser class
Class: PHP CSS Parser
Get HTML document nodes matching a CSS selector
Author: By
Last change: commit
commit
Merge branch 'master' of https://github.com/cequiel/cssparser
update
Date: 1 year ago
Size: 11,560 bytes
 

Contents

Class file image Download
<?php
require_once dirname(__DIR__) . "/parser/parser.php";
require_once dirname(__DIR__) . "/css-parser/css-term.php";
require_once dirname(__DIR__) . "/css-parser/filters/css-attr-filter.php";
require_once dirname(__DIR__) . "/css-parser/filters/css-class-filter.php";
require_once dirname(__DIR__) . "/css-parser/filters/css-id-filter.php";
require_once dirname(__DIR__) . "/css-parser/filters/css-pseudo-first-child-filter.php";
require_once dirname(__DIR__) . "/css-parser/filters/css-pseudo-nth-child-filter.php";

/**
 * BNF Grammar:
 * 
 * <expression>                ::= <term> {"," <term>}
 * <term>                      ::= <factor> {<factor>}
 * <factor>                    ::= <operator> <element> | <element>
 * <element>                   ::= ("*" | <identifier>) {<filter>}
 * <filter>                    ::= <class-filter> | <id-filter> | <attr->filter> | <pseudo-filter>
 * <class-filter>              ::= "." <identifier>
 * <id-filter>                 ::= "#" <identifier>
 * <attr-filter>               ::= "[" <identifier> [<attr-operator> <value>] "]"
 * <pseudo-filter>             ::= ":" (<pseudo-first-child-filter> | <pseudo-nth-child-filter>)
 * <pseudo-nth-child-filter>   ::= "nth-child" "(" <number> ")"
 * <pseudo-first-child-filter> ::= "first-child"
 * <identifier>                ::= ( "_" | <alphanum> ) { "_" | "-" | <alphanum> }
 * <attr-operator>             ::= "=" | "~="
 * <operator>                  ::= ">" | "+"
 * <value>                     ::= <quoted string> | <alphanum> {<alphanum>}
 */
 
class CSSParser extends Parser {
    /**
     * Identifier pattern.
     */
    const IDENTIFIER = "[_a-z0-9][_\-a-z0-9]*";
    
    /**
     * List of operators.
     * @var array of strings
     */
    private $operators = array(">", "+");
    
    /**
     * List of 'attribute operators'.
     * @var array of strings
     */
    private $attr_operators = array("=", "~=");
    
    /**
     * A node context.
     * @var DOMNode
     */
    private $node;
    
    /**
     * @param DOMNode $node
     * @param string $query CSS selector expression
     */
    public function __construct($node, $query) {
        $this->node = $node;
        parent::__construct($query);
    }
    
    /**
     * Is the node in array?
     * @param DOMNode $node
     * @param array(DOMNode, ...) $items
     * @return boolean
     */
    private function isNodeInArray($node, $items) {
        foreach ($items as $item) {
            if ($node->isSameNode($item)) {
                return TRUE;
            }
        }
        return FALSE;
    }
    
    /**
     * Is the next an operator?
     * <operator> ::= ">" | "+"
     * 
     * @return FALSE|array(string)
     */
    protected function operator() {
        return $this->in($this->operators);
    }
    
    /**
     * Is the next an attribute operator?
     * <attr-operator> ::= "=" | "~="
     * 
     * @return FALSE|array(string)
     */
    protected function attrOperator() {
        return $this->in(CSSAttrFilter::getOperators());
    }
    
    /**
     * Is the next an identifier?
     * <identifier> ::= ( "_" | <alphanum> ) { "_" | "-" | <alphanum> }
     * 
     * @return FALSE|array(string)
     */
    protected function identifier() {
        if (list($id) = $this->match(CSSparser::IDENTIFIER)) {
            return array($id);
        }
        return FALSE;
    }
    
    /**
     * Is the next a value?
     * <value> ::= <a quoted string> | <a list of alphanumeric characters>
     * 
     * The following examples are values:
     * 'hello'
     * "hello\"man"
     * 'hello\'man'
     * 0015blah
     * _blah_
     * 
     * @return FALSE|array(string)
     */
    protected function value() {
        if ( !(list($value) = $this->str()) && !(list($value) = $this->match(CSSParser::IDENTIFIER)) ) {
            return array($value);
        }
        return array($value);
    }
    
    /**
     * Is the next a nth-child pseudo filter?
     * <pseudo-nth-child-filter> ::= "nth-child" "(" <number> ")"
     * 
     * @return FALSE|CSSPseudoNthChildFilter
     */
    protected function pseudoNthChildFilter() {
        $position = 0;
        
        if (!$this->eq("nth-child")) {
            return FALSE;
        }
        if (!$this->eq("(")) {
            throw new ParserException($this, "Invalid expression");
        }
        if (!list($position) = $this->number()) {
            throw new ParserException($this, "Invalid position");
        }
        if (!$this->eq(")")) {
            throw new ParserException($this, "Invalid expression");
        }
        return new CSSPseudoNthChildFilter(intval($position));
    }
    
    /**
     * Is the next a first-child pseudo filter?
     * <pseudo-first-child-filter> ::= "first-child"
     * 
     * @return FALSE|CSSPseudoFirstChildFilter
     */
    protected function pseudoFirstChildFilter() {
        if (!$this->eq("first-child")) {
            return FALSE;
        }
        
        return new CSSPseudoFirstChildFilter();
    }
    
    /**
     * Is the next a speudo filter?
     * <pseudo-filter> ::= ":" (<pseudo-first-child-filter> | <pseudo-nth-child-filter>)
     * 
     * @return FALSE|CSSPseudoFilter
     */
    protected function pseudoFilter() {
        $filter = NULL;
        
        if (!$this->match("/^\:/")) {
            return FALSE;
        }
        
        if (
            (!$filter = $this->is("pseudoFirstChildFilter")) &&
            (!$filter = $this->is("pseudoNthChildFilter"))
        ) {
            throw new ParserException($this, "Invalid pseudo filter");
        }
        return $filter;
    }
    
    /**
     * Is the next an attribute filter?
     * <attr-filter> ::= "[" <identifier> (<attr-operator> <value>) "]"
     * 
     * @return FALSE|CSSAttrFilter
     */
    protected function attrFilter() {
        $attr_name = "";
        $op = "";
        $value = "";
        
        if (!$this->match("/^\[/")) {
            return FALSE;
        }
        if (!list($attr_name) = $this->is("identifier")) {
            throw new ParserException($this, "Invalid identifier");
        }
        if (list($op) = $this->is("attrOperator")) {
            if (!list($value) = $this->is("value")) {
                throw new ParserException($this, "Invalid attribute operator");
            }
        }
        if (!$this->eq("]")) {
            throw new ParserException($this, "Invalid expression");
        }
        return new CSSAttrFilter($attr_name, $op, $value);
    }
    
    /**
     * Is the next an id filter?
     * <id-filter> ::= "#" <identifier>
     * 
     * @return FALSE|CSSIdFilter
     */
    protected function idFilter() {
        $id = "";
        
        if (!$this->match("/^\#/")) {
            return FALSE;
        }
        if (!list($id) = $this->is("identifier")) {
            throw new ParserException($this, "Invalid identifier");
        }
        return new CSSIdFilter($id);
    }
    
    /**
     * Is the next a class filter?
     * <class-filter> ::= "." <identifier>
     * 
     * @return FALSE|CSSClassFilter
     */
    protected function classFilter() {
        $class_name = "";
        
        if (!$this->match("/^\./")) {
            return FALSE;
        }
        if (!list($class_name) = $this->is("identifier")) {
            throw new ParserException($this, "Invalid identifier");
        }
        return new CSSClassFilter($class_name);
    }
    
    /**
     * Is the next a filter?
     * <filter> ::= <class-filter> | <id-filter> | <attr->filter> | <pseudo-filter>
     * 
     * @return FALSE|CSSFilter
     */
    protected function filter() {
        $filter = NULL;
        
        if (
            (!$filter = $this->is("classFilter")) &&
            (!$filter = $this->is("idFilter")) &&
            (!$filter = $this->is("attrFilter")) &&
            (!$filter = $this->is("pseudoFilter"))
        ) {
            return FALSE;
        }
        return $filter;
    }
    
    /**
     * Is the next an element?
     * <element> ::= ("*" | <identifier> | <filter>) {<filter>}
     * 
     * The following example is an element:
     * div.my-class[title = hello]:nth-child(1)
     * 
     * In the above example, 'div' is the tag name and the following strings are filters:
     * .my-class       // a class filter
     * [title = hello] // an attribute filter
     * :nth-child(1)   // a pseudo filter
     * 
     * @return FALSE|CSSElement
     */
    protected function element() {
        $element = NULL;
        $filter = NULL;
        $tag_name = "*";
        
        // ignores left spaces
        $this->match("\s+");
        
        if ( (list($name) = $this->eq("*")) || (list($name) = $this->is("identifier")) ) {;
            $tag_name = $name? $name : "*";
        } else
        if (!$filter = $this->is("filter")) {
            return FALSE;
        }
        
        $element = new CSSElement($tag_name);
        
        // first filter
        if ($filter) {
            $element->addFilter($filter);
        }
        
        // additional filters
        while ($filter = $this->is("filter")) {
            $element->addFilter($filter);
        }
        
        return $element;
    }
    
    /**
     * Is the next a factor?
     * <factor> ::= <operator> <element> | <element>
     * 
     * The following example is a factor:
     * > div.my-class[title = hello]
     * 
     * In the above example, ">" is an operator and "div.my-class[title = hello]" is an element.
     * 
     * @return FALSE|CSSFactor
     */
    protected function factor() {
        $op = "";
        
        if (list($op) = $this->is("operator")) {
            if (!$element = $this->is("element")) {
                throw new ParserException($this, "Invalid expression");
            }
        } else
        if (!$element = $this->is("element")) {
            return FALSE;
        }
        
        return new CSSFactor($op, $element);
    }
    
    /**
     * Is the next a term?
     * <term> ::= <factor> {<factor>}
     * 
     * The following example is a term:
     * div > div.class h2[title = 'main-title'] + h3
     * 
     * In the above example, the following strings are factors:
     * div
     * > div.class
     * h2[title = 'main-title']
     * + h3
     * 
     * @return FALSE|CSSTerm
     */
    protected function term() {
        $factor = NULL;
        
        // first factor
        if (!$factor = $this->is("factor")) {
            return FALSE;
        }
        $term = new CSSTerm();
        $term->addFactor($factor);
        
        // additional factors
        while ($factor = $this->is("factor")) {
            $term->addFactor($factor);
        }
        
        return $term;
    }
    
    /**
     * Is the next a css selector expression?
     * <expression> ::= <term> {"," <term>}
     * 
     * The following example is an expression:
     * div > div.class div#id, div > p, pre
     * 
     * In the above example, the following strings are terms:
     * div > div.class div
     * div > p
     * pre
     * 
     * @return array(DOMElement, ...)
     */
    protected function expression() {
        $nodes = array();
        do {
            if (!$term = $this->is("term")) {
                throw new ParserException($this, "Invalid expression");
            }
            $nodes = CSSHelper::mergeNodes($nodes, $term->filter($this->node));
        } while ($this->eq(","));
        return $nodes;
    }
    
    protected function _parse() {
        return $this->is("expression");
    }
}