<?php
/**
 * Copyright (c) 2003-2009, Klaus Guenther <klaus@capitalfocus.org>
 *                          Laurent Laville <pear@laurent-laville.org>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the authors nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * PHP versions 4 and 5
 *
 * @category  HTML
 * @package   HTML_CSS
 * @author    Klaus Guenther <klaus@capitalfocus.org>
 * @author    Laurent Laville <pear@laurent-laville.org>
 * @copyright 2003-2009 Klaus Guenther, Laurent Laville
 * @license   http://www.opensource.org/licenses/bsd-license.php  New BSD License
 * @version   CVS: $Id: CSS.php,v 1.89 2009/07/03 15:52:22 farell Exp $
 * @link      http://pear.php.net/package/HTML_CSS
 * @since     File available since Release 0.2.0
 */

require_once 'HTML/Common.php';

/**#@+
 * Basic error codes
 *
 * @var        integer
 * @since      0.3.3
 */
define('HTML_CSS_ERROR_UNKNOWN', -1);
define('HTML_CSS_ERROR_INVALID_INPUT', -100);
define('HTML_CSS_ERROR_INVALID_GROUP', -101);
define('HTML_CSS_ERROR_NO_GROUP', -102);
define('HTML_CSS_ERROR_NO_ELEMENT', -103);
define('HTML_CSS_ERROR_NO_ELEMENT_PROPERTY', -104);
define('HTML_CSS_ERROR_NO_FILE', -105);
define('HTML_CSS_ERROR_WRITE_FILE', -106);
define('HTML_CSS_ERROR_INVALID_SOURCE', -107);
define('HTML_CSS_ERROR_INVALID_DEPS', -108);
define('HTML_CSS_ERROR_NO_ATRULE', -109);
/**#@-*/

/**
 * Base class for CSS definitions
 *
 * This class handles the details for creating properly
 * constructed CSS declarations.
 *
 * @category  HTML
 * @package   HTML_CSS
 * @author    Klaus Guenther <klaus@capitalfocus.org>
 * @author    Laurent Laville <pear@laurent-laville.org>
 * @copyright 2003-2009 Klaus Guenther, Laurent Laville
 * @license   http://www.opensource.org/licenses/bsd-license.php  BSD
 * @version   Release: 1.5.4
 * @link      http://pear.php.net/package/HTML_CSS
 * @since     Class available since Release 0.2.0
 */

class HTML_CSS extends HTML_Common
{
    /**
     * Options configuration list
     *
     * - xhtml :
     *    Defines whether element selectors should be automatically lowercased.
     *    Determines how parseSelectors treats the data.
     *    @see setXhtmlCompliance()
     * - tab :
     *    Sets indent string.
     *    @see setTab(), HTML_Common::setTab()
     * - filename :
     *    Name of file to be parsed.
     *    @see parseFile()
     * - cache :
     *    Determines whether the nocache headers are sent.
     *    Controls caching of the page.
     *    @see setCache()
     * - oneline :
     *    Defines whether to output all properties on one line.
     *    @see setSingleLineOutput()
     * - charset :
     *    Contains the character encoding string.
     *    @see setCharset()
     * - contentDisposition :
     *    Contains the Content-Disposition filename.
     *    @see setContentDisposition()
     * - lineEnd :
     *    Sets the line end style to Windows, Mac, Unix or a custom string.
     *    @see setLineEnd(), HTML_Common::setLineEnd()
     * - groupsfirst :
     *    Determines whether to output groups before elements.
     *    @see setOutputGroupsFirst()
     * - allowduplicates :
     *    Allow to have duplicate rules in selector. Useful for IE hack.
     *
     * @var        array
     * @since      1.4.0
     * @access     private
     * @see        __set(), __get()
     */
    var $options;

    /**
     * Contains the CSS definitions.
     *
     * @var        array
     * @since      0.2.0
     * @access     private
     */
    var $_css = array();

    /**
     * Contains "alibis" (other elements that share a definition) of an element
     * defined in CSS
     *
     * @var        array
     * @since      0.2.0
     * @access     private
     */
    var $_alibis = array();

    /**
     * Contains last assigned index for duplicate styles
     *
     * @var        array
     * @since      0.3.0
     * @access     private
     */
    var $_duplicateCounter = 0;

    /**
     * Contains grouped styles
     *
     * @var        array
     * @since      0.3.0
     * @access     private
     */
    var $_groups = array();

    /**
     * Number of CSS definition groups
     *
     * @var        int
     * @since      0.3.0
     * @access     private
     */
    var $_groupCount = 0;

    /**
     * Error message callback.
     * This will be used to generate the error message
     * from the error code.
     *
     * @var        false|string|array
     * @since      1.0.0
     * @access     private
     * @see        _initErrorStack()
     */
    var $_callback_message = false;

    /**
     * Error context callback.
     * This will be used to generate the error context for an error.
     *
     * @var        false|string|array
     * @since      1.0.0
     * @access     private
     * @see        _initErrorStack()
     */
    var $_callback_context = false;

    /**
     * Error push callback.
     * The return value will be used to determine whether to allow
     * an error to be pushed or logged.
     *
     * @var        false|string|array
     * @since      1.0.0
     * @access     private
     * @see        _initErrorStack()
     */
    var $_callback_push = false;

    /**
     * Error callback.
     * User function that decides what to do with error (display, log, ...)
     *
     * @var        false|string|array
     * @since      1.4.0
     * @access     private
     * @see        _initErrorStack()
     */
    var $_callback_error = false;

    /**
     * Error handler callback.
     * This will handle any errors raised by this package.
     *
     * @var        false|string|array
     * @since      1.0.0
     * @access     private
     * @see        _initErrorStack()
     */
    var $_callback_errorhandler = false;

    /**
     * Associative array of key-value pairs
     * that are used to specify any handler-specific settings.
     *
     * @var        array
     * @since      1.0.0
     * @access     private
     * @see        _initErrorStack()
     */
    var $_errorhandler_options = array();

    /**
     * Last error that might occured
     *
     * @var        false|mixed
     * @since      1.0.0RC2
     * @access     private
     * @see        isError(), raiseError()
     */
    var $_lastError = false;


    /**
     * Class constructor
     *
     * Class constructors :
     * Zend Engine 1 uses HTML_CSS, while Zend Engine 2 uses __construct
     *
     * @param array $attributes (optional) Pass options to the constructor.
     *                          Valid options are :
     *                           - xhtml (sets xhtml compliance),
     *                           - tab (sets indent string),
     *                           - filename (name of file to be parsed),
     *                           - cache (determines whether the nocache headers
     *                             are sent),
     *                           - oneline (whether to output each definition
     *                             on one line),
     *                           - groupsfirst (determines whether to output groups
     *                             before elements)
     *                           - allowduplicates (allow to have duplicate rules
     *                             in selector)
     * @param array $errorPrefs (optional) has to configure error handler
     *
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     */
    function HTML_CSS($attributes = array(), $errorPrefs = array())
    {
        $this->__construct($attributes, $errorPrefs);
    }

    /**
     * Class constructor
     *
     * Class constructors :
     * Zend Engine 1 uses HTML_CSS, while Zend Engine 2 uses __construct
     *
     * @param array $attributes (optional) Pass options to the constructor.
     *                          Valid options are :
     *                           - xhtml (sets xhtml compliance),
     *                           - tab (sets indent string),
     *                           - filename (name of file to be parsed),
     *                           - cache (determines whether the nocache headers
     *                             are sent),
     *                           - oneline (whether to output each definition
     *                             on one line),
     *                           - groupsfirst (determines whether to output groups
     *                             before elements)
     *                           - allowduplicates (allow to have duplicate rules
     *                             in selector)
     * @param array $errorPrefs (optional) has to configure error handler
     *
     * @since      version 1.4.0 (2007-12-13)
     * @access     protected
     */
    function __construct($attributes = array(), $errorPrefs = array())
    {
        $this->_initErrorStack($errorPrefs);

        if (!is_array($attributes)) {
            $attributes = array($attributes);
        }
        if ($attributes) {
            $attributes = $this->_parseAttributes($attributes);
        }

        $tab = '  ';
        $eol = strtolower(substr(PHP_OS, 0, 3)) == 'win' ? "\r\n" : "\n";

        // default options
        $this->options = array('xhtml' => true, 'tab' => $tab, 'cache' => true,
            'oneline' => false, 'charset' => 'iso-8859-1',
            'contentDisposition' => false, 'lineEnd' => $eol,
            'groupsfirst' => true, 'allowduplicates' => false);
        // and options that come directly from HTML_Common
        $this->setTab($tab);
        $this->setLineEnd($eol);

        // apply user options
        foreach ($attributes as $opt => $val) {
            $this->__set($opt, $val);
        }
    }

