woocommerce/apigen/libs/Texy/texy/libs/TexyHtml.php

605 lines
12 KiB
PHP

<?php
/**
* Texy! - human-readable text to HTML converter.
*
* @copyright Copyright (c) 2004, 2010 David Grudl
* @license GNU GENERAL PUBLIC LICENSE version 2 or 3
* @link http://texy.info
* @package Texy
*/
/**
* HTML helper.
*
* usage:
* $anchor = TexyHtml::el('a')->href($link)->setText('Texy');
* $el->class = 'myclass';
*
* echo $el->startTag(), $el->endTag();
*
* @property mixed element's attributes
* @copyright Copyright (c) 2004, 2010 David Grudl
* @package Texy
*/
class TexyHtml extends TexyObject implements ArrayAccess, /* Countable, */ IteratorAggregate
{
/** @var string element's name */
private $name;
/** @var bool is element empty? */
private $isEmpty;
/** @var array element's attributes */
public $attrs = array();
/** @var array of TexyHtml | string nodes */
protected $children = array();
/** @var bool use XHTML syntax? */
public static $xhtml = TRUE;
/** @var array empty elements */
public static $emptyElements = 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);
/** @var array %inline; elements; replaced elements + br have value '1' */
public static $inlineElements = array('ins'=>0,'del'=>0,'tt'=>0,'i'=>0,'b'=>0,'big'=>0,'small'=>0,'em'=>0,
'strong'=>0,'dfn'=>0,'code'=>0,'samp'=>0,'kbd'=>0,'var'=>0,'cite'=>0,'abbr'=>0,'acronym'=>0,
'sub'=>0,'sup'=>0,'q'=>0,'span'=>0,'bdo'=>0,'a'=>0,'object'=>1,'img'=>1,'br'=>1,'script'=>1,
'map'=>0,'input'=>1,'select'=>1,'textarea'=>1,'label'=>0,'button'=>1,
'u'=>0,'s'=>0,'strike'=>0,'font'=>0,'applet'=>1,'basefont'=>0, // transitional
'embed'=>1,'wbr'=>0,'nobr'=>0,'canvas'=>1, // proprietary
);
/** @var array elements with optional end tag in HTML */
public static $optionalEnds = array('body'=>1,'head'=>1,'html'=>1,'colgroup'=>1,'dd'=>1,
'dt'=>1,'li'=>1,'option'=>1,'p'=>1,'tbody'=>1,'td'=>1,'tfoot'=>1,'th'=>1,'thead'=>1,'tr'=>1);
/** @see http://www.w3.org/TR/xhtml1/prohibitions.html */
public static $prohibits = array(
'a' => array('a','button'),
'img' => array('pre'),
'object' => array('pre'),
'big' => array('pre'),
'small' => array('pre'),
'sub' => array('pre'),
'sup' => array('pre'),
'input' => array('button'),
'select' => array('button'),
'textarea' => array('button'),
'label' => array('button', 'label'),
'button' => array('button'),
'form' => array('button', 'form'),
'fieldset' => array('button'),
'iframe' => array('button'),
'isindex' => array('button'),
);
/**
* Static factory.
* @param string element name (or NULL)
* @param array|string element's attributes (or textual content)
* @return TexyHtml
*/
public static function el($name = NULL, $attrs = NULL)
{
$el = new self;
$el->setName($name);
if (is_array($attrs)) {
$el->attrs = $attrs;
} elseif ($attrs !== NULL) {
$el->setText($attrs);
}
return $el;
}
/**
* Changes element's name.
* @param string
* @param bool Is element empty?
* @return TexyHtml provides a fluent interface
* @throws InvalidArgumentException
*/
final public function setName($name, $empty = NULL)
{
if ($name !== NULL && !is_string($name)) {
throw new InvalidArgumentException("Name must be string or NULL.");
}
$this->name = $name;
$this->isEmpty = $empty === NULL ? isset(self::$emptyElements[$name]) : (bool) $empty;
return $this;
}
/**
* Returns element's name.
* @return string
*/
final public function getName()
{
return $this->name;
}
/**
* Is element empty?
* @return bool
*/
final public function isEmpty()
{
return $this->isEmpty;
}
/**
* Overloaded setter for element's attribute.
* @param string property name
* @param mixed property value
* @return void
*/
final public function __set($name, $value)
{
$this->attrs[$name] = $value;
}
/**
* Overloaded getter for element's attribute.
* @param string property name
* @return mixed property value
*/
final public function &__get($name)
{
return $this->attrs[$name];
}
/**
* Overloaded setter for element's attribute.
* @param string attribute name
* @param array value
* @return TexyHtml provides a fluent interface
*/
/*
final public function __call($m, $args)
{
if (count($args) !== 1) {
throw new InvalidArgumentException("Just one argument is required.");
}
$this->attrs[$m] = $args[0];
return $this;
}
*/
/**
* Special setter for element's attribute.
* @param string path
* @param array query
* @return TexyHtml provides a fluent interface
*/
final public function href($path, $query = NULL)
{
if ($query) {
$query = http_build_query($query, NULL, '&');
if ($query !== '') $path .= '?' . $query;
}
$this->attrs['href'] = $path;
return $this;
}
/**
* Sets element's textual content.
* @param string
* @return TexyHtml provides a fluent interface
*/
final public function setText($text)
{
if (is_scalar($text)) {
$this->removeChildren();
$this->children = array($text);
} elseif ($text !== NULL) {
throw new InvalidArgumentException('Content must be scalar.');
}
return $this;
}
/**
* Gets element's textual content.
* @return string
*/
final public function getText()
{
$s = '';
foreach ($this->children as $child) {
if (is_object($child)) return FALSE;
$s .= $child;
}
return $s;
}
/**
* Adds new element's child.
* @param TexyHtml|string child node
* @return TexyHtml provides a fluent interface
*/
final public function add($child)
{
return $this->insert(NULL, $child);
}
/**
* Creates and adds a new TexyHtml child.
* @param string elements's name
* @param array|string element's attributes (or textual content)
* @return TexyHtml created element
*/
final public function create($name, $attrs = NULL)
{
$this->insert(NULL, $child = self::el($name, $attrs));
return $child;
}
/**
* Inserts child node.
* @param int
* @param TexyHtml node
* @param bool
* @return TexyHtml provides a fluent interface
* @throws Exception
*/
public function insert($index, $child, $replace = FALSE)
{
if ($child instanceof TexyHtml || is_string($child)) {
if ($index === NULL) { // append
$this->children[] = $child;
} else { // insert or replace
array_splice($this->children, (int) $index, $replace ? 1 : 0, array($child));
}
} else {
throw new /*::*/InvalidArgumentException('Child node must be scalar or TexyHtml object.');
}
return $this;
}
/**
* Inserts (replaces) child node (ArrayAccess implementation).
* @param int
* @param TexyHtml node
* @return void
*/
final public function offsetSet($index, $child)
{
$this->insert($index, $child, TRUE);
}
/**
* Returns child node (ArrayAccess implementation).
* @param int index
* @return mixed
*/
final public function offsetGet($index)
{
return $this->children[$index];
}
/**
* Exists child node? (ArrayAccess implementation).
* @param int index
* @return bool
*/
final public function offsetExists($index)
{
return isset($this->children[$index]);
}
/**
* Removes child node (ArrayAccess implementation).
* @param int index
* @return void
*/
public function offsetUnset($index)
{
if (isset($this->children[$index])) {
array_splice($this->children, (int) $index, 1);
}
}
/**
* Required by the Countable interface.
* @return int
*/
final public function count()
{
return count($this->children);
}
/**
* Removed all children.
* @return void
*/
public function removeChildren()
{
$this->children = array();
}
/**
* Required by the IteratorAggregate interface.
* @return ArrayIterator
*/
final public function getIterator()
{
return new ArrayIterator($this->children);
}
/**
* Returns all of children.
* return array
*/
final public function getChildren()
{
return $this->children;
}
/**
* Renders element's start tag, content and end tag to internal string representation.
* @param Texy
* @return string
*/
final public function toString(Texy $texy)
{
$ct = $this->getContentType();
$s = $texy->protect($this->startTag(), $ct);
// empty elements are finished now
if ($this->isEmpty) {
return $s;
}
// add content
foreach ($this->children as $child) {
if (is_object($child)) {
$s .= $child->toString($texy);
} else {
$s .= $child;
}
}
// add end tag
return $s . $texy->protect($this->endTag(), $ct);
}
/**
* Renders to final HTML.
* @param Texy
* @return string
*/
final public function toHtml(Texy $texy)
{
return $texy->stringToHtml($this->toString($texy));
}
/**
* Renders to final text.
* @param Texy
* @return string
*/
final public function toText(Texy $texy)
{
return $texy->stringToText($this->toString($texy));
}
/**
* 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);
} else {
$value = (string) $value;
}
// add new attribute
$value = str_replace(array('&', '"', '<', '>', '@'), array('&amp;', '&quot;', '&lt;', '&gt;', '&#64;'), $value);
$s .= ' ' . $key . '="' . Texy::freezeSpaces($value) . '"';
}
}
// finish start tag
if (self::$xhtml && $this->isEmpty) {
return $s . ' />';
}
return $s . '>';
}
/**
* Returns element's end tag.
* @return string
*/
public function endTag()
{
if ($this->name && !$this->isEmpty) {
return '</' . $this->name . '>';
}
return '';
}
/**
* Clones all children too.
*/
public function __clone()
{
foreach ($this->children as $key => $value) {
if (is_object($value)) {
$this->children[$key] = clone $value;
}
}
}
/**
* @return int
*/
final public function getContentType()
{
if (!isset(self::$inlineElements[$this->name])) return Texy::CONTENT_BLOCK;
return self::$inlineElements[$this->name] ? Texy::CONTENT_REPLACED : Texy::CONTENT_MARKUP;
}
/**
* @param array
* @return void
*/
final public function validateAttrs($dtd)
{
if (isset($dtd[$this->name])) {
$allowed = $dtd[$this->name][0];
if (is_array($allowed)) {
foreach ($this->attrs as $attr => $foo) {
if (!isset($allowed[$attr])) unset($this->attrs[$attr]);
}
}
}
}
public function validateChild($child, $dtd)
{
if (isset($dtd[$this->name])) {
if ($child instanceof TexyHtml) $child = $child->name;
return isset($dtd[$this->name][1][$child]);
} else {
return TRUE; // unknown element
}
}
/**
* Parses text as single line.
* @param Texy
* @param string
* @return void
*/
final public function parseLine(Texy $texy, $s)
{
// TODO!
// special escape sequences
$s = str_replace(array('\)', '\*'), array('&#x29;', '&#x2A;'), $s);
$parser = new TexyLineParser($texy, $this);
$parser->parse($s);
return $parser;
}
/**
* Parses text as block.
* @param Texy
* @param string
* @param bool
* @return void
*/
final public function parseBlock(Texy $texy, $s, $indented = FALSE)
{
$parser = new TexyBlockParser($texy, $this, $indented);
$parser->parse($s);
}
}