<?php
/*
XHTML.php, XHTML utility functions.
Copyright (C) 2004-2005 Arend van Beelen, Auton Rijnsburg
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
For any questions, comments or whatever, you may mail me at: hide@address.com
*/
require_once('String.php');
require_once('Widgets.php');
/**
* @internal
*/
class XHTMLTag
{
public function __construct($name, $single, $visible, $textAllowed, $attributes = array(), $allowedParents = array(), $requiredAttributes = array())
{
$this->name = $name;
$this->single = $single;
$this->visible = $visible;
$this->textAllowed = $textAllowed;
$this->attributes = $attributes;
$this->allowedParents = $allowedParents;
$this->requiredAttributes = $requiredAttributes;
}
public $name;
public $single;
public $visible;
public $textAllowed;
public $attributes;
public $allowedParents;
public $requiredAttributes;
}
/**
* @brief Provides some utility functions for working with XHTML documents.
*/
class XHTML
{
/**
* Returns the XHTML 1.1 Doctype Declaration.
*
* @return A string containing the XHTML 1.1 Doctype Declaration (without
* newline).
*/
public static function doctype()
{
return '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
}
/**
* Converts a loose HTML document to an XHTML 1.1 document.
*
* @param document A string containing the original HTML document.
* @param root This optional parameter will be filled with the root node
* of the resulting DOM document.
* @return A @p DOMDocument tree containing the XHTML 1.1 document.
*/
public static function html2XhtmlDocument($document, &$root = NULL)
{
self::initializeTags();
$tagStack = '/html';
$readingTag = false;
$readingComment = false;
$readingEntity = false;
$numberedEntity = false;
$tagOffset = 0;
$entityOffset = 0;
$parentNodes = array();
$currentParent = 0;
$currentNode = NULL;
$xhtmlDocument = new DOMDocument("1.0");
$root = $xhtmlDocument->createElement('html');
$root->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
$xhtmlDocument->appendChild($root);
$parentNodes[$currentParent] = $root;
for($i = 0; $i < strlen($document); $i++)
{
// currently reading text
if($readingTag == false)
{
// currently reading an entity
if($readingEntity == true)
{
if($document{$i} == "#")
{
$numberedEntity = true;
}
elseif($document{$i} == ';' || $document{$i} == ' ' ||
$document{$i} == '<' || $document{$i} == '>' ||
$document{$i} == '&')
{
if($currentNode == null)
{
$currentNode = $xhtmlDocument->createTextNode('');
}
if($i - $entityOffset == 1 ||
substr($document, $entityOffset, $i - $entityOffset - 1) == '&')
{
$currentNode->appendData('&');
}
elseif($numberedEntity == true)
{
$currentNode->appendData(chr((int) substr($document, $entityOffset + 2, $i - $entityOffset - 2)));
}
$readingEntity = false;
}
}
else
{
// a tag starts
if($document{$i} == '<')
{
if($currentNode != null)
{
$parentNodes[$currentParent]->appendChild($currentNode);
$currentNode = null;
}
$readingTag = true;
$tagOffset = $i;
}
// an entity starts
elseif($document{$i} == '&')
{
$readingEntity = true;
$numberedEntity = false;
$entityOffset = $i;
}
// reading a text node
else
{
if($currentNode == null)
{
$currentNode = $xhtmlDocument->createTextNode('');
}
$currentNode->appendData($document{$i});
}
}
}
// currently reading comments
elseif($readingComment == true)
{
if(substr($document, $i - 2, 3) == '-->')
{
$readingComment = false;
}
}
// currently reading a tag
else
{
// see if we start reading comments
if($i == $tagOffset + 3 &&
substr($document, $tagOffset, 4) == '<!--')
{
$readingComment = true;
continue;
}
// the tag is finished
if($document{$i} == '>')
{
$readingTag = false;
$tag = substr($document, $tagOffset, $i - $tagOffset + 1);
preg_match('/^<\s*(\/?\w+).*>$/', $tag, $matches);
$tagName = strtolower($matches[1]);
// ending tag
if(String::startsWith($tagName, '/'))
{
if($currentParent > 0 &&
(String::endsWith($tagStack, $tagName) || strpos($tagStack, "$tagName/") !== false))
{
$tagStack = String::substringBefore($tagStack, $tagName, true);
$currentParent = substr_count($tagStack, '/') - 1;
}
if($tagName == '/html')
{
break;
}
}
// starting tag
else if($tagName != 'html')
{
if(isset(self::$tags[$tagName]) == false)
{
continue;
}
$tagStruct = self::$tags[$tagName];
// check whether the parent tag is valid
if(in_array(String::substringAfter($tagStack, '/', true),
$tagStruct->allowedParents) == false)
{
$ancestors = array_reverse(explode('/', String::substringBefore($tagStack, '/', true)));
$parentFound = false;
$previousAncestor = String::substringAfter($tagStack, '/', true);
foreach($ancestors as $ancestor)
{
if(in_array($ancestor, $tagStruct->allowedParents))
{
$tagStack = String::substringBefore($tagStack, "/$previousAncestor", true);
$currentParent = substr_count($tagStack, '/') - 1;
$parentFound = true;
}
$previousAncestor = $ancestor;
}
if($parentFound == false)
{
foreach($tagStruct->allowedParents as $parent)
{
$parentStruct = self::$tags[$parent];
if(in_array(String::substringAfter($tagStack, '/', true),
$parentStruct->allowedParents) == true)
{
$currentNode = $xhtmlDocument->createElement($parent);
$parentNodes[$currentParent]->appendChild($currentNode);
$currentParent++;
$parentNodes[$currentParent] = $currentNode;
$tagStack .= "/$parent";
$currentNode = null;
$parentFound = true;
}
}
}
if($parentFound == false)
{
continue;
}
}
$attributes = array();
while(preg_match('/^<\s*\/?\w+\s+(\w+=(?:".*?"|[^" >]+)).*>$/', $tag, $matches))
{
$attributes[] = $matches[1];
$tag = str_replace($matches[1], '', $tag);
}
$currentNode = $xhtmlDocument->createElement($tagName);
$attributeKeys = array();
foreach($attributes as $attribute)
{
$key = strtolower(String::substringBefore($attribute, '='));
$value = String::substringAfter($attribute, '=');
$attributeKeys[] = $key;
if(in_array($key, $tagStruct->attributes) == false &&
in_array($key, $tagStruct->requiredAttributes) == false)
{
continue;
}
if($value{0} == '"')
{
$value = substr($value, 1);
}
if(substr($value, -1) == '"')
{
$value = substr($value, 0, strlen($value) - 1);
}
$currentNode->setAttribute($key, $value);
}
foreach($tagStruct->requiredAttributes as $key)
{
if(in_array($key, $attributeKeys) == false)
{
$currentNode->setAttribute($key, '');
}
}
$parentNodes[$currentParent]->appendChild($currentNode);
if($tagStruct->single == false)
{
$currentParent++;
$parentNodes[$currentParent] = $currentNode;
$tagStack .= "/$tagName";
}
$currentNode = null;
}
}
}
}
return $xhtmlDocument;
}
/**
* Converts a loose HTML document to an XHTML 1.1 document.
*
* @param document A string containing the original HTML document.
* @return A string containing the complete XHTML 1.1 document.
*/
public static function html2Xhtml($document)
{
$xhtmlDocument = self::html2XhtmlDocument($document, $root);
return self::doctype()."\n".
$xhtmlDocument->saveXML($root)."\n";
}
private static function initializeTags()
{
if(isset(self::$tags))
{
return;
}
self::$tags['html'] = new XHTMLTag('html', false, true, true);
self::$tags['head'] = new XHTMLTag('head', false, true, false, array('style'), array('html'));
self::$tags['title'] = new XHTMLTag('title', false, true, true, array('style'), array('head'));
self::$tags['link'] = new XHTMLTag('link', true, true, false, array('style', 'href', 'rel', 'type'),array('head'));
self::$tags['meta'] = new XHTMLTag('meta', true, true, false, array('style', 'http-equiv', 'name'), array('head'), array('content'));
self::$tags['body'] = new XHTMLTag('body', false, true, true, array('style'), array('html'));
self::$tags['h1'] = new XHTMLTag('h1', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['h2'] = new XHTMLTag('h2', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['h3'] = new XHTMLTag('h3', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['h4'] = new XHTMLTag('h4', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['h5'] = new XHTMLTag('h5', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['h6'] = new XHTMLTag('h6', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['p'] = new XHTMLTag('p', false, true, true, array('style'), array('body', 'td', 'li', 'div'));
self::$tags['a'] = new XHTMLTag('a', false, true, true, array('style', 'href', 'name'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'strong', 'em'));
self::$tags['div'] = new XHTMLTag('div', false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'));
self::$tags['span'] = new XHTMLTag('span', false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'));
self::$tags['strong']= new XHTMLTag('strong',false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'));
self::$tags['em'] = new XHTMLTag('em', false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'));
self::$tags['br'] = new XHTMLTag('br', true, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'));
self::$tags['hr'] = new XHTMLTag('hr', true, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'));
self::$tags['img'] = new XHTMLTag('img', true, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'span', 'strong', 'em'), array('src', 'alt'));
self::$tags['table'] = new XHTMLTag('table', false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'strong', 'em'));
self::$tags['thead'] = new XHTMLTag('thead', false, true, true, array('style'), array('table'));
self::$tags['tbody'] = new XHTMLTag('tbody', false, true, true, array('style'), array('table'));
self::$tags['tfoot'] = new XHTMLTag('tfoot', false, true, true, array('style'), array('table'));
self::$tags['tr'] = new XHTMLTag('tr', false, true, true, array('style'), array('table', 'thead', 'tbody', 'tfoot'));
self::$tags['td'] = new XHTMLTag('td', false, true, true, array('style', 'colspan', 'rowspan'), array('tr'));
self::$tags['ol'] = new XHTMLTag('ol', false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'strong', 'em'));
self::$tags['ul'] = new XHTMLTag('ul', false, true, true, array('style'), array('body', 'td', 'li', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'strong', 'em'));
self::$tags['li'] = new XHTMLTag('li', false, true, true, array('style'), array('ul', 'ol'));
self::$tags['b'] = self::$tags['strong'];
self::$tags['i'] = self::$tags['em'];
//self::$tags['script']= new XHTMLTag('script',false, false, false, array(), array('head'));
//self::$tags['style'] = new XHTMLTag('style', false, false, false, array(), array('head'));
}
private static $tags;
}
/**
* @brief This widget can be used to insert arbitrary HTML contents.
*
* The contents you supply will be automatically converted to valid XHTML code
* and possibly offending tags will be removed.
*
* @sa XHTML
*/
class HTMLWidget extends RawWidget
{
/**
* Constructor.
*
* @param parent Parent container to add this widget to.
* @param contents The HTML contents to show.
*/
public function __construct(Container $parent, $contents)
{
$domDocument = XHTML::html2XhtmlDocument("<html><body>$contents</body></html>");
$xPath = new DOMXPath($domDocument);
$nodeList = $xPath->query('/html/body');
$html = substr($domDocument->saveXML($nodeList->item(0)), 6, -7);
parent::__construct($parent, "<htmlblock xmlns=\"http://www.w3.org/1999/xhtml\"><span>$html</span></htmlblock>");
}
}
?>