    /**
     * Return the current API version
     *
     * Since 1.0.0 a string is returned rather than a float (for previous versions).
     *
     * @return     string                   compatible with php.version_compare()
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     */
    function apiVersion()
    {
        return '1.5.0';
    }

    /**
     * Set option for the class
     *
     * Set an individual option value. Option must exist.
     *
     * @param string $option Name of option to set
     * @param string $val    Value of option to set
     *
     * @return void
     * @since  version 1.4.0 (2007-12-13)
     * @access public
     */
    function __set($option, $val)
    {
        if (isset($this->options[$option])) {
            $this->options[$option] = $val;
        }
    }

    /**
     * Get option for the class
     *
     * Return current value of an individual option. If option does not exist,
     * returns value is NULL.
     *
     * @param string $option Name of option to set
     *
     * @return mixed
     * @since  version 1.4.0 (2007-12-13)
     * @access public
     */
    function __get($option)
    {
        if (isset($this->options[$option])) {
            $r = $this->options[$option];
        } else {
            $r = null;
        }
        return $r;
    }

    /**
     * Return all options for the class
     *
     * Return all configuration options at once
     *
     * @return array
     * @since  version 1.5.0 (2008-01-15)
     * @access public
     */
    function getOptions()
    {
        return $this->options;
    }

    /**
     * Set tab value
     *
     * Sets the string used to indent HTML
     *
     * @param string $string String used to indent ("\11", "\t", '  ', etc.).
     *
     * @since     version 1.4.0 (2007-12-13)
     * @access    public
     * @return    void
     */
    function setTab($string)
    {
        $this->__set('tab', $string);
        parent::setTab($string);
    }

    /**
     * Set lineend value
     *
     * Set the line end style to Windows, Mac, Unix or a custom string
     *
     * @param string $style "win", "mac", "unix" or custom string.
     *
     * @since   version 1.4.0 (2007-12-13)
     * @access  public
     * @return  void
     */
    function setLineEnd($style)
    {
        $this->__set('lineEnd', $style);
        parent::setLineEnd($style);
    }

