<?php

/**
 * This file is part of the Nette Framework (http://nette.texy.info/)
 *
 * @author     David Grudl
 * @copyright  Copyright (c) 2004-2007 David Grudl aka -dgx- (http://www.dgx.cz)
 * @license    New BSD License
 * @version    $Revision: 80 $ $Date: 2007-07-04 02:51:27 +0200 (st, 04 VII 2007) $
 * @category   Nette
 * @package    Nette-Html
 */



/**
 * HTML helper
 *
 * usage:
 *       $anchor = NHtml::el('a')->href($link)->setText('Nette');
 *       $el->class = 'myclass';
 *       echo $el;
 *
 *       echo $el->startTag(), $el->endTag();
 *
 * Requirements:
 *     - PHP 5.0.0
 *     - NetteException
 *     - optionally NApplication
 *
 * @property mixed element's attributes
 */
class NHtml
{
    /** @var string  element's name */
    private $name;

    /** @var bool  is element empty? */
    private $isEmpty;

    /**
     * @var mixed  element's content
     *   array of NHtml - child nodes
     *   string - content as string (text-node)
     */
    public $children;

    /** @var array  element's attributes */
    public $attrs = array();

    /** @var bool  use XHTML syntax? */
    public static $xhtml = TRUE;

    /** @var array  empty elements */
    public static $emptyTags = array('img'=>1,'hr'=>1,'br'=>1,'input'=>1,'meta'=>1,'area'=>1,
        'base'=>1,'col'=>1,'link'=>1,'param'=>1,'basefont'=>1,'frame'=>1,'isindex'=>1,'wbr'=>1,'embed'=>1);



    /**
     * Static factory
     * @param string element name (or NULL)
     * @param array element's attributes
     * @throws NHtmlException
     * @return NHtml
     */
    public static function el($name = NULL, $attrs = NULL)
    {
        $el = new self;

        if ($name !== NULL)
            $el->setName($name);

        if ($attrs !== NULL) {
            if (!is_array($attrs))
                throw new NHtmlException('Attributes must be array');

            $el->attrs = $attrs;
        }

        return $el;
    }


    /**
     * Static factory for textual element
     * @param string
     * @return NHtml
     */
    public static function text($text)
    {
        $el = new self;
        $el->setText($text);
        return $el;
    }


    /**
     * Changes element's name
     * @param string
     * @throws NHtmlException
     * @return NHtml  provides a fluent interface
     */
    public function setName($name)
    {
        if ($name !== NULL && !is_string($name))
            throw new NHtmlException("Element's name must string or NULL");

        $this->name = $name;
        $this->isEmpty = isset(self::$emptyTags[$name]);
        return $this;
    }


    /**
     * Returns element's name
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }


    /**
     * Is element empty?
     * @param optional setter
     * @return bool
     */
    public function isEmpty($value = NULL)
    {
        if (is_bool($value)) $this->isEmpty = $value;
        return $this->isEmpty;
    }


    /**
     * Sets element's textual content
     * @param string
     * @param bool is the string HTML encoded yet?
     * @throws NHtmlException
     * @return NHtml  provides a fluent interface
     */
    public function setText($text, $isHtml = FALSE)
    {
        if ($text === NULL)
            $text = '';
        elseif (!is_scalar($text))
            throw new NHtmlException("Textual content must be a scalar");

        if (!$isHtml)
            $text = str_replace(array('&', '<', '>'), array('&amp;', '&lt;', '&gt;'), $text);

        $this->children = $text;
        return $this;
    }



    /**
     * Gets element's textual content
     * @return string
     */
    public function getText()
    {
        if (is_array($this->children)) return FALSE;

        return $this->children;
    }



    /**
     * Adds new element's child
     * @param NHtml object
     * @return NHtml  provides a fluent interface
     */
    public function addChild(NHtml $child)
    {
        $this->children[] = $child;
        return $this;
    }


    /**
     * Returns child node
     * @param mixed index
     * @return NHtml
     */
    public function getChild($index)
    {
        if (isset($this->children[$index]))
            return $this->children[$index];

        return NULL;
    }


    /**
     * Adds and creates new NHtml child
     * @param string  elements's name
     * @param string optional textual content
     * @return NHtml
     */
    public function add($name, $text = NULL)
    {
        $child = new self;
        $child->setName($name);
        if ($text !== NULL) $child->setText($text);
        return $this->children[] = $child;
    }