    /**
     * Set oneline flag
     *
     * Determine whether definitions are output on a single line or multi lines
     *
     * @param bool $value flag to true if single line, false for multi lines
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.3 (2004-05-20)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     */
    function setSingleLineOutput($value)
    {
        if (!is_bool($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'boolean',
                      'paramnum' => 1)
            );
        }
        $this->options['oneline'] = $value;
    }

    /**
     * Set groupsfirst flag
     *
     * Determine whether groups are output before elements or not
     *
     * @param bool $value flag to true if groups are output before elements,
     *                    false otherwise
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.3 (2004-05-20)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     */
    function setOutputGroupsFirst($value)
    {
        if (!is_bool($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'boolean',
                      'paramnum' => 1)
            );
        }
        $this->options['groupsfirst'] = $value;
    }

    /**
     * Parse a string containing selector(s)
     *
     * It processes it and returns an array or string containing
     * modified selectors (depends on XHTML compliance setting;
     * defaults to ensure lowercase element names)
     *
     * @param string $selectors  Selector string
     * @param int    $outputMode (optional) 0 = string; 1 = array; 2 = deep array
     *
     * @return     mixed|PEAR_Error
     * @since      version 0.3.2 (2004-03-24)
     * @access     protected
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     */
    function parseSelectors($selectors, $outputMode = 0)
    {
        if (!is_string($selectors)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$selectors',
                      'was' => gettype($selectors),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!is_int($outputMode)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$outputMode',
                      'was' => gettype($outputMode),
                      'expected' => 'integer',
                      'paramnum' => 2)
            );

        } elseif ($outputMode < 0 || $outputMode > 3) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$outputMode',
                      'was' => $outputMode,
                      'expected' => '0 | 1 | 2 | 3',
                      'paramnum' => 2)
            );
        }

        $selectors_array =  explode(',', $selectors);
        $i               = 0;
        foreach ($selectors_array as $selector) {
            // trim to remove possible whitespace
            $selector = trim($this->collapseInternalSpaces($selector));
            if (strpos($selector, ' ')) {
                $sel_a = array();
                foreach (explode(' ', $selector) as $sub_selector) {
                    $sel_a[] = $this->parseSelectors($sub_selector, $outputMode);
                }
                if ($outputMode === 0) {
                        $array[$i] = implode(' ', $sel_a);
                } else {
                    $sel_a2 = array();
                    foreach ($sel_a as $sel_a_temp) {
                        $sel_a2 = array_merge($sel_a2, $sel_a_temp);
                    }
                    if ($outputMode == 2) {
                        $array[$i]['inheritance'] = $sel_a2;
                    } else {
                        $array[$i] = implode(' ', $sel_a2);
                    }
                }
                $i++;
            } else {
                // initialize variables
                $element = '';
                $id      = '';
                $class   = '';
                $pseudo  = '';

                if (strpos($selector, ':') !== false) {
                    $pseudo   = strstr($selector, ':');
                    $selector = substr($selector, 0, strpos($selector, ':'));
                }
                if (strpos($selector, '.') !== false) {
                    $class    = strstr($selector, '.');
                    $selector = substr($selector, 0, strpos($selector, '.'));
                }
                if (strpos($selector, '#') !== false) {
                    $id       = strstr($selector, '#');
                    $selector = substr($selector, 0, strpos($selector, '#'));
                }
                if ($selector != '') {
                    $element = $selector;
                }
                if ($this->options['xhtml']) {
                    $element = strtolower($element);
                    $pseudo  = strtolower($pseudo);
                }
                if ($outputMode == 2) {
                    $array[$i]['element'] = $element;
                    $array[$i]['id']      = $id;
                    $array[$i]['class']   = $class;
                    $array[$i]['pseudo']  = $pseudo;
                } else {
                    $array[$i] = $element.$id.$class.$pseudo;
                }
                $i++;
            }
        }
        if ($outputMode == 0) {
            $output = implode(', ', $array);
            return $output;
        } else {
            return $array;
        }
    }

    /**
     * Strips excess spaces in string.
     *
     * @param string $subject string to format
     *
     * @return     string
     * @since      version 0.3.2 (2004-03-24)
     * @access     protected
     */
    function collapseInternalSpaces($subject)
    {
        $string = preg_replace('/\s+/', ' ', $subject);
        return $string;
    }

    /**
     * sort and move simple declarative At-Rules to the top
     *
     * @return     void
     * @access     protected
     * @since      version 1.5.0 (2008-01-15)
     */
    function sortAtRules()
    {
        // split simple declarative At-Rules from the other
        $return = array('atrules' => array(), 'newcss' => array());

        foreach ($this->_css as $key => $value) {
            if ((0 === strpos($key, "@")) && (1 !== strpos($key, "-"))) {
                $return["atrules"][$key] = $value;
            } else {
                $return["newcss"][$key] = $value;
            }
        }

        // bring sprecial rules to the top
        foreach (array('@namespace', '@import', '@charset') as $name) {
            if (isset($return['atrules'][$name])) {
                $rule = array($name => $return['atrules'][$name]);
                unset($return['atrules'][$name]);
                $return['atrules'] = $rule + $return['atrules'];
            }
        }

        $this->_css = $return['atrules'] + $return['newcss'];
    }

    /**
     * Set xhtml flag
     *
     * Active or not the XHTML mode compliant
     *
     * @param bool $value flag to true if XHTML compliance needed,
     *                    false otherwise
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.2 (2004-03-24)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     */
    function setXhtmlCompliance($value)
    {
        if (!is_bool($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'boolean',
                      'paramnum' => 1)
            );
        }
        $this->options['xhtml'] = $value;
    }

    /**
     * Return list of supported At-Rules
     *
     * Return the list of At-Rules supported by API 1.5.0 of HTML_CSS
     *
     * @return void
     * @since  version 1.5.0 (2008-01-15)
     * @access public
     */
    function getAtRulesList()
    {
        $atRules = array('@charset', '@font-face',
                         '@import', '@media', '@page', '@namespace');
        return $atRules;
    }

    /**
     * Create a new simple declarative At-Rule
     *
     * Create a simple at-rule without declaration style blocks.
     * That include @charset, @import and @namespace
     *
     * @param string $atKeyword  at-rule keyword
     * @param string $arguments  argument list for @charset, @import or @namespace
     * @param bool   $duplicates (optional) Allow or disallow duplicates
     *
     * @return void|PEAR_Error
     * @since  version 1.5.0 (2008-01-15)
     * @access public
     * @throws HTML_CSS_ERROR_INVALID_INPUT
     * @see    unsetAtRule()
     */
    function createAtRule($atKeyword, $arguments = '', $duplicates = null)
    {
        $allowed_atrules = array('@charset', '@import', '@namespace');

        if (!is_string($atKeyword)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$atKeyword',
                      'was' => gettype($atKeyword),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!in_array(strtolower($atKeyword), $allowed_atrules)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$atKeyword',
                      'was' => $atKeyword,
                      'expected' => implode('|', $allowed_atrules),
                      'paramnum' => 1)
            );

        } elseif (!is_string($arguments)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$arguments',
                      'was' => gettype($arguments),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }

        if (empty($arguments)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$arguments',
                      'was' => $arguments,
                      'expected' => 'not empty value',
                      'paramnum' => 2)
            );
        }

        if (!isset($duplicates)) {
            $duplicates = $this->__get('allowduplicates');
        }

        if ($duplicates) {
            $this->_duplicateCounter++;
            $this->_css[strtolower($atKeyword)][$this->_duplicateCounter]
                = array($arguments => '');
        } else {
            $this->_css[strtolower($atKeyword)] = array($arguments => '');
        }
    }

    /**
     * Remove an existing At-Rule
     *
     * Remove an existing and supported at-rule. See HTML_CSS::getAtRulesList()
     * for a full list of supported At-Rules.
     *
     * @param string $atKeyword at-rule keyword
     *
     * @return void|PEAR_Error
     * @since  version 1.5.0 (2008-01-15)
     * @access public
     * @throws HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_NO_ATRULE
     */
    function unsetAtRule($atKeyword)
    {
        $allowed_atrules = $this->getAtRulesList();

        if (!is_string($atKeyword)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$atKeyword',
                      'was' => gettype($atKeyword),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!in_array(strtolower($atKeyword), $allowed_atrules)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$atKeyword',
                      'was' => $atKeyword,
                      'expected' => implode('|', $allowed_atrules),
                      'paramnum' => 1)
            );

        } elseif (!isset($this->_css[strtolower($atKeyword)])) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_ATRULE, 'error',
                array('identifier' => $atKeyword)
            );
        }

        unset($this->_css[strtolower($atKeyword)]);
    }

    /**
     * Define a conditional/informative At-Rule
     *
     * Set arguments and declaration style block for at-rules that follow :
     * "@media, @page, @font-face"
     *
     * @param string $atKeyword  at-rule keyword
     * @param string $arguments  argument list
     *                           (optional for @font-face)
     * @param string $selectors  selectors of declaration style block
     *                           (optional for @media, @page, @font-face)
     * @param string $property   property of a single declaration style block
     * @param string $value      value of a single declaration style block
     * @param bool   $duplicates (optional) Allow or disallow duplicates
     *
     * @return void|PEAR_Error
     * @since  version 1.5.0 (2008-01-15)
     * @access public
     * @throws HTML_CSS_ERROR_INVALID_INPUT
     * @see    getAtRuleStyle()
     */
    function setAtRuleStyle($atKeyword, $arguments, $selectors, $property, $value,
        $duplicates = null
    ) {
        $allowed_atrules = array('@media', '@page', '@font-face');

        if (!is_string($atKeyword)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$atKeyword',
                      'was' => gettype($atKeyword),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!in_array(strtolower($atKeyword), $allowed_atrules)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$atKeyword',
                      'was' => $atKeyword,
                      'expected' => implode('|', $allowed_atrules),
                      'paramnum' => 1)
            );

        } elseif (empty($arguments) && strtolower($atKeyword) != '@font-face') {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$arguments',
                      'was' => $arguments,
                      'expected' => 'not empty value for '. $atKeyword,
                      'paramnum' => 2)
            );

        } elseif (!is_string($selectors)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$selectors',
                      'was' => gettype($selectors),
                      'expected' => 'string',
                      'paramnum' => 3)
            );

        } elseif (!is_string($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'string',
                      'paramnum' => 4)
            );

        } elseif (!is_string($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'string',
                      'paramnum' => 5)
            );

        } elseif (empty($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$property',
                      'was' => $property,
                      'expected' => 'no empty string',
                      'paramnum' => 4)
            );

        } elseif (empty($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'no empty string',
                      'paramnum' => 5)
            );
        }

        if (!isset($duplicates)) {
            $duplicates = $this->__get('allowduplicates');
        }

        $atKeyword = strtolower($atKeyword);

        if (!empty($selectors)) {
            $selectors = $this->parseSelectors($selectors);
        }
        $this->_css[$atKeyword][$arguments][$selectors][$property] = $value;
    }

    /**
     * Get style value of an existing At-Rule
     *
     * Retrieve arguments or style value of an existing At-Rule.
     * See HTML_CSS::getAtRulesList() for a full list of supported At-Rules.
     *
     * @param string $atKeyword at-rule keyword
     * @param string $arguments argument list
     *                          (optional for @font-face)
     * @param string $selectors selectors of declaration style block
     *                          (optional for @media, @page, @font-face)
     * @param string $property  property of a single declaration style block
     *
     * @return void|PEAR_Error
     * @since  version 1.5.0 (2008-01-15)
     * @access public
     * @throws HTML_CSS_ERROR_INVALID_INPUT
     * @see    setAtRuleStyle()
     */
    function getAtRuleStyle($atKeyword, $arguments, $selectors, $property)
    {
        $allowed_atrules = $this->getAtRulesList();

        if (!is_string($atKeyword)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$atKeyword',
                      'was' => gettype($atKeyword),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!in_array(strtolower($atKeyword), $allowed_atrules)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$atKeyword',
                      'was' => $atKeyword,
                      'expected' => implode('|', $allowed_atrules),
                      'paramnum' => 1)
            );

        } elseif (!is_string($arguments)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$arguments',
                      'was' => gettype($arguments),
                      'expected' => 'string',
                      'paramnum' => 2)
            );

        } elseif (!is_string($selectors)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$selectors',
                      'was' => gettype($selectors),
                      'expected' => 'string',
                      'paramnum' => 3)
            );

        } elseif (!is_string($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'string',
                      'paramnum' => 4)
            );
        }

        if (isset($this->_css[$atKeyword][$arguments][$selectors][$property])) {
            $val = $this->_css[$atKeyword][$arguments][$selectors][$property];
        } else {
            $val = null;
        }
        return $val;
    }

    /**
     * Create a new CSS definition group
     *
     * Create a new CSS definition group. Return an integer identifying the group.
     *
     * @param string $selectors Selector(s) to be defined, comma delimited.
     * @param mixed  $group     (optional) Group identifier. If not passed,
     *                          will return an automatically assigned integer.
     *
     * @return     mixed|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_INVALID_GROUP
     * @see        unsetGroup()
     */
    function createGroup($selectors, $group = null)
    {
        if (!is_string($selectors)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$selectors',
                      'was' => gettype($selectors),
                      'expected' => 'string',
                      'paramnum' => 1)
            );
        }

        if (!isset($group)) {
            $this->_groupCount++;
            $group = $this->_groupCount;
        } else {
            if (isset($this->_groups['@-'.$group])) {
                return $this->raiseError(
                    HTML_CSS_ERROR_INVALID_GROUP, 'error',
                    array('identifier' => $group)
                );
            }
        }

        $groupIdent = '@-'.$group;

        $selectors = $this->parseSelectors($selectors, 1);
        foreach ($selectors as $selector) {
            $this->_alibis[$selector][] = $groupIdent;
        }

        $this->_groups[$groupIdent] = $selectors;

        return $group;
    }

    /**
     * Remove a CSS definition group
     *
     * Remove a CSS definition group. Use the same identifier as for group creation.
     *
     * @param mixed $group CSS definition group identifier
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_NO_GROUP
     * @see        createGroup()
     */
    function unsetGroup($group)
    {
        if (!is_int($group) && !is_string($group)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$group',
                      'was' => gettype($group),
                      'expected' => 'integer | string',
                      'paramnum' => 1)
            );
        }
        $groupIdent = '@-'.$group;
        if ($group < 0 || $group > $this->_groupCount
            || !isset($this->_groups[$groupIdent])
        ) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_GROUP, 'error',
                array('identifier' => $group)
            );
        }

        $alibis = $this->_alibis;
        foreach ($alibis as $selector => $data) {
            foreach ($data as $key => $value) {
                if ($value == $groupIdent) {
                    unset($this->_alibis[$selector][$key]);
                    break;
                }
            }
            if (count($this->_alibis[$selector]) == 0) {
                unset($this->_alibis[$selector]);
            }
        }
        unset($this->_groups[$groupIdent]);
        unset($this->_css[$groupIdent]);
    }

    /**
     * Set or add a CSS definition for a CSS group
     *
     * Define the new value of a property for a CSS group. The group should exist.
     * If not, use HTML_CSS::createGroup first
     *
     * @param mixed  $group      CSS definition group identifier
     * @param string $property   Property defined
     * @param string $value      Value assigned
     * @param bool   $duplicates (optional) Allow or disallow duplicates.
     *
     * @return     void|int|PEAR_Error     Returns an integer if duplicates
     *                                     are allowed.
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_NO_GROUP
     * @see        getGroupStyle()
     */
    function setGroupStyle($group, $property, $value, $duplicates = null)
    {
        if (!is_int($group) && !is_string($group)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$group',
                      'was' => gettype($group),
                      'expected' => 'integer | string',
                      'paramnum' => 1)
            );

        } elseif (!is_string($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'string',
                      'paramnum' => 2)
            );

        } elseif (empty($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'no empty string',
                      'paramnum' => 2)
            );

        } elseif (!is_string($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'string',
                      'paramnum' => 3)
            );

        } elseif (isset($duplicates) && !is_bool($duplicates)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$duplicates',
                      'was' => gettype($duplicates),
                      'expected' => 'bool',
                      'paramnum' => 4)
            );
        }

        if (!isset($duplicates)) {
            $duplicates = $this->__get('allowduplicates');
        }

        $groupIdent = '@-'.$group;
        if ($group < 0 || $group > $this->_groupCount
            || !isset($this->_groups[$groupIdent])
        ) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_GROUP, 'error',
                array('identifier' => $group)
            );
        }

        if ($duplicates === true) {
            $this->_duplicateCounter++;
            $this->_css[$groupIdent][$this->_duplicateCounter][$property] = $value;
            return $this->_duplicateCounter;
        } else {
            $this->_css[$groupIdent][$property] = $value;
        }
    }

    /**
     * Return CSS definition for a CSS group
     *
     * Get the CSS definition for group created by setGroupStyle()
     *
     * @param mixed  $group    CSS definition group identifier
     * @param string $property Property defined
     *
     * @return     mixed|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_NO_GROUP,
     *             HTML_CSS_ERROR_NO_ELEMENT
     * @see        setGroupStyle()
     */
    function getGroupStyle($group, $property)
    {
        if (!is_int($group) && !is_string($group)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$group',
                      'was' => gettype($group),
                      'expected' => 'integer | string',
                      'paramnum' => 1)
            );

        } elseif (!is_string($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }
        $groupIdent = '@-'.$group;
        if ($group < 0 || $group > $this->_groupCount
            || !isset($this->_groups[$groupIdent])
        ) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_GROUP, 'error',
                array('identifier' => $group)
            );
        }

        $styles = array();

        if (!isset($this->_css[$groupIdent])) {
            return $styles;
        }

        foreach ($this->_css[$groupIdent] as $rank => $prop) {
            // if the style is not duplicate
            if (!is_numeric($rank)) {
                $prop = array($rank => $prop);
            }
            foreach ($prop as $key => $value) {
                if ($key == $property) {
                    $styles[] = $value;
                }
            }
        }

        if (count($styles) < 2) {
            $styles = array_shift($styles);
        }
        return $styles;
    }

    /**
     * Add a selector to a CSS definition group.
     *
     * Add a selector to a CSS definition group
     *
     * @param mixed  $group     CSS definition group identifier
     * @param string $selectors Selector(s) to be defined, comma delimited.
     *
     * @return   void|PEAR_Error
     * @since    version 0.3.0 (2003-11-03)
     * @access   public
     * @throws   HTML_CSS_ERROR_NO_GROUP, HTML_CSS_ERROR_INVALID_INPUT
     */
    function addGroupSelector($group, $selectors)
    {
        if (!is_int($group) && !is_string($group)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$group',
                      'was' => gettype($group),
                      'expected' => 'integer | string',
                      'paramnum' => 1)
            );
        }
        $groupIdent = '@-'.$group;
        if ($group < 0 || $group > $this->_groupCount
            || !isset($this->_groups[$groupIdent])
        ) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_GROUP, 'error',
                array('identifier' => $group)
            );

        } elseif (!is_string($selectors)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$selectors',
                      'was' => gettype($selectors),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }

        $newSelectors = $this->parseSelectors($selectors, 1);
        foreach ($newSelectors as $selector) {
            $this->_alibis[$selector][] = $groupIdent;
        }
        $oldSelectors = $this->_groups[$groupIdent];

        $this->_groups[$groupIdent] = array_merge($oldSelectors, $newSelectors);
    }

    /**
     * Remove a selector from a group
     *
     * Definitively remove a selector from a CSS group
     *
     * @param mixed  $group     CSS definition group identifier
     * @param string $selectors Selector(s) to be removed, comma delimited.
     *
     * @return   void|PEAR_Error
     * @since    version 0.3.0 (2003-11-03)
     * @access   public
     * @throws   HTML_CSS_ERROR_NO_GROUP, HTML_CSS_ERROR_INVALID_INPUT
     */
    function removeGroupSelector($group, $selectors)
    {
        if (!is_int($group) && !is_string($group)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$group',
                      'was' => gettype($group),
                      'expected' => 'integer | string',
                      'paramnum' => 1)
            );
        }
        $groupIdent = '@-'.$group;
        if ($group < 0 || $group > $this->_groupCount
            || !isset($this->_groups[$groupIdent])
        ) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_GROUP, 'error',
                array('identifier' => $group)
            );

        } elseif (!is_string($selectors)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$selectors',
                      'was' => gettype($selectors),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }

        $oldSelectors = $this->_groups[$groupIdent];
        $selectors    = $this->parseSelectors($selectors, 1);
        foreach ($selectors as $selector) {
            foreach ($oldSelectors as $key => $value) {
                if ($value == $selector) {
                    unset($this->_groups[$groupIdent][$key]);
                }
            }
            foreach ($this->_alibis[$selector] as $key => $value) {
                if ($value == $groupIdent) {
                    unset($this->_alibis[$selector][$key]);
                }
            }
        }
    }

    /**
     * Set or add a CSS definition
     *
     * Add or change a single value for an element property
     *
     * @param string $element    Element (or class) to be defined
     * @param string $property   Property defined
     * @param string $value      Value assigned
     * @param bool   $duplicates (optional) Allow or disallow duplicates.
     *
     * @return     void|PEAR_Error
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     * @see        getStyle()
     */
    function setStyle($element, $property, $value, $duplicates = null)
    {
        if (!is_string($element)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$element',
                      'was' => gettype($element),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!is_string($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'string',
                      'paramnum' => 2)
            );

        } elseif (!is_string($value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$value',
                      'was' => gettype($value),
                      'expected' => 'string',
                      'paramnum' => 3)
            );

        } elseif (strpos($element, ',')) {
            // Check if there are any groups.
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$element',
                      'was' => $element,
                      'expected' => 'string without comma',
                      'paramnum' => 1)
            );

        } elseif (isset($duplicates) && !is_bool($duplicates)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$duplicates',
                      'was' => gettype($duplicates),
                      'expected' => 'bool',
                      'paramnum' => 4)
            );
        }

        if (!isset($duplicates)) {
             $duplicates = $this->__get('allowduplicates');
        }

        $element = $this->parseSelectors($element);

        if ($duplicates === true) {
            $this->_duplicateCounter++;
            $this->_css[$element][$this->_duplicateCounter][$property] = $value;
            return $this->_duplicateCounter;
        } else {
            $this->_css[$element][$property] = $value;
        }
    }

    /**
     * Return the value of a CSS property
     *
     * Get the value of a property to an identifed simple CSS element
     *
     * @param string $element  Element (or class) to be defined
     * @param string $property Property defined
     *
     * @return     mixed|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT,
     *             HTML_CSS_ERROR_NO_ELEMENT, HTML_CSS_ERROR_NO_ELEMENT_PROPERTY
     * @see        setStyle()
     */
    function getStyle($element, $property)
    {
        if (!is_string($element)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$element',
                      'was' => gettype($element),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!is_string($property)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$property',
                      'was' => gettype($property),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }
        if (!isset($this->_css[$element]) && !isset($this->_alibis[$element])) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_ELEMENT, 'error',
                array('identifier' => $element)
            );
        }

        if (isset($this->_css[$element]) && isset($this->_alibis[$element])) {
            $lastImplementation = array_keys($this->_alibis[$element]);
            $lastImplementation = array_pop($lastImplementation);

            $group = substr($this->_alibis[$element][$lastImplementation], 2);

            $property_value = $this->getGroupStyle($group, $property);
            if (count($property_value) == 0) {
                unset($property_value);
            }
        }
        if (isset($this->_css[$element]) && !isset($property_value)) {
            $property_value = array();
            foreach ($this->_css[$element] as $rank => $prop) {
                if (!is_numeric($rank)) {
                    $prop = array($rank => $prop);
                }
                foreach ($prop as $key => $value) {
                    if ($key == $property) {
                        $property_value[] = $value;
                    }
                }
            }
            if (count($property_value) == 1) {
                $property_value = $property_value[0];
            } elseif (count($property_value) == 0) {
                unset($property_value);
            }
        }

        if (!isset($property_value)) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_ELEMENT_PROPERTY, 'error',
                array('identifier' => $element,
                      'property'   => $property)
            );
        }
        return $property_value;
    }

    /**
     * Retrieve styles corresponding to an element filter
     *
     * Return array entries of styles that match patterns (Perl compatible)
     *
     * @param string $elmPattern Element or class pattern to retrieve
     * @param string $proPattern (optional) Property pattern to retrieve
     *
     * @return     array|PEAR_Error
     * @since      version 1.1.0 (2007-01-01)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     * @link       http://www.php.net/en/ref.pcre.php
     *             Regular Expression Functions (Perl-Compatible)
     */
    function grepStyle($elmPattern, $proPattern = null)
    {
        if (!is_string($elmPattern)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$elmPattern',
                      'was' => gettype($elmPattern),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (isset($proPattern) && !is_string($proPattern)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$proPattern',
                      'was' => gettype($proPattern),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }

        $styles = array();

        // first, search inside alibis
        $alibis = array_keys($this->_alibis);
        $alibis = preg_grep($elmPattern, $alibis);
        foreach ($alibis as $a) {
            foreach ($this->_alibis[$a] as $g) {
                if (isset($proPattern)) {
                    $properties = array_keys($this->_css[$g]);
                    $properties = preg_grep($proPattern, $properties);
                    if (count($properties) == 0) {
                        // this group does not have a such property pattern
                        continue;
                    }
                }
                if (isset($styles[$a])) {
                    $styles[$a] = array_merge($styles[$a], $this->_css[$g]);
                } else {
                    $styles[$a] = $this->_css[$g];
                }
            }
        }

        // second, search inside elements
        $elements = array_keys($this->_css);
        $elements = preg_grep($elmPattern, $elements);
        foreach ($elements as $e) {
            if (substr($e, 0, 1) == '@' ) {
                // excludes groups (already found with alibis)
                continue;
            }
            if (isset($proPattern)) {
                $properties = array_keys($this->_css[$e]);
                $properties = preg_grep($proPattern, $properties);
                if (count($properties) == 0) {
                    // this element does not have a such property pattern
                    continue;
                }
            }
            if (isset($styles[$e])) {
                $styles[$e] = array_merge($styles[$e], $this->_css[$e]);
            } else {
                $styles[$e] = $this->_css[$e];
            }
        }
        return $styles;
    }

    /**
     * Apply same styles on two selectors
     *
     * Set or change the properties of new selectors
     * to the values of an existing selector
     *
     * @param string $new New selector(s) that should share the same
     *                    definitions, separated by commas
     * @param string $old Selector that is already defined
     *
     * @return     void|PEAR_Error
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_NO_ELEMENT
     */
    function setSameStyle($new, $old)
    {
        if (!is_string($new)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$new',
                      'was' => gettype($new),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!is_string($old)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$old',
                      'was' => gettype($old),
                      'expected' => 'string',
                      'paramnum' => 2)
            );

        } elseif (strpos($new, ',')) {
            // Check if there are any groups.
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$new',
                      'was' => $new,
                      'expected' => 'string without comma',
                      'paramnum' => 1)
            );

        } elseif (strpos($old, ',')) {
            // Check if there are any groups.
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'error',
                array('var' => '$old',
                      'was' => $old,
                      'expected' => 'string without comma',
                      'paramnum' => 2)
            );
        }

        $old = $this->parseSelectors($old);
        if (!isset($this->_css[$old])) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_ELEMENT, 'error',
                array('identifier' => $old)
            );
        }

        $selector = implode(', ', array($old, $new));
        $grp      = $this->createGroup($selector, 'samestyleas_'.$old);

        $others = $this->parseSelectors($new, 1);
        foreach ($others as $other) {
            $other = trim($other);
            foreach ($this->_css[$old] as $rank => $property) {
                if (!is_numeric($rank)) {
                    $property = array($rank => $property);
                }
                foreach ($property as $key => $value) {
                    $this->setGroupStyle($grp, $key, $value);
                }
            }
            unset($this->_css[$old]);
        }
    }

    /**
     * Set cache flag
     *
     * Define if the document should be cached by the browser. Default to false.
     *
     * @param bool $cache (optional) flag to true to cache result, false otherwise
     *
     * @return     void|PEAR_Error
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     */
    function setCache($cache = true)
    {
        if (!is_bool($cache)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$cache',
                      'was' => gettype($cache),
                      'expected' => 'boolean',
                      'paramnum' => 1)
            );
        }
        $this->options['cache'] = $cache;
    }

    /**
     * Returns the cache option value
     *
     * @return     boolean
     * @since      version 1.4.0 (2007-12-13)
     * @access     public
     * @see        setCache()
     */
    function getCache()
    {
        return $this->__get('cache');
    }

    /**
     * Set Content-Disposition header
     *
     * Define the Content-Disposition header to supply a recommended filename
     * and force the browser to display the save dialog.
     * Default to basename($_SERVER['PHP_SELF']).'.css'
     *
     * @param bool   $enable   (optional)
     * @param string $filename (optional)
     *
     * @return     void|PEAR_Error
     * @since      version 1.3.0 (2007-10-22)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     * @see        getContentDisposition()
     * @link       http://pear.php.net/bugs/bug.php?id=12195
     *             Patch by Carsten Wiedmann
     */
    function setContentDisposition($enable = true, $filename = '')
    {
        if (!is_bool($enable)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$enable',
                      'was' => gettype($enable),
                      'expected' => 'bool',
                      'paramnum' => 1)
            );
        } elseif (!is_string($filename)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$filename',
                      'was' => gettype($filename),
                      'expected' => 'string',
                      'paramnum' => 2)
            );
        }

        if ($enable == false) {
            $filename = false;
        } elseif ($filename == '') {
            $filename = basename($_SERVER['PHP_SELF']) . '.css';
        }

        $this->options['contentDisposition'] = $filename;
    }

    /**
     * Return the Content-Disposition header
     *
     * Get value of Content-Disposition header (inline filename) used
     * to display results
     *
     * @return     mixed     boolean FALSE if no content disposition, otherwise
     *                       string for inline filename
     * @since      version 1.3.0 (2007-10-22)
     * @access     public
     * @see        setContentDisposition()
     * @link       http://pear.php.net/bugs/bug.php?id=12195
     *             Patch by Carsten Wiedmann
     */
    function getContentDisposition()
    {
        return $this->__get('contentDisposition');
    }

    /**
     * Set charset value
     *
     * Define the charset for the file. Default to ISO-8859-1 because of CSS1
     * compatability issue for older browsers.
     *
     * @param string $type (optional) Charset encoding; defaults to ISO-8859-1.
     *
     * @return     void|PEAR_Error
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     * @see        getCharset()
     */
    function setCharset($type = 'iso-8859-1')
    {
        if (!is_string($type)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$type',
                      'was' => gettype($type),
                      'expected' => 'string',
                      'paramnum' => 1)
            );
        }
        $this->options['charset'] = $type;
    }

    /**
     * Return the charset encoding string
     *
     * By default, HTML_CSS uses iso-8859-1 encoding.
     *
     * @return     string
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @see        setCharset()
     */
    function getCharset()
    {
        return $this->__get('charset');
    }

    /**
     * Parse a string
     *
     * Parse a string that contains CSS information
     *
     * @param string $str        text string to parse
     * @param bool   $duplicates (optional) Allows or disallows
     *                           duplicate style definitions
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     * @see        createGroup(), setGroupStyle(), setStyle()
     */
    function parseString($str, $duplicates = null)
    {
        if (!is_string($str)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$str',
                      'was' => gettype($str),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (isset($duplicates) && !is_bool($duplicates)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$duplicates',
                      'was' => gettype($duplicates),
                      'expected' => 'bool',
                      'paramnum' => 2)
            );
        }

        if (!isset($duplicates)) {
            $duplicates = $this->__get('allowduplicates');
        }

        // Remove comments
        $str = preg_replace("/\/\*(.*)?\*\//Usi", '', $str);

        // Protect parser vs IE hack
        $str = str_replace('"\"}\""', '#34#125#34', $str);

        // Parse simple declarative At-Rules
        $atRules    = array();
        $elements   = array();
        $properties = array();

        // core of major 1.5.4 parser
        preg_match_all(
            '/(?ims)([a-z0-9\s\.\:#_\-@,]+)\{([^\{|^\}]*)\}/',
            $str, $rules, PREG_SET_ORDER
        );

        // structure simplified
        $structure = preg_replace(
            '/(?ims)([a-zA-Z0-9\s\.\:#_\-@,]+)\{([^\{|^\}]*)\}/',
            '\1{}', $str
        );
        // structure map
        $structure = preg_split(
            '/([a-zA-Z0-9\s\.\:#_\-@,]+)\{(.*)\}/', $structure, -1,
            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
        );

        $atRulesMap = array();
        $atRule = '';
        foreach ($structure as $struct) {
            $struct = trim($struct);
            if (empty($struct)) {
                continue;
            }
            if ($struct{0} == '}') {
                $atRule = '';
                $struct = substr($struct, 1);
                $struct = ltrim($struct);
            }

            if ($this->options['xhtml']) {
                $struct = strtolower($struct);
            }

            
            $has_AtRules = preg_match_all(
                '/^(@[a-zA-Z\-]+)\s+(.+);\s*$/m', $struct, $atRules,
                PREG_SET_ORDER
            );
            if ($has_AtRules) {
                foreach ($atRules as $value) {
                    $this->createAtRule(
                        trim($value[1]), trim($value[2]), $duplicates
                    );
                }
                continue;
            }
            $struct = $this->collapseInternalSpaces($struct);

            $pos = strpos($struct, '{');
            if ($pos
                || (strpos($struct, ':') && !empty($atRule))
            ) {
                $cc = count_chars($struct, 1);
                if ((isset($cc[64]) && $cc[64] > 1)
                    || (isset($cc[58]) && $cc[58] == 1)
                ) {
                    $context  = debug_backtrace();
                    $context  = @array_pop($context);
                    $function = strtolower($context['function']);
                    if ($function === 'parsestring') {
                        $var = 'str';
                    } elseif ($function === 'parsefile') {
                        $var = 'filename';
                    } else {
                        $var = 'styles';
                    }

                    return $this->raiseError(
                        HTML_CSS_ERROR_INVALID_INPUT, 'error',
                        array('var' => '$'.$var,
                              'was' => 'invalid data source',
                              'expected' => 'valid CSS structure',
                              'paramnum' => 1)
                    );
                }
                $atRule = rtrim(substr($struct, 0, $pos));

            } else {
                $atRulesMap[$struct][] = $atRule;
            }
        }

        foreach ($rules as $rule) {

            // prevent invalid css data structure
            $pos = strpos($rule[0], '{');
            $sel = trim($rule[1]);
            if ((strpos($rule[0], '{', $pos+1) !== false)
                || (substr($sel, -1, 1) == ':')
            ) {
                $context  = debug_backtrace();
                $context  = @array_pop($context);
                $function = strtolower($context['function']);
                if ($function === 'parsestring') {
                    $var = 'str';
                } elseif ($function === 'parsefile') {
                    $var = 'filename';
                } else {
                    $var = 'styles';
                }

                return $this->raiseError(
                    HTML_CSS_ERROR_INVALID_INPUT, 'error',
                    array('var' => '$'.$var,
                          'was' => 'invalid data source',
                          'expected' => 'valid CSS structure',
                          'paramnum' => 1)
                );
            }

            if ($this->options['xhtml']) {
                $rule[1] = strtolower($rule[1]);
            }

            $elements[]   = trim($rule[1]);
            $properties[] = trim($rule[2]);
        }

        foreach ($elements as $i => $keystr) {

            $key_a   = $this->parseSelectors($keystr, 1);
            $keystr  = implode(', ', $key_a);
            $codestr = $properties[$i];

            $key = trim($keystr);
            $parentAtRule = isset($atRulesMap[$key][$i])
                ? $atRulesMap[$key][$i] : $atRulesMap[$key][0];

            // Check if there are any groups; in standard selectors exclude at-rules
            if (strpos($keystr, ',') && (empty($parentAtRule))) {
                $group = $this->createGroup($keystr);

                // Parse each property of an element
                $codes = explode(";", trim($codestr));
                foreach ($codes as $code) {
                    if (strlen(trim($code)) > 0) {
                        // find the property and the value
                        $property
                            = trim(substr($code, 0, strpos($code, ':', 0)));
                        $value
                            = trim(substr($code, strpos($code, ':', 0) + 1));
                        // IE hack only
                        if (strcasecmp($property, 'voice-family') == 0) {
                            $value
                                = str_replace('#34#125#34', '"\"}\""', $value);
                        }
                        $this->setGroupStyle(
                            $group, $property, $value, $duplicates
                        );
                    }
                }
            } else {
                // Parse each property of an element
                $codes = explode(";", trim($codestr));
                foreach ($codes as $code) {
                    if (strlen(trim($code)) == 0) {
                        continue;
                    }
                    $code = ltrim($code, "\r\n}");

                    $p = trim(substr($code, 0, strpos($code, ':')));
                    $v = trim(substr($code, strpos($code, ':') + 1));
                    // IE hack only
                    if (strcasecmp($p, 'voice-family') == 0) {
                        $v = str_replace('#34#125#34', '"\"}\""', $v);
                    }

                    if (!empty($parentAtRule)) {
                        // at-rules
                        $atkw_args = preg_split(
                            '/(@[a-zA-Z\-]+)\s+(.+)/', $parentAtRule, -1,
                            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
                        );
                        if (count($atkw_args) == 1) {
                            // special case of @font-face (without argument)
                            $atkw_args[] = '';
                        }
                        list($atKeyword, $arguments) = $atkw_args;
                        $this->setAtRuleStyle(
                            $atKeyword, $arguments, $keystr, $p, $v, $duplicates
                        );

                    } elseif ($key{0} == '@') {
                        $atkw_args = preg_split(
                            '/(@[a-zA-Z\-]+)\s+(.+)/', $key, -1,
                            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
                        );
                        if (count($atkw_args) == 1) {
                            $atkw_args[] = '';
                        }
                        list($atKeyword, $arguments) = $atkw_args;
                        $this->setAtRuleStyle(
                            $atKeyword, $arguments, '', $p, $v, $duplicates
                        );
                    } else {
                        // simple declarative style
                        $this->setStyle($key, $p, $v, $duplicates);
                    }
                }
            }
        }
    }

    /**
     * Parse file content
     *
     * Parse a file that contains CSS information
     *
     * @param string $filename   file to parse
     * @param bool   $duplicates (optional) Allow or disallow duplicates.
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_NO_FILE
     * @see        parseString()
     */
    function parseFile($filename, $duplicates = null)
    {
        if (!is_string($filename)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$filename',
                      'was' => gettype($filename),
                      'expected' => 'string',
                      'paramnum' => 1)
            );

        } elseif (!file_exists($filename)) {
            return $this->raiseError(
                HTML_CSS_ERROR_NO_FILE, 'error',
                array('identifier' => $filename)
            );

        } elseif (isset($duplicates) && !is_bool($duplicates)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$duplicates',
                      'was' => gettype($duplicates),
                      'expected' => 'bool',
                      'paramnum' => 2)
            );
        }

        if (!isset($duplicates)) {
            $duplicates = $this->__get('allowduplicates');
        }

        $ret = $this->parseString(file_get_contents($filename), $duplicates);
        return $ret;
    }

    /**
     * Parse multiple data sources
     *
     * Parse data sources, file(s) or string(s), that contains CSS information
     *
     * @param array $styles     data sources to parse
     * @param bool  $duplicates (optional) Allow or disallow duplicates.
     *
     * @return     void|PEAR_Error
     * @since      version 1.0.0RC2 (2005-12-15)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     * @see        parseString(), parseFile()
     */
    function parseData($styles, $duplicates = null)
    {
        if (!is_array($styles)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$styles',
                      'was' => gettype($styles),
                      'expected' => 'array',
                      'paramnum' => 1)
            );

        } elseif (isset($duplicates) && !is_bool($duplicates)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$duplicates',
                      'was' => gettype($duplicates),
                      'expected' => 'bool',
                      'paramnum' => 2)
            );
        }

        if (!isset($duplicates)) {
            $duplicates = $this->__get('allowduplicates');
        }

        foreach ($styles as $i => $style) {
            if (!is_string($style)) {
                return $this->raiseError(
                    HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                    array('var' => '$styles[' . $i . ']',
                          'was' => gettype($styles[$i]),
                          'expected' => 'string',
                          'paramnum' => 1)
                );
            }
            if (strcasecmp(substr($style, -4, 4), '.css') == 0) {
                $this->parseFile($style, $duplicates);
            } else {
                $this->parseString($style, $duplicates);
            }
        }
    }

    /**
     * Validate a CSS data source
     *
     * Execute the W3C CSS validator service on each data source (filename
     * or string) given by parameter $styles.
     *
     * @param array $styles    Data sources to check validity
     * @param array &$messages Error and Warning messages
     *                         issue from W3C CSS validator service
     *
     * @return     boolean|PEAR_Error
     * @since      version 1.5.0 (2008-01-15)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT,
     *             HTML_CSS_ERROR_INVALID_DEPS, HTML_CSS_ERROR_INVALID_SOURCE
     */
    function validate($styles, &$messages)
    {
        $php = phpversion();
        if (version_compare($php, '5.0.0', '<')) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_DEPS, 'exception',
                array('funcname' => __FUNCTION__,
                      'dependency' => 'PHP 5',
                      'currentdep' => "PHP $php")
            );
        }
        @include_once 'Services/W3C/CSSValidator.php';
        if (class_exists('Services_W3C_CSSValidator', false) === false) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_DEPS, 'exception',
                array('funcname' => __FUNCTION__,
                      'dependency' => 'PEAR::Services_W3C_CSSValidator',
                      'currentdep' => 'nothing')
            );
        }
        if (!is_array($styles)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$styles',
                      'was' => gettype($styles),
                      'expected' => 'array',
                      'paramnum' => 1)
            );

        } elseif (!is_array($messages)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$messages',
                      'was' => gettype($messages),
                      'expected' => 'array',
                      'paramnum' => 2)
            );
        }

        // prepare to call the W3C CSS validator service
        $v        = new Services_W3C_CSSValidator();
        $validity = true;
        $messages = array('errors' => array(), 'warnings' => array());

        foreach ($styles as $i => $source) {
            if (!is_string($source)) {
                return $this->raiseError(
                    HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                    array('var' => '$styles[' . $i . ']',
                          'was' => gettype($styles[$i]),
                          'expected' => 'string',
                          'paramnum' => 1)
                );
            }
            if (strcasecmp(substr($source, -4, 4), '.css') == 0) {
                // validate a file as CSS content
                $r = $v->validateFile($source);
            } else {
                // validate a string as CSS content
                $r = $v->validateFragment($source);
            }
            if ($r === false) {
                $validity = false;
            }
            if ($r->isValid() === false) {
                $validity = false;
                foreach ($r->errors as $error) {
                    $properties           = get_object_vars($error);
                    $messages['errors'][] = $properties;
                }
                foreach ($r->warnings as $warning) {
                    $properties             = get_object_vars($warning);
                    $messages['warnings'][] = $properties;
                }
                $this->raiseError(
                    HTML_CSS_ERROR_INVALID_SOURCE,
                    ((count($r->errors) == 0) ? 'warning' : 'error'),
                    array('sourcenum' => $i,
                          'errcount' => count($r->errors),
                          'warncount' => count($r->warnings))
                );
            }
        }
        return $validity;
    }

    /**
     * Return the CSS contents in an array
     *
     * Return the full contents of CSS data sources (parsed) in an array
     *
     * @return     array
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     */
    function toArray()
    {
        $css = array();

        // bring AtRules in correct order
        $this->sortAtRules();

        foreach ($this->_css as $key => $value) {
            if (strpos($key, '@-') === 0) {
                $key = implode(', ', $this->_groups[$key]);
            }
            $css[$key] = $value;
        }
        return $css;
    }

    /**
     * Return a string-properties for style attribute of an HTML element
     *
     * Generate and return the CSS properties of an element or class
     * as a string for inline use.
     *
     * @param string $element Element or class
     *                        for which inline CSS should be generated
     *
     * @return     string|PEAR_Error
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT
     */
    function toInline($element)
    {
        if (!is_string($element)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$element',
                      'was' => gettype($element),
                      'expected' => 'string',
                      'paramnum' => 1)
            );
        }

        $strCss      = '';
        $newCssArray = array();

        // This allows for grouped elements definitions to work
        if (isset($this->_alibis[$element])) {
            $alibis = $this->_alibis[$element];

            // All the groups must be run through to be able to
            // properly assign the value to the inline.
            foreach ($alibis as $alibi) {
                foreach ($this->_css[$alibi] as $key => $value) {
                    $newCssArray[$key] = $value;
                }
            }
        }

        // This allows for single elements definitions to work
        if (isset($this->_css[$element])) {
            foreach ($this->_css[$element] as $rank => $property) {
                if (!is_numeric($rank)) {
                    $property = array($rank => $property);
                }
                foreach ($property as $key => $value) {
                    if ($key != 'other-elements') {
                        $newCssArray[$key] = $value;
                    }
                }
            }
        }

        foreach ($newCssArray as $key => $value) {
            if ((0 === strpos($element, '@')) && ('' == $value)) {
                // simple declarative At-Rule definition
                $strCss .= $key . ';';
            } else {
                // other CSS definition
                $strCss .= $key . ':' . $value . ";";
            }
        }

        return $strCss;
    }

    /**
     * Generate CSS and stores it in a file
     *
     * Generate current parsed CSS data sources and write result in a user file
     *
     * @param string $filename Name of file that content the stylesheet
     *
     * @return     void|PEAR_Error
     * @since      version 0.3.0 (2003-11-03)
     * @access     public
     * @throws     HTML_CSS_ERROR_INVALID_INPUT, HTML_CSS_ERROR_WRITE_FILE
     * @see        toString()
     */
    function toFile($filename)
    {
        if (!is_string($filename)) {
            return $this->raiseError(
                HTML_CSS_ERROR_INVALID_INPUT, 'exception',
                array('var' => '$filename',
                      'was' => gettype($filename),
                      'expected' => 'string',
                      'paramnum' => 1)
            );
        }

        if (function_exists('file_put_contents')) {
            file_put_contents($filename, $this->toString());
        } else {
            $file = fopen($filename, 'wb');
            fwrite($file, $this->toString());
            fclose($file);
        }
        if (!file_exists($filename)) {
            return $this->raiseError(
                HTML_CSS_ERROR_WRITE_FILE, 'error',
                array('filename' => $filename)
            );
        }
    }

    /**
     * Return current CSS parsed data as a string
     *
     * Generate current parsed CSS data sources and return result as a string
     *
     * @return     string
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     */
    function toString()
    {
        // get line endings
        $lnEnd = $this->_getLineEnd();
        $tabs  = $this->_getTabs();
        $tab   = $this->_getTab();

        // initialize $alibis
        $alibis = array();

        $strCss     = '';
        $strAtRules = '';

        // Allow a CSS comment
        if ($this->_comment) {
            $strCss = $tabs . '/* ' . $this->getComment() . ' */' . $lnEnd;
        }

        // If groups are to be output first, initialize a special variable
        if ($this->__get('groupsfirst')) {
            $strCssElements = '';
        }

        // bring AtRules in correct order
        $this->sortAtRules();

        // Iterate through the array and process each element
        foreach ($this->_css as $identifier => $rank) {

            // Groups are handled separately
            if (strpos($identifier, '@-') !== false) {
                // its a group
                $element = implode(', ', $this->_groups[$identifier]);
            } else {
                $element = $identifier;
            }

            if ((0 === strpos($element, '@')) && (1 !== strpos($element, '-'))) {
                // simple declarative At-Rule definition
                foreach ($rank as $arg => $decla) {
                    // check to see if it is a duplicate
                    if (is_numeric($arg)) {
                        $arg   = array_keys($decla);
                        $arg   = array_shift($arg);
                        $decla = array_values($decla);
                        $decla = array_shift($decla);
                    }
                    if (is_array($decla)) {
                        $strAtRules .= $element . ' ' . $arg;
                        foreach ($decla as $s => $d) {
                            $t = $tabs . $tab;
                            if (empty($s)) {
                                $strAtRules .= ' {' . $lnEnd;
                            } else {
                                $t          .= $tab;
                                $strAtRules .= ' {' . $lnEnd .
                                    $tab . $s . ' {' . $lnEnd;
                            }
                            foreach ($d as $p => $v) {
                                $strAtRules .= $t . $p . ': ' . $v . ';' . $lnEnd;
                            }
                            if (empty($s)) {
                                $strAtRules .= $tabs . '}';
                            } else {
                                $strAtRules .=  $tabs . $tab . '}' . $lnEnd . '}';
                            }
                        }
                        $strAtRules .=  $lnEnd . $lnEnd;;
                    } else {
                        $strAtRules .= $element . ' ' . $arg . ';' . $lnEnd . $lnEnd;
                    }
                }
            } else {
                // Start CSS element definition
                $definition = $element . ' {' . $lnEnd;

                // Iterate through the array of properties
                foreach ($rank as $pos => $property) {
                    // check to see if it is a duplicate
                    if (!is_numeric($pos)) {
                        $property = array($pos => $property);
                        unset($pos);
                    }
                    foreach ($property as $key => $value) {
                        $definition .= $tabs . $tab
                                    . $key . ': ' . $value . ';' . $lnEnd;
                    }
                }

                // end CSS element definition
                $definition .= $tabs . '}';
            }

            // if this is to be on a single line, collapse
            if ($this->options['oneline']) {
                $definition = $this->collapseInternalSpaces($definition);
                $strAtRules = $this->collapseInternalSpaces($strAtRules);
            }

            // if groups are to be output first, elements must be placed in a
            // different string which will be appended in the end
            if (isset($definition)) {
                if ($this->__get('groupsfirst') === true
                    && strpos($identifier, '@-') === false
                ) {
                    // add to elements
                    $strCssElements .= $lnEnd . $tabs . $definition . $lnEnd;
                } else {
                    // add to strCss
                    $strCss .= $lnEnd . $tabs . $definition . $lnEnd;
                }
            }
        }

        if ($this->__get('groupsfirst')) {
            $strCss .= $strCssElements;
        }

        $strAtRules = rtrim($strAtRules);
        if (!empty($strAtRules)) {
            $strAtRules .= $lnEnd;
        }
        $strCss = $strAtRules . $strCss;

        if ($this->options['oneline']) {
            $strCss = preg_replace('/(\n|\r\n|\r)/', '', $strCss);
        }

        return $strCss;
    }

    /**
     * Output CSS Code.
     *
     * Send the stylesheet content to standard output, handling cacheControl
     * and contentDisposition headers
     *
     * @return     void
     * @since      version 0.2.0 (2003-07-31)
     * @access     public
     * @see        toString()
     */
    function display()
    {
        if (!headers_sent()) {
            if ($this->__get('cache') !== true) {
                header("Expires: Tue, 1 Jan 1980 12:00:00 GMT");
                header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
                header("Cache-Control: no-cache");
                header("Pragma: no-cache");
            }

            // set character encoding
            header("Content-Type: text/css; charset=" . $this->__get('charset'));

            // set Content-Disposition
            if ($this->__get('contentDisposition') !== false) {
                header(
                    'Content-Disposition: inline; filename="' .
                    $this->__get('contentDisposition') . '"'
                );
            }
        }

        $strCss = $this->toString();
        print $strCss;
    }

    /**
     * Initialize Error engine preferences
     *
     * @param array $prefs (optional) hash of params to customize error generation
     *
     * @return     void
     * @since      version 0.3.3 (2004-05-20)
     * @access     private
     */
    function _initErrorStack($prefs = array())
    {
        // error message mapping callback
        if (isset($prefs['message_callback'])
            && is_callable($prefs['message_callback'])
        ) {
            $this->_callback_message = $prefs['message_callback'];
        } else {
            $this->_callback_message = array('HTML_CSS_Error', '_msgCallback');
        }

        // error context mapping callback
        if (isset($prefs['context_callback'])
            && is_callable($prefs['context_callback'])
        ) {
            $this->_callback_context = $prefs['context_callback'];
        } else {
            $this->_callback_context = array('HTML_CSS_Error', 'getBacktrace');
        }

        // determine whether to allow an error to be pushed or logged
        if (isset($prefs['push_callback'])
            && is_callable($prefs['push_callback'])
        ) {
            $this->_callback_push = $prefs['push_callback'];
        } else {
            $this->_callback_push = array('HTML_CSS_Error', '_handleError');
        }

        // determine whether to display or log an error by a free user function
        if (isset($prefs['error_callback'])
            && is_callable($prefs['error_callback'])
        ) {
            $this->_callback_error = $prefs['error_callback'];
        } else {
            $this->_callback_error = null;
        }

        // default error handler will use PEAR_Error
        if (isset($prefs['error_handler'])
            && is_callable($prefs['error_handler'])
        ) {
            $this->_callback_errorhandler = $prefs['error_handler'];
        } else {
            $this->_callback_errorhandler = array(&$this, '_errorHandler');
        }

        // any handler-specific settings
        if (isset($prefs['handler'])) {
            $this->_errorhandler_options = $prefs['handler'];
        }
    }

    /**
     * Standard error handler that will use PEAR_Error object
     *
     * To improve performances, the PEAR.php file is included dynamically.
     * The file is so included only when an error is triggered. So, in most
     * cases, the file isn't included and perfs are much better.
     *
     * @param integer $code   Error code.
     * @param string  $level  The error level of the message.
     * @param array   $params Associative array of error parameters
     *
     * @return     PEAR_Error
     * @since      version 1.0.0 (2006-06-24)
     * @access     private
     */
    function _errorHandler($code, $level, $params)
    {
        include_once 'HTML/CSS/Error.php';

        $mode    = call_user_func($this->_callback_push, $code, $level);
        $message = call_user_func($this->_callback_message, $code, $params);
        $options = $this->_callback_error;

        $userinfo['level'] = $level;

        if (isset($this->_errorhandler_options['display'])) {
            $userinfo['display'] = $this->_errorhandler_options['display'];
        } else {
            $userinfo['display'] = array();
        }
        if (isset($this->_errorhandler_options['log'])) {
            $userinfo['log'] = $this->_errorhandler_options['log'];
        } else {
            $userinfo['log'] = array();
        }

        return PEAR::raiseError(
            $message, $code, $mode, $options, $userinfo, 'HTML_CSS_Error'
        );
    }

    /**
     * A basic wrapper around the default PEAR_Error object
     *
     * This method is a wrapper that returns an instance of the configured
     * error class with this object's default error handling applied.
     *
     * @return     object    PEAR_Error when default error handler is used
     * @since      version 0.3.3 (2004-05-20)
     * @access     public
     * @see        _errorHandler()
     */
    function raiseError()
    {
        $args = func_get_args();
        $this->_lastError
            = call_user_func_array($this->_callback_errorhandler, $args);
        return $this->_lastError;
    }

    /**
     * Determine whether there is an error
     *
     * Determine whether last action raised an error or not
     *
     * @return     boolean               TRUE if error raised, FALSE otherwise
     * @since      version 1.0.0RC2 (2005-12-15)
     * @access     public
     */
    function isError()
    {
         $res              = (!is_bool($this->_lastError));
         $this->_lastError = false;
         return $res;
    }
}
?>