    /**
     * Overloaded setter for element's attribute
     * @param string    property name
     * @param mixed     property value
     * @return void
     */
    public function __set($name, $value)
    {
        $this->attrs[$name] = $value;
    }


    /**
     * Overloaded getter for element's attribute
     * @param string    property name
     * @return mixed    property value
     */
    public function &__get($name)
    {
        return $this->attrs[$name];
    }


    /**
     * Overloaded setter for element's attribute
     * @param string attribute name
     * @param array value
     * @return NHtml  provides a fluent interface
     */
    public function __call($m, $args)
    {
        $this->attrs[$m] = $args[0];
        return $this;
    }



    /**
     * Special setter for element's attribute
     * @param string path
     * @param array query
     * @return NHtml  provides a fluent interface
     */
    public function href($path, $query = NULL)
    {
        // for Nette framework
        if (class_exists('NApplication', FALSE)) {
            if (substr($path, 0, 6) === 'event:') {
                if ($query === NULL) $query = array();
                $this->attrs['href'] = NApplication::getPage()->constructEvent(substr($path, 6), $query)->constructUrl();
                return $this;

            } elseif (substr($path, 0, 5) === 'page:') {
                if ($query === NULL) $query = array();
                $this->attrs['href'] = NApplication::getPage()->forwardArgs(substr($path, 5), $query)->constructUrl();
                return $this;
            }
        }

        if ($query) {
            $query = http_build_query($query, NULL, '&');
            if ($query !== '') $path .= '?' . $query;
        }
        $this->attrs['href'] = $path;
        return $this;
    }


    /**
     * Renders element's start tag, content and end tag
     * @return string
     */
    public function render()
    {
        $s = $this->startTag();

        // empty elements are finished now
        if ($this->isEmpty) return $s;

        // add content
        if (is_array($this->children)) {
            foreach ($this->children as $value)
                $s .= $value->__toString();

        } else {
            $s .= $this->children;
        }

        // add end tag
        return $s . $this->endTag();
    }


    /**
     * Returns element's start tag
     * @return string
     */
    public function startTag()
    {
        if (!$this->name) return '';

        $s = '<' . $this->name;

        if (is_array($this->attrs)) {
            foreach ($this->attrs as $key => $value)
            {
                // skip NULLs and false boolean attributes
                if ($value === NULL || $value === FALSE) continue;

                // true boolean attribute
                if ($value === TRUE) {
                    // in XHTML must use unminimized form
                    if (self::$xhtml) $s .= ' ' . $key . '="' . $key . '"';
                    // in HTML should use minimized form
                    else $s .= ' ' . $key;
                    continue;

                } elseif (is_array($value)) {

                    // prepare into temporary array
                    $tmp = NULL;
                    foreach ($value as $k => $v) {
                        // skip NULLs & empty string; composite 'style' vs. 'others'
                        if ($v == NULL) continue;

                        if (is_string($k)) $tmp[] = $k . ':' . $v;
                        else $tmp[] = $v;
                    }

                    if (!$tmp) continue;
                    $value = implode($key === 'style' ? ';' : ' ', $tmp);

                } elseif ($key === 'href' && substr($value, 0, 7) === 'mailto:') {
                    // email-obfuscate hack
                    $tmp = '';
                    for ($i = 0; $i<strlen($value); $i++) $tmp .= '&#' . ord($value[$i]) . ';'; // WARNING: no utf support
                    $s .= ' href="' . $tmp . '"';
                    continue;
                }

                // add new attribute
                $s .= ' ' . $key . '="'
                    . str_replace(array('&', '"', '<', '>'), array('&amp;', '&quot;', '&lt;', '&gt;'), $value)
                    . '"';
            }
        }

        // finish start tag
        if (self::$xhtml && $this->isEmpty) return $s . ' />';
        return $s . '>';
    }


    public function __toString()
    {
        return $this->render();
    }


    /**
     * Returns element's end tag
     * @return string
     */
    public function endTag()
    {
        if ($this->name && !$this->isEmpty)
            return '</' . $this->name . '>';
        return '';
    }


    /**
     * Is element textual node?
     * @return bool
     */
    public function isTextual()
    {
        return !$this->isEmpty && is_scalar($this->children);
    }


    /**
     * Clones all children too
     */
    public function __clone()
    {
        if (is_array($this->children)) {
            foreach ($this->children as $key => $value)
                $this->children[$key] = clone $value;
        }
    }

}
