code:
<?php
/**
 * @file
 * The rules for generating output in the serializer.
 *
 * These output rules are likely to generate output similar to the document that
 * was parsed. It is not intended to output exactly the document that was parsed.
 */

namespace Masterminds\HTML5\Serializer;

use Masterminds\HTML5\Elements;

/**
 * Generate the output html5 based on element rules.
 */
class OutputRules implements RulesInterface
{
    /**
     * Defined in http://www.w3.org/TR/html51/infrastructure.html#html-namespace-0.
     */
    const NAMESPACE_HTML = 'http://www.w3.org/1999/xhtml';

    const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';

    const NAMESPACE_SVG = 'http://www.w3.org/2000/svg';

    const NAMESPACE_XLINK = 'http://www.w3.org/1999/xlink';

    const NAMESPACE_XML = 'http://www.w3.org/XML/1998/namespace';

    const NAMESPACE_XMLNS = 'http://www.w3.org/2000/xmlns/';

    /**
     * Holds the HTML5 element names that causes a namespace switch.
     *
     * @var array
     */
    protected $implicitNamespaces = array(
        self::NAMESPACE_HTML,
        self::NAMESPACE_SVG,
        self::NAMESPACE_MATHML,
        self::NAMESPACE_XML,
        self::NAMESPACE_XMLNS,
    );

    const IM_IN_HTML = 1;

    const IM_IN_SVG = 2;

    const IM_IN_MATHML = 3;

    /**
     * Used as cache to detect if is available ENT_HTML5.
     *
     * @var bool
     */
    private $hasHTML5 = false;

    protected $traverser;

    protected $encode = false;

    protected $out;

    protected $outputMode;

    private $xpath;

    protected $nonBooleanAttributes = array(
        /*
        array(
            'nodeNamespace'=>'http://www.w3.org/1999/xhtml',
            'attrNamespace'=>'http://www.w3.org/1999/xhtml',

            'nodeName'=>'img', 'nodeName'=>array('img', 'a'),
            'attrName'=>'alt', 'attrName'=>array('title', 'alt'),
        ),
        */
        array(
            'nodeNamespace' => 'http://www.w3.org/1999/xhtml',
            'attrName' => array('href',
                'hreflang',
                'http-equiv',
                'icon',
                'id',
                'keytype',
                'kind',
                'label',
                'lang',
                'language',
                'list',
                'maxlength',
                'media',
                'method',
                'name',
                'placeholder',
                'rel',
                'rows',
                'rowspan',
                'sandbox',
                'spellcheck',
                'scope',
                'seamless',
                'shape',
                'size',
                'sizes',
                'span',
                'src',
                'srcdoc',
                'srclang',
                'srcset',
                'start',
                'step',
                'style',
                'summary',
                'tabindex',
                'target',
                'title',
                'type',
                'value',
                'width',
                'border',
                'charset',
                'cite',
                'class',
                'code',
                'codebase',
                'color',
                'cols',
                'colspan',
                'content',
                'coords',
                'data',
                'datetime',
                'default',
                'dir',
                'dirname',
                'enctype',
                'for',
                'form',
                'formaction',
                'headers',
                'height',
                'accept',
                'accept-charset',
                'accesskey',
                'action',
                'align',
                'alt',
                'bgcolor',
            ),
        ),
        array(
            'nodeNamespace' => 'http://www.w3.org/1999/xhtml',
            'xpath' => 'starts-with(local-name(), \'data-\')',
        ),
    );

    const DOCTYPE = '<!DOCTYPE html>';

    public function __construct($output, $options = array())
    {
        if (isset($options['encode_entities'])) {
            $this->encode = $options['encode_entities'];
        }

        $this->outputMode = static::IM_IN_HTML;
        $this->out = $output;
        $this->hasHTML5 = defined('ENT_HTML5');
    }

    public function addRule(array $rule)
    {
        $this->nonBooleanAttributes[] = $rule;
    }

    public function setTraverser(Traverser $traverser)
    {
        $this->traverser = $traverser;

        return $this;
    }

    public function document($dom)
    {
        $this->doctype();
        if ($dom->documentElement) {
            foreach ($dom->childNodes as $node) {
                $this->traverser->node($node);
            }
            $this->nl();
        }
    }

    protected function doctype()
    {
        $this->wr(static::DOCTYPE);
        $this->nl();
    }

    public function element($ele)
    {
        $name = $ele->tagName;

        // Per spec:
        // If the element has a declared namespace in the HTML, MathML or
        // SVG namespaces, we use the lname instead of the tagName.
        if ($this->traverser->isLocalElement($ele)) {
            $name = $ele->localName;
        }

        // If we are in SVG or MathML there is special handling.
        // Using if/elseif instead of switch because it's faster in PHP.
        if ('svg' == $name) {
            $this->outputMode = static::IM_IN_SVG;
            $name = Elements::normalizeSvgElement($name);
        } elseif ('math' == $name) {
            $this->outputMode = static::IM_IN_MATHML;
        }

        $this->openTag($ele);
        if (Elements::isA($name, Elements::TEXT_RAW)) {
            foreach ($ele->childNodes as $child) {
                if ($child instanceof \DOMCharacterData) {
                    $this->wr($child->data);
                } elseif ($child instanceof \DOMElement) {
                    $this->element($child);
                }
            }
        } else {
            // Handle children.
            if ($ele->hasChildNodes()) {
                $this->traverser->children($ele->childNodes);
            }

            // Close out the SVG or MathML special handling.
            if ('svg' == $name || 'math' == $name) {
                $this->outputMode = static::IM_IN_HTML;
            }
        }

        // If not unary, add a closing tag.
        if (!Elements::isA($name, Elements::VOID_TAG)) {
            $this->closeTag($ele);
        }
    }

    /**
     * Write a text node.
     *
     * @param \DOMText $ele The text node to write.
     */
    public function text($ele)
    {
        if (isset($ele->parentNode) && isset($ele->parentNode->tagName) && Elements::isA($ele->parentNode->localName, Elements::TEXT_RAW)) {
            $this->wr($ele->data);

            return;
        }

        // FIXME: This probably needs some flags set.
        $this->wr($this->enc($ele->data));
    }

    public function cdata($ele)
    {
        // This encodes CDATA.
        $this->wr($ele->ownerDocument->saveXML($ele));
    }

    public function comment($ele)
    {
        // These produce identical output.
        // $this->wr('<!--')->wr($ele->data)->wr('-->');
        $this->wr($ele->ownerDocument->saveXML($ele));
    }

    public function processorInstruction($ele)
    {
        $this->wr('<?')
            ->wr($ele->target)
            ->wr(' ')
            ->wr($ele->data)
            ->wr('?>');
    }

    /**
     * Write the namespace attributes.
     *
     * @param \DOMNode $ele The element being written.
     */
    protected function namespaceAttrs($ele)
    {
        if (!$this->xpath || $this->xpath->document !== $ele->ownerDocument) {
            $this->xpath = new \DOMXPath($ele->ownerDocument);
        }

        foreach ($this->xpath->query('namespace::*[not(.=../../namespace::*)]', $ele) as $nsNode) {
            if (!in_array($nsNode->nodeValue, $this->implicitNamespaces)) {
                $this->wr(' ')->wr($nsNode->nodeName)->wr('="')->wr($nsNode->nodeValue)->wr('"');
            }
        }
    }

    /**
     * Write the opening tag.
     *
     * Tags for HTML, MathML, and SVG are in the local name. Otherwise, use the
     * qualified name (8.3).
     *
     * @param \DOMNode $ele The element being written.
     */
    protected function openTag($ele)
    {
        $this->wr('<')->wr($this->traverser->isLocalElement($ele) ? $ele->localName : $ele->tagName);

        $this->attrs($ele);
        $this->namespaceAttrs($ele);

        if ($this->outputMode == static::IM_IN_HTML) {
            $this->wr('>');
        }         // If we are not in html mode we are in SVG, MathML, or XML embedded content.
        else {
            if ($ele->hasChildNodes()) {
                $this->wr('>');
            }             // If there are no children this is self closing.
            else {
                $this->wr(' />');
            }
        }
    }

    protected function attrs($ele)
    {
        // FIXME: Needs support for xml, xmlns, xlink, and namespaced elements.
        if (!$ele->hasAttributes()) {
            return $this;
        }

        // TODO: Currently, this always writes name="value", and does not do
        // value-less attributes.
        $map = $ele->attributes;
        $len = $map->length;
        for ($i = 0; $i < $len; ++$i) {
            $node = $map->item($i);
            $val = $this->enc($node->value, true);

            // XXX: The spec says that we need to ensure that anything in
            // the XML, XMLNS, or XLink NS's should use the canonical
            // prefix. It seems that DOM does this for us already, but there
            // may be exceptions.
            $name = $node->nodeName;

            // Special handling for attributes in SVG and MathML.
            // Using if/elseif instead of switch because it's faster in PHP.
            if ($this->outputMode == static::IM_IN_SVG) {
                $name = Elements::normalizeSvgAttribute($name);
            } elseif ($this->outputMode == static::IM_IN_MATHML) {
                $name = Elements::normalizeMathMlAttribute($name);
            }

            $this->wr(' ')->wr($name);

            if ((isset($val) && '' !== $val) || $this->nonBooleanAttribute($node)) {
                $this->wr('="')->wr($val)->wr('"');
            }
        }
    }

    protected function nonBooleanAttribute(\DOMAttr $attr)
    {
        $ele = $attr->ownerElement;
        foreach ($this->nonBooleanAttributes as $rule) {
            if (isset($rule['nodeNamespace']) && $rule['nodeNamespace'] !== $ele->namespaceURI) {
                continue;
            }
            if (isset($rule['attNamespace']) && $rule['attNamespace'] !== $attr->namespaceURI) {
                continue;
            }
            if (isset($rule['nodeName']) && !is_array($rule['nodeName']) && $rule['nodeName'] !== $ele->localName) {
                continue;
            }
            if (isset($rule['nodeName']) && is_array($rule['nodeName']) && !in_array($ele->localName, $rule['nodeName'], true)) {
                continue;
            }
            if (isset($rule['attrName']) && !is_array($rule['attrName']) && $rule['attrName'] !== $attr->localName) {
                continue;
            }
            if (isset($rule['attrName']) && is_array($rule['attrName']) && !in_array($attr->localName, $rule['attrName'], true)) {
                continue;
            }
            if (isset($rule['xpath'])) {
                $xp = $this->getXPath($attr);
                if (isset($rule['prefixes'])) {
                    foreach ($rule['prefixes'] as $nsPrefix => $ns) {
                        $xp->registerNamespace($nsPrefix, $ns);
                    }
                }
                if (!$xp->evaluate($rule['xpath'], $attr)) {
                    continue;
                }
            }

            return true;
        }

        return false;
    }

    private function getXPath(\DOMNode $node)
    {
        if (!$this->xpath) {
            $this->xpath = new \DOMXPath($node->ownerDocument);
        }

        return $this->xpath;
    }

    /**
     * Write the closing tag.
     *
     * Tags for HTML, MathML, and SVG are in the local name. Otherwise, use the
     * qualified name (8.3).
     *
     * @param \DOMNode $ele The element being written.
     */
    protected function closeTag($ele)
    {
        if ($this->outputMode == static::IM_IN_HTML || $ele->hasChildNodes()) {
            $this->wr('</')->wr($this->traverser->isLocalElement($ele) ? $ele->localName : $ele->tagName)->wr('>');
        }
    }

    /**
     * Write to the output.
     *
     * @param string $text The string to put into the output
     *
     * @return $this
     */
    protected function wr($text)
    {
        fwrite($this->out, $text);

        return $this;
    }

    /**
     * Write a new line character.
     *
     * @return $this
     */
    protected function nl()
    {
        fwrite($this->out, PHP_EOL);

        return $this;
    }

    /**
     * Encode text.
     *
     * When encode is set to false, the default value, the text passed in is
     * escaped per section 8.3 of the html5 spec. For details on how text is
     * escaped see the escape() method.
     *
     * When encoding is set to true the text is converted to named character
     * references where appropriate. Section 8.1.4 Character references of the
     * html5 spec refers to using named character references. This is useful for
     * characters that can't otherwise legally be used in the text.
     *
     * The named character references are listed in section 8.5.
     *
     * @see http://www.w3.org/TR/2013/CR-html5-20130806/syntax.html#named-character-references True encoding will turn all named character references into their entities.
     *      This includes such characters as +.# and many other common ones. By default
     *      encoding here will just escape &'<>".
     *
     *      Note, PHP 5.4+ has better html5 encoding.
     *
     * @todo Use the Entities class in php 5.3 to have html5 entities.
     *
     * @param string $text      Text to encode.
     * @param bool   $attribute True if we are encoding an attrubute, false otherwise.
     *
     * @return string The encoded text.
     */
    protected function enc($text, $attribute = false)
    {
        // Escape the text rather than convert to named character references.
        if (!$this->encode) {
            return $this->escape($text, $attribute);
        }

        // If we are in PHP 5.4+ we can use the native html5 entity functionality to
        // convert the named character references.

        if ($this->hasHTML5) {
            return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false);
        }         // If a version earlier than 5.4 html5 entities are not entirely handled.
        // This manually handles them.
        else {
            return strtr($text, HTML5Entities::$map);
        }
    }

    /**
     * Escape test.
     *
     * According to the html5 spec section 8.3 Serializing HTML fragments, text
     * within tags that are not style, script, xmp, iframe, noembed, and noframes
     * need to be properly escaped.
     *
     * The & should be converted to &, no breaking space unicode characters
     * converted to  , when in attribute mode the " should be converted to
     * ", and when not in attribute mode the < and > should be converted to
     * < and >.
     *
     * @see http://www.w3.org/TR/2013/CR-html5-20130806/syntax.html#escapingString
     *
     * @param string $text      Text to escape.
     * @param bool   $attribute True if we are escaping an attrubute, false otherwise.
     */
    protected function escape($text, $attribute = false)
    {
        // Not using htmlspecialchars because, while it does escaping, it doesn't
        // match the requirements of section 8.5. For example, it doesn't handle
        // non-breaking spaces.
        if ($attribute) {
            $replace = array(
                '"' => '"',
                '&' => '&',
                "\xc2\xa0" => ' ',
            );
        } else {
            $replace = array(
                '<' => '<',
                '>' => '>',
                '&' => '&',
                "\xc2\xa0" => ' ',
            );
        }

        return strtr($text, $replace);
    }
}


code:
# The Serializer (Writer) Model

The serializer roughly follows sections _8.1 Writing HTML documents_ and section
_8.3 Serializing HTML fragments_ by converting DOMDocument, DOMDocumentFragment,
and DOMNodeList into HTML5.

       [ HTML5 ]   // Interface for saving.
          ||
     [ Traverser ]   // Walk the DOM
          ||
       [ Rules ]     // Convert DOM elements into strings.
          ||
       [ HTML5 ]     // HTML5 document or fragment in text.

## HTML5 Class

Provides the top level interface for saving.

## The Traverser

Walks the DOM finding each element and passing it off to the output rules to
convert to HTML5.

## Output Rules

The output rules are defined in the RulesInterface which can have multiple
implementations. Currently, the OutputRules is the default implementation that
converts a DOM as is into HTML5.

## HTML5 String

The output of the process it HTML5 as a string or saved to a file.

repo_name: spookie/gnu

license: mit


code:
<?php
/**
 * @file
 * The interface definition for Rules to generate output.
 */

namespace Masterminds\HTML5\Serializer;

/**
 * To create a new rule set for writing output the RulesInterface needs to be implemented.
 * The resulting class can be specified in the options with the key of rules.
 *
 * For an example implementation see Serializer\OutputRules.
 */
interface RulesInterface
{
    /**
     * The class constructor.
     *
     * Note, before the rules can be used a traverser must be registered.
     *
     * @param mixed $output  The output stream to write output to.
     * @param array $options An array of options.
     */
    public function __construct($output, $options = array());

    /**
     * Register the traverser used in but the rules.
     *
     * Note, only one traverser can be used by the rules.
     *
     * @param Traverser $traverser The traverser used in the rules.
     *
     * @return RulesInterface $this for the current object.
     */
    public function setTraverser(Traverser $traverser);

    /**
     * Write a document element (\DOMDocument).
     *
     * Instead of returning the result write it to the output stream ($output)
     * that was passed into the constructor.
     *
     * @param \DOMDocument $dom
     */
    public function document($dom);

    /**
     * Write an element.
     *
     * Instead of returning the result write it to the output stream ($output)
     * that was passed into the constructor.
     *
     * @param mixed $ele
     */
    public function element($ele);

    /**
     * Write a text node.
     *
     * Instead of returning the result write it to the output stream ($output)
     * that was passed into the constructor.
     *
     * @param mixed $ele
     */
    public function text($ele);

    /**
     * Write a CDATA node.
     *
     * Instead of returning the result write it to the output stream ($output)
     * that was passed into the constructor.
     *
     * @param mixed $ele
     */
    public function cdata($ele);

    /**
     * Write a comment node.
     *
     * Instead of returning the result write it to the output stream ($output)
     * that was passed into the constructor.
     *
     * @param mixed $ele
     */
    public function comment($ele);

    /**
     * Write a processor instruction.
     *
     * To learn about processor instructions see InstructionProcessor
     *
     * Instead of returning the result write it to the output stream ($output)
     * that was passed into the constructor.
     *
     * @param mixed $ele
     */
    public function processorInstruction($ele);
}


code:
<?php

namespace Masterminds\HTML5\Serializer;

/**
 * Traverser for walking a DOM tree.
 *
 * This is a concrete traverser designed to convert a DOM tree into an
 * HTML5 document. It is not intended to be a generic DOMTreeWalker
 * implementation.
 *
 * @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#serializing-html-fragments
 */
class Traverser
{
    /**
     * Namespaces that should be treated as "local" to HTML5.
     */
    protected static $local_ns = array(
        'http://www.w3.org/1999/xhtml' => 'html',
        'http://www.w3.org/1998/Math/MathML' => 'math',
        'http://www.w3.org/2000/svg' => 'svg',
    );

    protected $dom;

    protected $options;

    protected $encode = false;

    protected $rules;

    protected $out;

    /**
     * Create a traverser.
     *
     * @param \DOMNode|\DOMNodeList $dom     The document or node to traverse.
     * @param resource              $out     A stream that allows writing. The traverser will output into this
     *                                       stream.
     * @param array                 $options An array of options for the traverser as key/value pairs. These include:
     *                                       - encode_entities: A bool to specify if full encding should happen for all named
     *                                       charachter references. Defaults to false which escapes &'<>".
     *                                       - output_rules: The path to the class handling the output rules.
     */
    public function __construct($dom, $out, RulesInterface $rules, $options = array())
    {
        $this->dom = $dom;
        $this->out = $out;
        $this->rules = $rules;
        $this->options = $options;

        $this->rules->setTraverser($this);
    }

    /**
     * Tell the traverser to walk the DOM.
     *
     * @return resource $out Returns the output stream.
     */
    public function walk()
    {
        if ($this->dom instanceof \DOMDocument) {
            $this->rules->document($this->dom);
        } elseif ($this->dom instanceof \DOMDocumentFragment) {
            // Document fragments are a special case. Only the children need to
            // be serialized.
            if ($this->dom->hasChildNodes()) {
                $this->children($this->dom->childNodes);
            }
        }        // If NodeList, loop
        elseif ($this->dom instanceof \DOMNodeList) {
            // If this is a NodeList of DOMDocuments this will not work.
            $this->children($this->dom);
        }         // Else assume this is a DOMNode-like datastructure.
        else {
            $this->node($this->dom);
        }

        return $this->out;
    }

    /**
     * Process a node in the DOM.
     *
     * @param mixed $node A node implementing \DOMNode.
     */
    public function node($node)
    {
        // A listing of types is at http://php.net/manual/en/dom.constants.php
        switch ($node->nodeType) {
            case XML_ELEMENT_NODE:
                $this->rules->element($node);
                break;
            case XML_TEXT_NODE:
                $this->rules->text($node);
                break;
            case XML_CDATA_SECTION_NODE:
                $this->rules->cdata($node);
                break;
            case XML_PI_NODE:
                $this->rules->processorInstruction($node);
                break;
            case XML_COMMENT_NODE:
                $this->rules->comment($node);
                break;
            // Currently we don't support embedding DTDs.
            default:
                //print '<!-- Skipped -->';
                break;
        }
    }

    /**
     * Walk through all the nodes on a node list.
     *
     * @param \DOMNodeList $nl A list of child elements to walk through.
     */
    public function children($nl)
    {
        foreach ($nl as $node) {
            $this->node($node);
        }
    }

    /**
     * Is an element local?
     *
     * @param mixed $ele An element that implement \DOMNode.
     *
     * @return bool true if local and false otherwise.
     */
    public function isLocalElement($ele)
    {
        $uri = $ele->namespaceURI;
        if (empty($uri)) {
            return false;
        }

        return isset(static::$local_ns[$uri]);
    }
}


code:
<?php

namespace Masterminds\HTML5\Tests;

use Masterminds\HTML5\Elements;

class ElementsTest extends TestCase
{
    public $html5Elements = array(
        'a',
        'abbr',
        'address',
        'area',
        'article',
        'aside',
        'audio',
        'b',
        'base',
        'bdi',
        'bdo',
        'blockquote',
        'body',
        'br',
        'button',
        'canvas',
        'caption',
        'cite',
        'code',
        'col',
        'colgroup',
        'command',
        // "data",
        'datalist',
        'dd',
        'del',
        'details',
        'dfn',
        'dialog',
        'div',
        'dl',
        'dt',
        'em',
        'embed',
        'fieldset',
        'figcaption',
        'figure',
        'footer',
        'form',
        'h1',
        'h2',
        'h3',
        'h4',
        'h5',
        'h6',
        'head',
        'header',
        'hgroup',
        'hr',
        'html',
        'i',
        'iframe',
        'img',
        'input',
        'ins',
        'kbd',
        'keygen',
        'label',
        'legend',
        'li',
        'link',
        'map',
        'mark',
        'menu',
        'meta',
        'meter',
        'nav',
        'noscript',
        'object',
        'ol',
        'optgroup',
        'option',
        'output',
        'p',
        'param',
        'pre',
        'progress',
        'q',
        'rp',
        'rt',
        'ruby',
        's',
        'samp',
        'script',
        'section',
        'select',
        'small',
        'source',
        'span',
        'strong',
        'style',
        'sub',
        'summary',
        'sup',
        'table',
        'tbody',
        'td',
        'textarea',
        'tfoot',
        'th',
        'thead',
        'time',
        'title',
        'tr',
        'track',
        'u',
        'ul',
        'var',
        'video',
        'wbr',
    );

    public $mathmlElements = array(
        'maction',
        'maligngroup',
        'malignmark',
        'math',
        'menclose',
        'merror',
        'mfenced',
        'mfrac',
        'mglyph',
        'mi',
        'mlabeledtr',
        'mlongdiv',
        'mmultiscripts',
        'mn',
        'mo',
        'mover',
        'mpadded',
        'mphantom',
        'mroot',
        'mrow',
        'ms',
        'mscarries',
        'mscarry',
        'msgroup',
        'msline',
        'mspace',
        'msqrt',
        'msrow',
        'mstack',
        'mstyle',
        'msub',
        'msup',
        'msubsup',
        'mtable',
        'mtd',
        'mtext',
        'mtr',
        'munder',
        'munderover',
    );

    public $svgElements = array(
        'a',
        'altGlyph',
        'altGlyphDef',
        'altGlyphItem',
        'animate',
        'animateColor',
        'animateMotion',
        'animateTransform',
        'circle',
        'clipPath',
        'color-profile',
        'cursor',
        'defs',
        'desc',
        'ellipse',
        'feBlend',
        'feColorMatrix',
        'feComponentTransfer',
        'feComposite',
        'feConvolveMatrix',
        'feDiffuseLighting',
        'feDisplacementMap',
        'feDistantLight',
        'feFlood',
        'feFuncA',
        'feFuncB',
        'feFuncG',
        'feFuncR',
        'feGaussianBlur',
        'feImage',
        'feMerge',
        'feMergeNode',
        'feMorphology',
        'feOffset',
        'fePointLight',
        'feSpecularLighting',
        'feSpotLight',
        'feTile',
        'feTurbulence',
        'filter',
        'font',
        'font-face',
        'font-face-format',
        'font-face-name',
        'font-face-src',
        'font-face-uri',
        'foreignObject',
        'g',
        'glyph',
        'glyphRef',
        'hkern',
        'image',
        'line',
        'linearGradient',
        'marker',
        'mask',
        'metadata',
        'missing-glyph',
        'mpath',
        'path',
        'pattern',
        'polygon',
        'polyline',
        'radialGradient',
        'rect',
        'script',
        'set',
        'stop',
        'style',
        'svg',
        'switch',
        'symbol',
        'text',
        'textPath',
        'title',
        'tref',
        'tspan',
        'use',
        'view',
        'vkern',
    );

    public function testIsHtml5Element()
    {
        foreach ($this->html5Elements as $element) {
            $this->assertTrue(Elements::isHtml5Element($element), 'html5 element test failed on: ' . $element);

            $this->assertTrue(Elements::isHtml5Element(strtoupper($element)), 'html5 element test failed on: ' . strtoupper($element));
        }

        $nonhtml5 = array(
            'foo',
            'bar',
            'baz',
        );
        foreach ($nonhtml5 as $element) {
            $this->assertFalse(Elements::isHtml5Element($element), 'html5 element test failed on: ' . $element);

            $this->assertFalse(Elements::isHtml5Element(strtoupper($element)), 'html5 element test failed on: ' . strtoupper($element));
        }
    }

    public function testIsMathMLElement()
    {
        foreach ($this->mathmlElements as $element) {
            $this->assertTrue(Elements::isMathMLElement($element), 'MathML element test failed on: ' . $element);

            // MathML is case sensitive so these should all fail.
            $this->assertFalse(Elements::isMathMLElement(strtoupper($element)), 'MathML element test failed on: ' . strtoupper($element));
        }

        $nonMathML = array(
            'foo',
            'bar',
            'baz',
        );
        foreach ($nonMathML as $element) {
            $this->assertFalse(Elements::isMathMLElement($element), 'MathML element test failed on: ' . $element);
        }
    }

    public function testIsSvgElement()
    {
        foreach ($this->svgElements as $element) {
            $this->assertTrue(Elements::isSvgElement($element), 'SVG element test failed on: ' . $element);

            // SVG is case sensitive so these should all fail.
            $this->assertFalse(Elements::isSvgElement(strtoupper($element)), 'SVG element test failed on: ' . strtoupper($element));
        }

        $nonSVG = array(
            'foo',
            'bar',
            'baz',
        );
        foreach ($nonSVG as $element) {
            $this->assertFalse(Elements::isSvgElement($element), 'SVG element test failed on: ' . $element);
        }
    }

    public function testIsElement()
    {
        foreach ($this->html5Elements as $element) {
            $this->assertTrue(Elements::isElement($element), 'html5 element test failed on: ' . $element);

            $this->assertTrue(Elements::isElement(strtoupper($element)), 'html5 element test failed on: ' . strtoupper($element));
        }

        foreach ($this->mathmlElements as $element) {
            $this->assertTrue(Elements::isElement($element), 'MathML element test failed on: ' . $element);

            // MathML is case sensitive so these should all fail.
            $this->assertFalse(Elements::isElement(strtoupper($element)), 'MathML element test failed on: ' . strtoupper($element));
        }

        foreach ($this->svgElements as $element) {
            $this->assertTrue(Elements::isElement($element), 'SVG element test failed on: ' . $element);

            // SVG is case sensitive so these should all fail. But, there is duplication
            // html5 and SVG. Since html5 is case insensitive we need to make sure
            // it's not a html5 element first.
            if (!in_array($element, $this->html5Elements)) {
                $this->assertFalse(Elements::isElement(strtoupper($element)), 'SVG element test failed on: ' . strtoupper($element));
            }
        }

        $nonhtml5 = array(
            'foo',
            'bar',
            'baz',
        );
        foreach ($nonhtml5 as $element) {
            $this->assertFalse(Elements::isElement($element), 'html5 element test failed on: ' . $element);

            $this->assertFalse(Elements::isElement(strtoupper($element)), 'html5 element test failed on: ' . strtoupper($element));
        }
    }

    public function testElement()
    {
        foreach ($this->html5Elements as $element) {
            $this->assertGreaterThan(0, Elements::element($element));
        }
        $nonhtml5 = array(
            'foo',
            'bar',
            'baz',
        );
        foreach ($nonhtml5 as $element) {
            $this->assertEquals(0, Elements::element($element));
        }
    }

    public function testIsA()
    {
        $this->assertTrue(Elements::isA('script', Elements::KNOWN_ELEMENT));
        $this->assertFalse(Elements::isA('scriptypoo', Elements::KNOWN_ELEMENT));
        $this->assertTrue(Elements::isA('script', Elements::TEXT_RAW));
        $this->assertFalse(Elements::isA('script', Elements::TEXT_RCDATA));

        $voidElements = array(
            'area',
            'base',
            'basefont',
            'bgsound',
            'br',
            'col',
            'command',
            'embed',
            'frame',
            'hr',
            'img',
        );

        foreach ($voidElements as $element) {
            $this->assertTrue(Elements::isA($element, Elements::VOID_TAG), 'Void element test failed on: ' . $element);
        }

        $nonVoid = array(
            'span',
            'a',
            'div',
        );
        foreach ($nonVoid as $tag) {
            $this->assertFalse(Elements::isA($tag, Elements::VOID_TAG), 'Void element test failed on: ' . $tag);
        }

        $blockTags = array(
            'address',
            'article',
            'aside',
            'blockquote',
            'canvas',
            'dd',
            'div',
            'dl',
            'fieldset',
            'figcaption',
            'figure',
            'footer',
            'form',
            'h1',
            'h2',
            'h3',
            'h4',
            'h5',
            'h6',
            'header',
            'hgroup',
            'hr',
            'noscript',
            'ol',
            'output',
            'p',
            'pre',
            'section',
            'table',
            'tfoot',
            'ul',
            'video',
        );

        foreach ($blockTags as $tag) {
            $this->assertTrue(Elements::isA($tag, Elements::BLOCK_TAG), 'Block tag test failed on: ' . $tag);
        }

        $nonBlockTags = array(
            'span',
            'img',
            'label',
        );
        foreach ($nonBlockTags as $tag) {
            $this->assertFalse(Elements::isA($tag, Elements::BLOCK_TAG), 'Block tag test failed on: ' . $tag);
        }
    }

    public function testNormalizeSvgElement()
    {
        $tests = array(
            'foo' => 'foo',
            'altglyph' => 'altGlyph',
            'BAR' => 'bar',
            'fespecularlighting' => 'feSpecularLighting',
            'bAz' => 'baz',
            'foreignobject' => 'foreignObject',
        );

        foreach ($tests as $input => $expected) {
            $this->assertEquals($expected, Elements::normalizeSvgElement($input));
        }
    }

    public function testNormalizeSvgAttribute()
    {
        $tests = array(
            'foo' => 'foo',
            'attributename' => 'attributeName',
            'BAR' => 'bar',
            'limitingconeangle' => 'limitingConeAngle',
            'bAz' => 'baz',
            'patterncontentunits' => 'patternContentUnits',
        );

        foreach ($tests as $input => $expected) {
            $this->assertEquals($expected, Elements::normalizeSvgAttribute($input));
        }
    }

    public function testNormalizeMathMlAttribute()
    {
        $tests = array(
            'foo' => 'foo',
            'definitionurl' => 'definitionURL',
            'BAR' => 'bar',
        );

        foreach ($tests as $input => $expected) {
            $this->assertEquals($expected, Elements::normalizeMathMlAttribute($input));
        }
    }
}


code:
<!doctype html>

    <meta http-equiv="content-type" content="text/html;charset=utf-8" />

Žťčýů


code:
<!doctype html>
<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=windows-1252">
</head>
<body>
    <p>�����</p>
</body>
</html>


code:
<!doctype html>

    <meta charset="utf-8">
    <title>Test</title>

This is a test.


code:
<?php

namespace Masterminds\HTML5\Tests;

use Masterminds\HTML5;

class Html5Test extends TestCase
{
    /**
     * @var HTML5
     */
    private $html5;

    public function setUp()
    {
        $this->html5 = $this->getInstance();
    }

    /**
     * Parse and serialize a string.
     */
    protected function cycle($html)
    {
        $dom = $this->html5->loadHTML('<!DOCTYPE html>' . $html . '');
        $out = $this->html5->saveHTML($dom);

        return $out;
    }

    protected function cycleFragment($fragment)
    {
        $dom = $this->html5->loadHTMLFragment($fragment);
        $out = $this->html5->saveHTML($dom);

        return $out;
    }

    public function testImageTagsInSvg()
    {
        $html = '<!DOCTYPE html>

                            <title>foo</title>

                            <svg>
                                <image height="10" width="10"></image>
                            </svg>

                    ';
        $doc = $this->html5->loadHTML($html);
        $this->assertInstanceOf('DOMElement', $doc->getElementsByTagName('image')->item(0));
        $this->assertEmpty($this->html5->getErrors());
    }

    public function testLoadOptions()
    {
        // doc
        $dom = $this->html5->loadHTML($this->wrap('<t:tag/>'), array(
            'implicitNamespaces' => array('t' => 'http://example.com'),
            'xmlNamespaces' => true,
        ));
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());
        $this->assertFalse($this->html5->hasErrors());

        $xpath = new \DOMXPath($dom);
        $xpath->registerNamespace('t', 'http://example.com');
        $this->assertEquals(1, $xpath->query('//t:tag')->length);

        // doc fragment
        $frag = $this->html5->loadHTMLFragment('<t:tag/>', array(
            'implicitNamespaces' => array('t' => 'http://example.com'),
            'xmlNamespaces' => true,
        ));
        $this->assertInstanceOf('\DOMDocumentFragment', $frag);
        $this->assertEmpty($this->html5->getErrors());
        $this->assertFalse($this->html5->hasErrors());

        $frag->ownerDocument->appendChild($frag);
        $xpath = new \DOMXPath($frag->ownerDocument);
        $xpath->registerNamespace('t', 'http://example.com');
        $this->assertEquals(1, $xpath->query('//t:tag', $frag)->length);
    }

    public function testEncodingUtf8()
    {
        $dom = $this->html5->load(__DIR__ . '/Fixtures/encoding/utf-8.html');
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());
        $this->assertFalse($this->html5->hasErrors());

        $this->assertContains('Žťčýů', $dom->saveHTML());
    }

    public function testEncodingWindows1252()
    {
        $dom = $this->html5->load(__DIR__ . '/Fixtures/encoding/windows-1252.html', array(
            'encoding' => 'Windows-1252',
        ));

        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());
        $this->assertFalse($this->html5->hasErrors());

        $dumpedAsUtf8 = mb_convert_encoding($dom->saveHTML(), 'UTF-8', 'Windows-1252');
        $this->assertNotFalse(mb_strpos($dumpedAsUtf8, 'Ž'));
        $this->assertNotFalse(mb_strpos($dumpedAsUtf8, 'è'));
        $this->assertNotFalse(mb_strpos($dumpedAsUtf8, 'ý'));
        $this->assertNotFalse(mb_strpos($dumpedAsUtf8, 'ù'));
    }

    public function testErrors()
    {
        $dom = $this->html5->loadHTML('<xx as>');
        $this->assertInstanceOf('\DOMDocument', $dom);

        $this->assertNotEmpty($this->html5->getErrors());
        $this->assertTrue($this->html5->hasErrors());
    }

    public function testLoad()
    {
        $dom = $this->html5->load(__DIR__ . '/Html5Test.html');
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());
        $this->assertFalse($this->html5->hasErrors());

        $file = fopen(__DIR__ . '/Html5Test.html', 'r');
        $dom = $this->html5->load($file);
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());

        $dom = $this->html5->loadHTMLFile(__DIR__ . '/Html5Test.html');
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());
    }

    public function testLoadHTML()
    {
        $contents = file_get_contents(__DIR__ . '/Html5Test.html');
        $dom = $this->html5->loadHTML($contents);
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());
    }

    public function testLoadHTMLWithComments()
    {
        $contents = '

';

        $dom = $this->html5->loadHTML($contents);
        $this->assertInstanceOf('\DOMDocument', $dom);

        $expected = '<!DOCTYPE html>

';
        $this->assertEquals($expected, $this->html5->saveHTML($dom));
    }

    public function testLoadHTMLFragment()
    {
        $fragment = '

Baz

';
        $dom = $this->html5->loadHTMLFragment($fragment);
        $this->assertInstanceOf('\DOMDocumentFragment', $dom);
        $this->assertEmpty($this->html5->getErrors());
    }

    public function testSaveHTML()
    {
        $dom = $this->html5->load(__DIR__ . '/Html5Test.html');
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());

        $saved = $this->html5->saveHTML($dom);
        $this->assertRegExp('|
This is a test.
|', $saved);
    }

    public function testSaveHTMLFragment()
    {
        $fragment = '

Baz

';
        $dom = $this->html5->loadHTMLFragment($fragment);

        $string = $this->html5->saveHTML($dom);
        $this->assertEquals($fragment, $string);
    }

    public function testSave()
    {
        $dom = $this->html5->load(__DIR__ . '/Html5Test.html');
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());

        // Test resource
        $file = fopen('php://temp', 'w');
        $this->html5->save($dom, $file);
        $content = stream_get_contents($file, -1, 0);
        $this->assertRegExp('|
This is a test.
|', $content);

        // Test file
        $tmpfname = tempnam(sys_get_temp_dir(), 'html5-php');
        $this->html5->save($dom, $tmpfname);
        $content = file_get_contents($tmpfname);
        $this->assertRegExp('|
This is a test.
|', $content);
        unlink($tmpfname);
    }

    // This test reads a document into a dom, turn the dom into a document,
    // then tries to read that document again. This makes sure we are reading,
    // and generating a document that works at a high level.
    public function testItWorks()
    {
        $dom = $this->html5->load(__DIR__ . '/Html5Test.html');
        $this->assertInstanceOf('\DOMDocument', $dom);
        $this->assertEmpty($this->html5->getErrors());

        $saved = $this->html5->saveHTML($dom);

        $dom2 = $this->html5->loadHTML($saved);
        $this->assertInstanceOf('\DOMDocument', $dom2);
        $this->assertEmpty($this->html5->getErrors());
    }

    public function testConfig()
    {
        $html5 = $this->getInstance();
        $options = $html5->getOptions();
        $this->assertEquals(false, $options['encode_entities']);

        $html5 = $this->getInstance(array(
            'foo' => 'bar',
            'encode_entities' => true,
        ));
        $options = $html5->getOptions();
        $this->assertEquals('bar', $options['foo']);
        $this->assertEquals(true, $options['encode_entities']);

        // Need to reset to original so future tests pass as expected.
        // $this->getInstance()->setOption('encode_entities', false);
    }

    public function testSvg()
    {
        $dom = $this->html5->loadHTML(
            '<!doctype html>

foo bar baz

          <svg width="150" height="100" viewBox="0 0 3 2">
            <rect width="1" height="2" x="0" fill="#008d46" />
            <rect width="1" height="2" x="1" fill="#ffffff" />
            <rect width="1" height="2" x="2" fill="#d2232c" />
            <text font-family="Verdana" font-size="32">
              <textPath xlink:href="#Foo">
                Test Text.
              </textPath>
            </text>
          </svg>

      ');

        $this->assertEmpty($this->html5->getErrors());

        // Test a mixed case attribute.
        $list = $dom->getElementsByTagName('svg');
        $this->assertNotEmpty($list->length);
        $svg = $list->item(0);
        $this->assertEquals('0 0 3 2', $svg->getAttribute('viewBox'));
        $this->assertFalse($svg->hasAttribute('viewbox'));

        // Test a mixed case tag.
        // Note: getElementsByTagName is not case sensitive.
        $list = $dom->getElementsByTagName('textPath');
        $this->assertNotEmpty($list->length);
        $textPath = $list->item(0);
        $this->assertEquals('textPath', $textPath->tagName);
        $this->assertNotEquals('textpath', $textPath->tagName);

        $html = $this->html5->saveHTML($dom);
        $this->assertRegExp('|<svg width="150" height="100" viewBox="0 0 3 2">|', $html);
        $this->assertRegExp('|<rect width="1" height="2" x="0" fill="#008d46" />|', $html);
    }

    public function testMathMl()
    {
        $dom = $this->html5->loadHTML(
            '<!doctype html>

foo bar baz

          <math>
            <mi>x</mi>
            <csymbol definitionURL="http://www.example.com/mathops/multiops.html#plusminus">
              <mo>±</mo>
            </csymbol>
            <mi>y</mi>
          </math>

      ');

        $this->assertEmpty($this->html5->getErrors());
        $list = $dom->getElementsByTagName('math');
        $this->assertNotEmpty($list->length);

        $list = $dom->getElementsByTagName('div');
        $this->assertNotEmpty($list->length);
        $div = $list->item(0);
        $this->assertEquals('http://example.com', $div->getAttribute('definitionurl'));
        $this->assertFalse($div->hasAttribute('definitionURL'));
        $list = $dom->getElementsByTagName('csymbol');
        $csymbol = $list->item(0);
        $this->assertEquals('http://www.example.com/mathops/multiops.html#plusminus', $csymbol->getAttribute('definitionURL'));
        $this->assertFalse($csymbol->hasAttribute('definitionurl'));

        $html = $this->html5->saveHTML($dom);
        $this->assertRegExp('|<csymbol definitionURL="http://www.example.com/mathops/multiops.html#plusminus">|', $html);
        $this->assertRegExp('|<mi>y</mi>|', $html);
    }

    public function testUnknownElements()
    {
        // The : should not have special handling accourding to section 2.9 of the
        // spec. This is differenant than XML. Since we don't know these elements
        // they are handled as normal elements. Note, to do this is really
        // an invalid example and you should not embed prefixed xml in html5.
        $dom = $this->html5->loadHTMLFragment(
            '<f:rug>
      <f:name>Big rectangle thing</f:name>
      <f:width>40</f:width>
      <f:length>80</f:length>
    </f:rug>
    <sarcasm>um, yeah</sarcasm>');

        $this->assertEmpty($this->html5->getErrors());
        $markup = $this->html5->saveHTML($dom);
        $this->assertRegExp('|<f:name>Big rectangle thing</f:name>|', $markup);
        $this->assertRegExp('|<sarcasm>um, yeah</sarcasm>|', $markup);
    }

    public function testElements()
    {
        // Should have content.
        $res = $this->cycle('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        // Should be empty
        $res = $this->cycle('');
        $this->assertRegExp('||', $res);

        // Should have content.
        $res = $this->cycleFragment('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        // Should be empty
        $res = $this->cycleFragment('');
        $this->assertRegExp('||', $res);

        // Elements with dashes and underscores
        $res = $this->cycleFragment('<sp-an></sp-an>');
        $this->assertRegExp('|<sp-an></sp-an>|', $res);
        $res = $this->cycleFragment('<sp_an></sp_an>');
        $this->assertRegExp('|<sp_an></sp_an>|', $res);

        // Should have no closing tag.
        $res = $this->cycle('
');
        $this->assertRegExp('|
|', $res);
    }

    public function testAttributes()
    {
        $res = $this->cycle('<use xlink:href="#svg-track" xmlns:xlink="http://www.w3.org/1999/xlink"></use>');
        $this->assertContains('<use xlink:href="#svg-track" xmlns:xlink="http://www.w3.org/1999/xlink"></use>', $res);

        $res = $this->cycle('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        // XXX: Note that spec does NOT require attrs in the same order.
        $res = $this->cycle('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        $res = $this->cycle('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        $res = $this->cycleFragment('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        // XXX: Note that spec does NOT require attrs in the same order.
        $res = $this->cycleFragment('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);

        $res = $this->cycleFragment('
FOO
');
        $this->assertRegExp('|
FOO
|', $res);
    }

    public function testPCData()
    {
        $res = $this->cycle('This is a test.');
        $this->assertRegExp('|This is a test.|', $res);

        $res = $this->cycleFragment('This is a test.');
        $this->assertRegExp('|This is a test.|', $res);

        $res = $this->cycle('This
      is
      a
      test.');

        // Check that newlines are there, but don't count spaces.
        $this->assertRegExp('|This\n\s*is\n\s*a\n\s*test.|', $res);

        $res = $this->cycleFragment('This
      is
      a
      test.');

        // Check that newlines are there, but don't count spaces.
        $this->assertRegExp('|This\n\s*is\n\s*a\n\s*test.|', $res);

        $res = $this->cycle('This is a test.');
        $this->assertRegExp('|This is a test.|', $res);

        $res = $this->cycleFragment('This is a test.');
        $this->assertRegExp('|This is a test.|', $res);
    }

    public function testUnescaped()
    {
        $res = $this->cycle('');
        $this->assertRegExp('|2 < 1|', $res);

        $res = $this->cycle('');
        $this->assertRegExp('|div>div>div|', $res);

        $res = $this->cycleFragment('');
        $this->assertRegExp('|2 < 1|', $res);

        $res = $this->cycleFragment('');
        $this->assertRegExp('|div>div>div|', $res);
    }

    public function testEntities()
    {
        $res = $this->cycle('Apples & bananas.');
        $this->assertRegExp('|Apples & bananas.|', $res);

        $res = $this->cycleFragment('Apples & bananas.');
        $this->assertRegExp('|Apples & bananas.|', $res);

        $res = $this->cycleFragment('
R&D
');
        $this->assertRegExp('|R&D|', $res);
    }

    public function testCaseSensitiveTags()
    {
        $dom = $this->html5->loadHTML(
            '<Button color="red">Error</Button>',
            array(
                'xmlNamespaces' => true,
            )
        );
        $out = $this->html5->saveHTML($dom);
        $this->assertRegExp('|<Button color="red">Error</Button>|', $out);
    }

    public function testComment()
    {
        $res = $this->cycle('ab');
        $this->assertRegExp('||', $res);

        $res = $this->cycleFragment('ab');
        $this->assertRegExp('||', $res);
    }

    public function testCDATA()
    {
        $res = $this->cycle('a<![CDATA[ This <is> a test. ]]>b');
        $this->assertRegExp('|<!\[CDATA\[ This <is> a test\. \]\]>|', $res);

        $res = $this->cycleFragment('a<![CDATA[ This <is> a test. ]]>b');
        $this->assertRegExp('|<!\[CDATA\[ This <is> a test\. \]\]>|', $res);
    }
}


code:
<?php
/**
 * @file
 * Test the Scanner. This requires the InputStream tests are all good.
 */

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Parser\CharacterReference;

class CharacterReferenceTest extends \Masterminds\HTML5\Tests\TestCase
{
    public function testLookupName()
    {
        $this->assertEquals('&', CharacterReference::lookupName('amp'));
        $this->assertEquals('<', CharacterReference::lookupName('lt'));
        $this->assertEquals('>', CharacterReference::lookupName('gt'));
        $this->assertEquals('"', CharacterReference::lookupName('quot'));
        $this->assertEquals('∌', CharacterReference::lookupName('NotReverseElement'));

        $this->assertNull(CharacterReference::lookupName('StinkyCheese'));
    }

    public function testLookupHex()
    {
        $this->assertEquals('<', CharacterReference::lookupHex('3c'));
        $this->assertEquals('<', CharacterReference::lookupHex('003c'));
        $this->assertEquals('&', CharacterReference::lookupHex('26'));
        $this->assertEquals('}', CharacterReference::lookupHex('7d'));
        $this->assertEquals('Σ', CharacterReference::lookupHex('3A3'));
        $this->assertEquals('Σ', CharacterReference::lookupHex('03A3'));
        $this->assertEquals('Σ', CharacterReference::lookupHex('3a3'));
        $this->assertEquals('Σ', CharacterReference::lookupHex('03a3'));
    }

    public function testLookupDecimal()
    {
        $this->assertEquals('&', CharacterReference::lookupDecimal(38));
        $this->assertEquals('&', CharacterReference::lookupDecimal('38'));
        $this->assertEquals('<', CharacterReference::lookupDecimal(60));
        $this->assertEquals('Σ', CharacterReference::lookupDecimal(931));
        $this->assertEquals('Σ', CharacterReference::lookupDecimal('0931'));
    }
}


code:
<?php
/**
 * @file
 * Test the Tree Builder.
 */

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Parser\Scanner;
use Masterminds\HTML5\Parser\Tokenizer;
use Masterminds\HTML5\Parser\DOMTreeBuilder;

/**
 * These tests are functional, not necessarily unit tests.
 */
class DOMTreeBuilderTest extends \Masterminds\HTML5\Tests\TestCase
{
    protected $errors = array();

    /**
     * Convenience function for parsing.
     */
    protected function parse($string, array $options = array())
    {
        $treeBuilder = new DOMTreeBuilder(false, $options);
        $scanner = new Scanner($string);
        $parser = new Tokenizer($scanner, $treeBuilder);

        $parser->parse();
        $this->errors = $treeBuilder->getErrors();

        return $treeBuilder->document();
    }

    /**
     * Utility function for parsing a fragment of HTML5.
     */
    protected function parseFragment($string)
    {
        $treeBuilder = new DOMTreeBuilder(true);
        $scanner = new Scanner($string);
        $parser = new Tokenizer($scanner, $treeBuilder);

        $parser->parse();
        $this->errors = $treeBuilder->getErrors();

        return $treeBuilder->fragment();
    }

    public function testDocument()
    {
        $html = '<!DOCTYPE html>';
        $doc = $this->parse($html);

        $this->assertEquals('UTF-8', $doc->encoding);
        $this->assertInstanceOf('\DOMDocument', $doc);
        $this->assertEquals('html', $doc->documentElement->tagName);
        $this->assertEquals('http://www.w3.org/1999/xhtml', $doc->documentElement->namespaceURI);
    }

    public function testBareAmpersand()
    {
        $html = "<!doctype html>

        ";
        $doc = $this->parse($html);

        $this->assertEmpty($this->errors);
        $this->assertXmlStringEqualsXmlString('
        <!DOCTYPE html>

        ', $doc->saveXML());
    }

    public function testBareAmpersandNotAllowedInAttributes()
    {
        $html = "<!doctype html>

        ";
        $doc = $this->parse($html);

        $this->assertCount(2, $this->errors);
        $this->assertXmlStringEqualsXmlString('
        <!DOCTYPE html>

        ', $doc->saveXML());
    }

    public function testBareAmpersandNotAllowedInBody()
    {
        $html = '<!doctype html>

                a&b
                a&=
                a&=c
                a&=9
                a&+
                a& -- valid

        ';
        $doc = $this->parse($html);

        $this->assertCount(5, $this->errors);
        $this->assertXmlStringEqualsXmlString('
        <!DOCTYPE html>

                a&b
                a&=
                a&=c
                a&=9
                a&+
                a& -- valid

        ', $doc->saveXML());
    }

    public function testStrangeCapitalization()
    {
        $html = '<!doctype html>

                <Title>Hello, world!</TitlE>

            TheBody
        ';
        $doc = $this->parse($html);

        $this->assertInstanceOf('\DOMDocument', $doc);
        $this->assertEquals('html', $doc->documentElement->tagName);

        $xpath = new \DOMXPath($doc);
        $xpath->registerNamespace('x', 'http://www.w3.org/1999/xhtml');

        $this->assertEquals('Hello, world!', $xpath->query('//x:title')->item(0)->nodeValue);
        $this->assertEquals('foo', $xpath->query('//x:script')->item(0)->nodeValue);
    }

    public function testDocumentWithDisabledNamespaces()
    {
        $html = '<!DOCTYPE html>';
        $doc = $this->parse($html, array('disable_html_ns' => true));

        $this->assertInstanceOf('\DOMDocument', $doc);
        $this->assertEquals('html', $doc->documentElement->tagName);
        $this->assertNull($doc->documentElement->namespaceURI);
    }

    public function testDocumentWithATargetDocument()
    {
        $targetDom = new \DOMDocument();

        $html = '<!DOCTYPE html>';
        $doc = $this->parse($html, array('target_document' => $targetDom));

        $this->assertInstanceOf('\DOMDocument', $doc);
        $this->assertSame($doc, $targetDom);
        $this->assertEquals('html', $doc->documentElement->tagName);
    }

    public function testDocumentFakeAttrAbsence()
    {
        $html = '<!DOCTYPE html>foo';
        $doc = $this->parse($html, array('xmlNamespaces' => true));

        $xp = new \DOMXPath($doc);
        $this->assertEquals(0, $xp->query('//@html5-php-fake-id-attribute')->length);
    }

    public function testFragment()
    {
        $html = '
test
test2';
        $doc = $this->parseFragment($html);

        $this->assertInstanceOf('\DOMDocumentFragment', $doc);
        $this->assertTrue($doc->hasChildNodes());
        $this->assertEquals('div', $doc->childNodes->item(0)->tagName);
        $this->assertEquals('test', $doc->childNodes->item(0)->textContent);
        $this->assertEquals('span', $doc->childNodes->item(1)->tagName);
        $this->assertEquals('test2', $doc->childNodes->item(1)->textContent);
    }

    public function testElements()
    {
        $html = '<!DOCTYPE html><title></title>';
        $doc = $this->parse($html);
        $root = $doc->documentElement;

        $this->assertEquals('html', $root->tagName);
        $this->assertEquals('html', $root->localName);
        $this->assertEquals('html', $root->nodeName);

        $this->assertEquals(2, $root->childNodes->length);
        $kids = $root->childNodes;

        $this->assertEquals('head', $kids->item(0)->tagName);
        $this->assertEquals('body', $kids->item(1)->tagName);

        $head = $kids->item(0);
        $this->assertEquals(1, $head->childNodes->length);
        $this->assertEquals('title', $head->childNodes->item(0)->tagName);
    }

    public function testImplicitNamespaces()
    {
        $dom = $this->parse('<!DOCTYPE html>foo');
        $a = $dom->getElementsByTagName('a')->item(0);
        $attr = $a->getAttributeNode('xlink:href');
        $this->assertEquals('http://www.w3.org/1999/xlink', $attr->namespaceURI);

        $dom = $this->parse('<!DOCTYPE html>foo');
        $a = $dom->getElementsByTagName('a')->item(0);
        $attr = $a->getAttributeNode('xml:base');
        $this->assertEquals('http://www.w3.org/XML/1998/namespace', $attr->namespaceURI);
    }

    public function testCustomImplicitNamespaces()
    {
        $dom = $this->parse('<!DOCTYPE html>foo', array(
            'implicitNamespaces' => array(
                't' => 'http://www.example.com',
            ),
        ));
        $a = $dom->getElementsByTagName('a')->item(0);
        $attr = $a->getAttributeNode('t:href');
        $this->assertEquals('http://www.example.com', $attr->namespaceURI);

        $dom = $this->parse('<!DOCTYPE html><t:a>foo</t:a>', array(
            'implicitNamespaces' => array(
                't' => 'http://www.example.com',
            ),
        ));
        $list = $dom->getElementsByTagNameNS('http://www.example.com', 'a');
        $this->assertEquals(1, $list->length);
    }

    public function testXmlNamespaces()
    {
        $dom = $this->parse(
            '<!DOCTYPE html>
            <t:body xmlns:t="http://www.example.com">
                foo

foo

          ', array(
                'xmlNamespaces' => true,
            ));
        $a = $dom->getElementsByTagName('a')->item(0);
        $attr = $a->getAttributeNode('t:href');
        $this->assertEquals('http://www.example.com', $attr->namespaceURI);

        $list = $dom->getElementsByTagNameNS('http://www.example.com', 'body');
        $this->assertEquals(1, $list->length);
    }

    public function testXmlNamespaceNesting()
    {
        $dom = $this->parse(
            '<!DOCTYPE html>

                    <x:prefixed id="prefixed"/>

                <svg id="svg"></svg>
                <c id="bar2" xmlns="http://www.prefixed.com/bar2"></c>

                <d id="bar3"></d>
                <xn:d xmlns:xn="http://www.prefixed.com/xn" xmlns="http://www.prefixed.com/bar5_x" id="bar5"><x id="bar5_x"/></xn:d>

          ', array(
                'xmlNamespaces' => true,
            ));

        $this->assertEmpty($this->errors);

        $div = $dom->getElementById('div');
        $this->assertEquals('http://www.w3.org/1999/xhtml', $div->namespaceURI);

        $body = $dom->getElementById('body');
        $this->assertEquals('http://www.w3.org/1999/xhtml', $body->namespaceURI);

        $bar1 = $dom->getElementById('bar1');
        $this->assertEquals('http://www.prefixed.com/bar1', $bar1->namespaceURI);

        $bar2 = $dom->getElementById('bar2');
        $this->assertEquals('http://www.prefixed.com/bar2', $bar2->namespaceURI);

        $bar3 = $dom->getElementById('bar3');
        $this->assertEquals('http://www.w3.org/1999/xhtml', $bar3->namespaceURI);

        $bar4 = $dom->getElementById('bar4');
        $this->assertEquals('http://www.prefixed.com/bar4', $bar4->namespaceURI);

        $svg = $dom->getElementById('svg');
        $this->assertEquals('http://www.w3.org/2000/svg', $svg->namespaceURI);

        $prefixed = $dom->getElementById('prefixed');
        $this->assertEquals('http://www.prefixed.com', $prefixed->namespaceURI);

        $prefixed = $dom->getElementById('bar5');
        $this->assertEquals('http://www.prefixed.com/xn', $prefixed->namespaceURI);

        $prefixed = $dom->getElementById('bar5_x');
        $this->assertEquals('http://www.prefixed.com/bar5_x', $prefixed->namespaceURI);
    }

    public function testMoveNonInlineElements()
    {
        $doc = $this->parse('
line1

line2
');
        $this->assertEquals('
line1

line2', $doc->saveXML($doc->documentElement), 'Move non-inline elements outside of inline containers.');

        $doc = $this->parse('
line1
line2

');
        $this->assertEquals('
line1

line2
', $doc->saveXML($doc->documentElement), 'Move non-inline elements outside of inline containers.');
    }

    public function testAttributes()
    {
        $html = "<!DOCTYPE html>

      <title></title>

      ";
        $doc = $this->parse($html);
        $root = $doc->documentElement;

        $body = $root->GetElementsByTagName('body')->item(0);
        $this->assertEquals('body', $body->tagName);
        $this->assertTrue($body->hasAttributes());
        $this->assertEquals('a', $body->getAttribute('id'));
        $this->assertEquals('b c', $body->getAttribute('class'));

        $body2 = $doc->getElementById('a');
        $this->assertEquals('body', $body2->tagName);
        $this->assertEquals('a', $body2->getAttribute('id'));
    }

    public function testSVGAttributes()
    {
        $html = "<!DOCTYPE html>

      <svg width='150' viewbox='2'>
      <rect textlength='2'/>
      <animatecolor>foo</animatecolor>
      </svg>
      ";
        $doc = $this->parse($html);
        $root = $doc->documentElement;

        $svg = $root->getElementsByTagName('svg')->item(0);
        $this->assertTrue($svg->hasAttribute('viewBox'));

        $rect = $root->getElementsByTagName('rect')->item(0);
        $this->assertTrue($rect->hasAttribute('textLength'));

        $ac = $root->getElementsByTagName('animateColor');
        $this->assertEquals(1, $ac->length);
    }

    public function testMathMLAttribute()
    {
        $html = '<!doctype html>

          <math>
            <mi>x</mi>
            <csymbol definitionurl="http://www.example.com/mathops/multiops.html#plusminus">
              <mo>±</mo>
            </csymbol>
            <mi>y</mi>
          </math>

      ';

        $doc = $this->parse($html);
        $root = $doc->documentElement;

        $csymbol = $root->getElementsByTagName('csymbol')->item(0);
        $this->assertTrue($csymbol->hasAttribute('definitionURL'));
    }

    public function testMissingHtmlTag()
    {
        $html = '<!DOCTYPE html><title>test</title>';
        $doc = $this->parse($html);

        $this->assertEquals('html', $doc->documentElement->tagName);
        $this->assertEquals('title', $doc->documentElement->childNodes->item(0)->tagName);
    }

    public function testComment()
    {
        $html = '';

        $doc = $this->parse($html);

        $comment = $doc->documentElement->childNodes->item(0);
        $this->assertEquals(XML_COMMENT_NODE, $comment->nodeType);
        $this->assertEquals('Hello World.', $comment->data);

        $html = '';
        $doc = $this->parse($html);

        $comment = $doc->childNodes->item(1);
        $this->assertEquals(XML_COMMENT_NODE, $comment->nodeType);
        $this->assertEquals('Hello World.', $comment->data);

        $comment = $doc->childNodes->item(2);
        $this->assertEquals(XML_ELEMENT_NODE, $comment->nodeType);
        $this->assertEquals('html', $comment->tagName);
    }

    public function testCDATA()
    {
        $html = '<!DOCTYPE html><math><![CDATA[test]]></math>';
        $doc = $this->parse($html);

        $wrapper = $doc->getElementsByTagName('math')->item(0);
        $this->assertEquals(1, $wrapper->childNodes->length);
        $cdata = $wrapper->childNodes->item(0);
        $this->assertEquals(XML_CDATA_SECTION_NODE, $cdata->nodeType);
        $this->assertEquals('test', $cdata->data);
    }

    public function testText()
    {
        $html = '<!DOCTYPE html><math>test</math>';
        $doc = $this->parse($html);

        $wrapper = $doc->getElementsByTagName('math')->item(0);
        $this->assertEquals(1, $wrapper->childNodes->length);
        $data = $wrapper->childNodes->item(0);
        $this->assertEquals(XML_TEXT_NODE, $data->nodeType);
        $this->assertEquals('test', $data->data);

        // The DomTreeBuilder has special handling for text when in before head mode.
        $html = '<!DOCTYPE html>
    Foo';
        $doc = $this->parse($html);
        $this->assertEquals('Line 0, Col 0: Unexpected text. Ignoring: Foo', $this->errors[0]);
        $headElement = $doc->documentElement->firstChild;
        $this->assertEquals('head', $headElement->tagName);
    }

    public function testParseErrors()
    {
        $html = '<!DOCTYPE html><math><![CDATA[test';
        $doc = $this->parse($html);

        // We're JUST testing that we can access errors. Actual testing of
        // error messages happen in the Tokenizer's tests.
        $this->assertGreaterThan(0, count($this->errors));
        $this->assertTrue(is_string($this->errors[0]));
    }

    public function testProcessingInstruction()
    {
        // Test the simple case, which is where PIs are inserted into the DOM.
        $doc = $this->parse('<!DOCTYPE html><?foo bar?>');
        $this->assertEquals(1, $doc->documentElement->childNodes->length);
        $pi = $doc->documentElement->firstChild;
        $this->assertInstanceOf('\DOMProcessingInstruction', $pi);
        $this->assertEquals('foo', $pi->nodeName);
        $this->assertEquals('bar', $pi->data);

        // Leading xml PIs should be ignored.
        $doc = $this->parse('<?xml version="1.0"?><!DOCTYPE html>');

        $this->assertEquals(2, $doc->childNodes->length);
        $this->assertInstanceOf('\DOMDocumentType', $doc->childNodes->item(0));
        $this->assertInstanceOf('\DOMElement', $doc->childNodes->item(1));
    }

    public function testAutocloseP()
    {
        $html = '<!DOCTYPE html>

';
        $doc = $this->parse($html);

        $p = $doc->getElementsByTagName('p')->item(0);
        $this->assertEquals(0, $p->childNodes->length);
        $this->assertEquals('figure', $p->nextSibling->tagName);
    }

    public function testAutocloseLI()
    {
        $html = '<!doctype html>

Foo
Bar
Baz

      ';

        $doc = $this->parse($html);
        $length = $doc->getElementsByTagName('ul')->item(0)->childNodes->length;
        $this->assertEquals(3, $length);
    }

    public function testMathML()
    {
        $html = '<!doctype html>

          <math xmlns="http://www.w3.org/1998/Math/MathML">
            <mi>x</mi>
            <csymbol definitionurl="http://www.example.com/mathops/multiops.html#plusminus">
              <mo>±</mo>
            </csymbol>
            <mi>y</mi>
          </math>

      ';

        $doc = $this->parse($html);
        $math = $doc->getElementsByTagName('math')->item(0);
        $this->assertEquals('math', $math->tagName);
        $this->assertEquals('math', $math->nodeName);
        $this->assertEquals('math', $math->localName);
        $this->assertEquals('http://www.w3.org/1998/Math/MathML', $math->namespaceURI);
    }

    public function testSVG()
    {
        $html = '<!doctype html>

          <svg width="150" height="100" viewBox="0 0 3 2" xmlns="http://www.w3.org/2000/svg">
            <rect width="1" height="2" x="2" fill="#d2232c" />
            <text font-family="Verdana" font-size="32">
              <textpath xlink:href="#Foo">
                Test Text.
              </textPath>
            </text>
          </svg>

      ';

        $doc = $this->parse($html);
        $svg = $doc->getElementsByTagName('svg')->item(0);
        $this->assertEquals('svg', $svg->tagName);
        $this->assertEquals('svg', $svg->nodeName);
        $this->assertEquals('svg', $svg->localName);
        $this->assertEquals('http://www.w3.org/2000/svg', $svg->namespaceURI);

        $textPath = $doc->getElementsByTagName('textPath')->item(0);
        $this->assertEquals('textPath', $textPath->tagName);
    }

    public function testNoScript()
    {
        $html = '<!DOCTYPE html>No JS';
        $doc = $this->parse($html);
        $this->assertEmpty($this->errors);
        $noscript = $doc->getElementsByTagName('noscript')->item(0);
        $this->assertEquals('noscript', $noscript->tagName);

        $html = '<!DOCTYPE html>
No JS
';
        $doc = $this->parse($html);
        $this->assertEmpty($this->errors);
        $p = $doc->getElementsByTagName('p')->item(0);
        $this->assertEquals('p', $p->tagName);
    }

    /**
     * Regression for issue #13.
     */
    public function testRegressionHTMLNoBody()
    {
        $html = '<!DOCTYPE html>Test';
        $doc = $this->parse($html);
        $span = $doc->getElementById('test');

        $this->assertEmpty($this->errors);

        $this->assertEquals('span', $span->tagName);
        $this->assertEquals('Test', $span->textContent);
    }

    public function testInstructionProcessor()
    {
        $string = '<!DOCTYPE html><?foo bar ?>';

        $treeBuilder = new DOMTreeBuilder();
        $is = new InstructionProcessorMock();
        $treeBuilder->setInstructionProcessor($is);

        $scanner = new Scanner($string);
        $parser = new Tokenizer($scanner, $treeBuilder);

        $parser->parse();
        $dom = $treeBuilder->document();
        $div = $dom->getElementsByTagName('div')->item(0);

        $this->assertEquals(1, $is->count);
        $this->assertEquals('foo', $is->name);
        $this->assertEquals('bar ', $is->data);
        $this->assertEquals('div', $div->tagName);
        $this->assertEquals('foo', $div->textContent);
    }

    public function testSelectGroupedOptions()
    {
        $html = <<<EOM
<!DOCTYPE html>

        <title>testSelectGroupedOptions</title>

        <select>
            <optgroup id="first" label="first">
                <option value="foo">foo</option>
                <option value="bar">bar</option>
                <option value="baz">baz</option>
            </optgroup>
            <optgroup id="second" label="second">
                <option value="lorem">lorem</option>
                <option value="ipsum">ipsum</option>
            </optgroup>
         </select>

EOM;
        $dom = $this->parse($html);

        $this->assertSame(3, $dom->getElementById('first')->getElementsByTagName('option')->length);
        $this->assertSame(2, $dom->getElementById('second')->getElementsByTagName('option')->length);
    }

    public function testVoidTag()
    {
        $html = <<<EOM
<!DOCTYPE html>

        <title>testVoidTag</title>
        <meta>
        <meta>

EOM;

        $dom = $this->parse($html);
        $this->assertSame(2, $dom->getElementsByTagName('meta')->length);
        $this->assertSame(0, $dom->getElementsByTagName('meta')->item(0)->childNodes->length);
        $this->assertSame(0, $dom->getElementsByTagName('meta')->item(1)->childNodes->length);
    }

    public function testIgnoreSelfClosingTag()
    {
        $html = <<<EOM
<!DOCTYPE html>

        <title>testIllegalSelfClosingTag</title>

Hello, World!

EOM;

        $dom = $this->parse($html);
        $this->assertSame(1, $dom->getElementsByTagName('div')->item(0)->childNodes->length);
    }

    public function testIAudioInParagraph()
    {
        $html = <<<EOM
<!DOCTYPE html>

        <title>testIllegalSelfClosingTag</title>

        <audio preload="none" controls="controls">
            <source src="https://example.com/test.mp3" type="audio/mpeg" />
            Your browser does not support the audio element.
        </audio>

>

EOM;

        $dom = $this->parse($html);
        $audio = $dom->getElementsByTagName('audio')->item(0);

        $this->assertSame('p', $audio->parentNode->nodeName);
        $this->assertSame(3, $audio->childNodes->length);
    }
}


code:
<?php

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Elements;
use Masterminds\HTML5\Parser\EventHandler;

/**
 * This testing class gathers events from a parser and builds a stack of events.
 * It is useful for checking the output of a tokenizer.
 *
 * IMPORTANT:
 *
 * The startTag event also kicks the parser into TEXT_RAW when it encounters
 * script or pre tags. This is to match the behavior required by the HTML5 spec,
 * which says that the tree builder must tell the tokenizer when to switch states.
 */
class EventStack implements EventHandler
{
    protected $stack;

    public function __construct()
    {
        $this->stack = array();
    }

    /**
     * Get the event stack.
     */
    public function events()
    {
        return $this->stack;
    }

    public function depth()
    {
        return count($this->stack);
    }

    public function get($index)
    {
        return $this->stack[$index];
    }

    protected function store($event, $data = null)
    {
        $this->stack[] = array(
            'name' => $event,
            'data' => $data,
        );
    }

    public function doctype($name, $type = 0, $id = null, $quirks = false)
    {
        $args = array(
            $name,
            $type,
            $id,
            $quirks,
        );
        $this->store('doctype', $args);
    }

    public function startTag($name, $attributes = array(), $selfClosing = false)
    {
        $args = func_get_args();
        $this->store('startTag', $args);
        if ('pre' == $name || 'script' == $name) {
            return Elements::TEXT_RAW;
        }
    }

    public function endTag($name)
    {
        $this->store('endTag', array(
            $name,
        ));
    }

    public function comment($cdata)
    {
        $this->store('comment', array(
            $cdata,
        ));
    }

    public function cdata($data)
    {
        $this->store('cdata', func_get_args());
    }

    public function text($cdata)
    {
        // fprintf(STDOUT, "Received TEXT event with: " . $cdata);
        $this->store('text', array(
            $cdata,
        ));
    }

    public function eof()
    {
        $this->store('eof');
    }

    public function parseError($msg, $line, $col)
    {
        // throw new EventStackParseError(sprintf("%s (line %d, col %d)", $msg, $line, $col));
        // $this->store(sprintf("%s (line %d, col %d)", $msg, $line, $col));
        $this->store('error', func_get_args());
    }

    public function processingInstruction($name, $data = null)
    {
        $this->store('pi', func_get_args());
    }
}


code:
<?php

namespace Masterminds\HTML5\Tests\Parser;

class EventStackError extends \Exception
{
}


code:
<?php

namespace Masterminds\HTML5\Tests\Parser;

class InstructionProcessorMock implements \Masterminds\HTML5\InstructionProcessor
{
    public $name = null;

    public $data = null;

    public $count = 0;

    public function process(\DOMElement $element, $name, $data)
    {
        $this->name = $name;
        $this->data = $data;
        ++$this->count;

        $div = $element->ownerDocument->createElement('div');
        $div->nodeValue = 'foo';

        $element->appendChild($div);

        return $div;
    }
}


code:
<?php
/**
 * @file
 * Test the Scanner. This requires the InputStream tests are all good.
 */

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Parser\StringInputStream;
use Masterminds\HTML5\Parser\Scanner;

class ScannerTest extends \Masterminds\HTML5\Tests\TestCase
{
    /**
     * A canary test to make sure the basics are setup and working.
     */
    public function testConstructDeprecated()
    {
        $is = new StringInputStream('abc');
        $s = new Scanner($is);

        $this->assertInstanceOf('\Masterminds\HTML5\Parser\Scanner', $s);
    }

    public function testConstruct()
    {
        $this->assertInstanceOf('\Masterminds\HTML5\Parser\Scanner', new Scanner('abc'));
    }

    public function testNextDeprecated()
    {
        $s = new Scanner(new StringInputStream('abc'));

        $this->assertEquals('b', $s->next());
        $this->assertEquals('c', $s->next());
    }

    public function testNext()
    {
        $s = new Scanner('abc');

        $this->assertEquals('b', $s->next());
        $this->assertEquals('c', $s->next());
    }

    public function testPosition()
    {
        $s = new Scanner('abc');

        $this->assertEquals(0, $s->position());

        $s->next();
        $this->assertEquals(1, $s->position());
    }

    public function testPeek()
    {
        $s = new Scanner('abc');

        $this->assertEquals('b', $s->peek());

        $s->next();
        $this->assertEquals('c', $s->peek());
    }

    public function testCurrent()
    {
        $s = new Scanner('abc');

        // Before scanning the string begins the current is empty.
        $this->assertEquals('a', $s->current());

        $c = $s->next();
        $this->assertEquals('b', $s->current());

        // Test movement through the string.
        $c = $s->next();
        $this->assertEquals('c', $s->current());
    }

    public function testUnconsume()
    {
        $s = new Scanner('abcdefghijklmnopqrst');

        // Get initial position.
        $s->next();
        $start = $s->position();

        // Move forward a bunch of positions.
        $amount = 7;
        for ($i = 0; $i < $amount; ++$i) {
            $s->next();
        }

        // Roll back the amount we moved forward.
        $s->unconsume($amount);

        $this->assertEquals($start, $s->position());
    }

    public function testGetHex()
    {
        $s = new Scanner('ab13ck45DE*');

        $this->assertEquals('ab13c', $s->getHex());

        $s->next();
        $this->assertEquals('45DE', $s->getHex());
    }

    public function testGetAsciiAlpha()
    {
        $s = new Scanner('abcdef1%mnop*');

        $this->assertEquals('abcdef', $s->getAsciiAlpha());

        // Move past the 1% to scan the next group of text.
        $s->next();
        $s->next();
        $this->assertEquals('mnop', $s->getAsciiAlpha());
    }

    public function testGetAsciiAlphaNum()
    {
        $s = new Scanner('abcdef1ghpo#mn94op');

        $this->assertEquals('abcdef1ghpo', $s->getAsciiAlphaNum());

        // Move past the # to scan the next group of text.
        $s->next();
        $this->assertEquals('mn94op', $s->getAsciiAlphaNum());
    }

    public function testGetNumeric()
    {
        $s = new Scanner('1784a 45 9867 #');

        $this->assertEquals('1784', $s->getNumeric());

        // Move past the 'a ' to scan the next group of text.
        $s->next();
        $s->next();
        $this->assertEquals('45', $s->getNumeric());
    }

    public function testCurrentLine()
    {
        $s = new Scanner("1784a\n45\n9867 #\nThis is a test.");

        $this->assertEquals(1, $s->currentLine());

        // Move to the next line.
        $s->getAsciiAlphaNum();
        $s->next();
        $this->assertEquals(2, $s->currentLine());
    }

    public function testColumnOffset()
    {
        $s = new Scanner("1784a a\n45 9867 #\nThis is a test.");

        // Move the pointer to the space.
        $s->getAsciiAlphaNum();
        $this->assertEquals(5, $s->columnOffset());

        // We move the pointer ahead. There must be a better way to do this.
        $s->next();
        $s->next();
        $s->next();
        $s->next();
        $s->next();
        $s->next();
        $this->assertEquals(3, $s->columnOffset());
    }

    public function testRemainingChars()
    {
        $string = "\n45\n9867 #\nThis is a test.";
        $s = new Scanner("1784a\n45\n9867 #\nThis is a test.");

        $s->getAsciiAlphaNum();
        $this->assertEquals($string, $s->remainingChars());
    }
}


code:
<?php

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Parser\UTF8Utils;
use Masterminds\HTML5\Parser\Scanner;
use Masterminds\HTML5\Parser\Tokenizer;

class TokenizerTest extends \Masterminds\HTML5\Tests\TestCase
{
    // ================================================================
    // Additional assertions.
    // ================================================================

    /**
     * Tests that an event matches both the event type and the expected value.
     *
     * @param string $type
     *                        Expected event type
     * @param string $expects
     *                        The value expected in $event['data'][0]
     */
    public function assertEventEquals($type, $expects, $event)
    {
        $this->assertEquals($type, $event['name'], "Event $type for " . print_r($event, true));
        if (is_array($expects)) {
            $this->assertEquals($expects, $event['data'], "Event $type should equal " . print_r($expects, true) . ': ' . print_r($event, true));
        } else {
            $this->assertEquals($expects, $event['data'][0], "Event $type should equal $expects: " . print_r($event, true));
        }
    }

    /**
     * Assert that a given event is 'error'.
     */
    public function assertEventError($event)
    {
        $this->assertEquals('error', $event['name'], 'Expected error for event: ' . print_r($event, true));
    }

    /**
     * Asserts that all of the tests are good.
     *
     * This loops through a map of tests/expectations and runs a few assertions on each test.
     *
     * Checks:
     * - depth (if depth is > 0)
     * - event name
     * - matches on event 0.
     */
    protected function isAllGood($name, $depth, $tests, $debug = false)
    {
        foreach ($tests as $try => $expects) {
            if ($debug) {
                fprintf(STDOUT, "%s expects %s\n", $try, print_r($expects, true));
            }
            $e = $this->parse($try);
            if ($depth > 0) {
                $this->assertEquals($depth, $e->depth(), "Expected depth $depth for test $try." . print_r($e, true));
            }
            $this->assertEventEquals($name, $expects, $e->get(0));
        }
    }

    // ================================================================
    // Utility functions.
    // ================================================================
    public function testParse()
    {
        list($tok, $events) = $this->createTokenizer('');

        $tok->parse();
        $e1 = $events->get(0);

        $this->assertEquals(1, $events->Depth());
        $this->assertEquals('eof', $e1['name']);
    }

    public function testWhitespace()
    {
        $spaces = '    ';
        list($tok, $events) = $this->createTokenizer($spaces);

        $tok->parse();

        $this->assertEquals(2, $events->depth());

        $e1 = $events->get(0);

        $this->assertEquals('text', $e1['name']);
        $this->assertEquals($spaces, $e1['data'][0]);
    }

    public function testCharacterReference()
    {
        $good = array(
            '&' => '&',
            '<' => '<',
            '&' => '&',
            '&' => '&',
        );
        $this->isAllGood('text', 2, $good);

        // Test with broken charref
        $str = '&foo';
        $events = $this->parse($str);
        $e1 = $events->get(0);
        $this->assertEquals('error', $e1['name']);

        $str = '&#xfoo';
        $events = $this->parse($str);
        $e1 = $events->get(0);
        $this->assertEquals('error', $e1['name']);

        $str = '&#foo';
        $events = $this->parse($str);
        $e1 = $events->get(0);
        $this->assertEquals('error', $e1['name']);

        // FIXME: Once the text processor is done, need to verify that the
        // tokens are transformed correctly into text.
    }

    public function testBogusComment()
    {
        $bogus = array(
            '</+this is a bogus comment. +>',
            '<!+this is a bogus comment. !>',
            '<!D OCTYPE foo bar>',
            '<!DOCTYEP foo bar>',
            '<![CADATA[ TEST ]]>',
            '<![CDATA Hello ]]>',
            '<![CDATA[ Hello [[>',
            '<!CDATA[[ test ]]>',
            '<![CDATA[',
            '<![CDATA[hellooooo hello',
            '<? Hello World ?>',
            '<? Hello World',
        );
        foreach ($bogus as $str) {
            $events = $this->parse($str);
            $this->assertEventError($events->get(0));
            $this->assertEventEquals('comment', $str, $events->get(1));
        }
    }

    public function testEndTag()
    {
        $succeed = array(
            '' => 'a',
            '</test>' => 'test',
            '</test
      >' => 'test',
            '</thisIsTheTagThatDoesntEndItJustGoesOnAndOnMyFriend>' => 'thisisthetagthatdoesntenditjustgoesonandonmyfriend',
            // See 8.2.4.10, which requires this and does not say error.
            '</a' => 'aisAllGood('endTag', 2, $succeed);

        // Recoverable failures
        $fail = array(
            '' => 'a',
            '</a ' => 'a',
            '</a <b <c>' => 'a',
            '' => 'a',
            ' 'a',
        );
        foreach ($fail as $test => $result) {
            $events = $this->parse($test);
            $this->assertEquals(3, $events->depth());
            // Should have triggered an error.
            $this->assertEventError($events->get(0));
            // Should have tried to parse anyway.
            $this->assertEventEquals('endTag', $result, $events->get(1));
        }

        // BogoComments
        $comments = array(
            '</>' => '</>',
            '</ >' => '</ >',
            '' => '',
        );
        foreach ($comments as $test => $result) {
            $events = $this->parse($test);
            $this->assertEquals(3, $events->depth());

            // Should have triggered an error.
            $this->assertEventError($events->get(0));

            // Should have tried to parse anyway.
            $this->assertEventEquals('comment', $result, $events->get(1));
        }
    }

    public function testComment()
    {
        $good = array(
            '' => 'easy',
            '' => ' 1 > 0 ',
            '' => ' --$i ',
            '' => '--$i',
            "" => "\nHello World.\na",
            '' => ' ' => '',
            '',
            '' => '<![CDATA[not a comment]]>',
        );
        foreach ($good as $test => $expects) {
            $events = $this->parse($test);
            $this->assertEventEquals('startTag', 'script', $events->get(0));
            $this->assertEventEquals('text', $expects, $events->get(1));
            $this->assertEventEquals('endTag', 'script', $events->get(2));
        }

        $bad = array(
            ' '&</script',
            '


code:
<?php
/**
 * @file
 * Test the Tree Builder's special-case rules.
 */

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Parser\TreeBuildingRules;
use Masterminds\HTML5\Parser\Tokenizer;
use Masterminds\HTML5\Parser\Scanner;
use Masterminds\HTML5\Parser\DOMTreeBuilder;

/**
 * These tests are functional, not necessarily unit tests.
 */
class TreeBuildingRulesTest extends \Masterminds\HTML5\Tests\TestCase
{
    const HTML_STUB = '<!DOCTYPE html><title>test</title>%s';

    /**
     * Convenience function for parsing.
     */
    protected function parse($string)
    {
        $treeBuilder = new DOMTreeBuilder();
        $scanner = new Scanner($string);
        $parser = new Tokenizer($scanner, $treeBuilder);

        $parser->parse();

        return $treeBuilder->document();
    }

    /**
     * Convenience function for parsing fragments.
     */
    protected function parseFragment($string)
    {
        $events = new DOMTreeBuilder(true);
        $scanner = new Scanner($string);
        $parser = new Tokenizer($scanner, $events);

        $parser->parse();

        return $events->fragment();
    }

    public function testTDFragment()
    {
        $frag = $this->parseFragment('This is a test of the HTML5 parser	');

        $td = $frag->childNodes->item(0);

        $this->assertEquals(1, $frag->childNodes->length);
        $this->assertEquals('td', $td->tagName);
        $this->assertEquals('This is a test of the HTML5 parser', $td->nodeValue);
    }

    public function testHasRules()
    {
        $doc = new \DOMDocument('1.0');
        $engine = new TreeBuildingRules($doc);

        $this->assertTrue($engine->hasRules('li'));
        $this->assertFalse($engine->hasRules('imaginary'));
    }

    public function testHandleLI()
    {
        $html = sprintf(self::HTML_STUB, '

test
test2
');
        $doc = $this->parse($html);

        $list = $doc->getElementById('a');

        $this->assertEquals(2, $list->childNodes->length);
        foreach ($list->childNodes as $ele) {
            $this->assertEquals('li', $ele->tagName);
        }
    }

    public function testHandleDT()
    {
        $html = sprintf(self::HTML_STUB, '

Hello
Hi
');
        $doc = $this->parse($html);

        $list = $doc->getElementById('a');

        $this->assertEquals(2, $list->childNodes->length);
        $this->assertEquals('dt', $list->firstChild->tagName);
        $this->assertEquals('dd', $list->lastChild->tagName);
    }

    public function testHandleOptionGroupAndOption()
    {
        $html = sprintf(self::HTML_STUB, '<optgroup id="foo" label="foo" ><option value="foo">bar</option></optgroup>');
        $doc = $this->parse($html);

        $list = $doc->getElementById('foo');

        $this->assertEquals(1, $list->childNodes->length);

        $option = $list->childNodes->item(0);
        $this->assertEquals('option', $option->tagName);
    }

    public function testTable()
    {
        $html = sprintf(self::HTML_STUB, '

foobarbaz');
        $doc = $this->parse($html);

        $list = $doc->getElementById('a');

        $this->assertEquals(3, $list->childNodes->length);
        $this->assertEquals('th', $list->firstChild->tagName);
        $this->assertEquals('td', $list->lastChild->tagName);
    }
}


code:
<?php

namespace Masterminds\HTML5\Tests\Parser;

use Masterminds\HTML5\Parser\UTF8Utils;

class UTF8UtilsTest extends \Masterminds\HTML5\Tests\TestCase
{
    public function testConvertToUTF8()
    {
        $out = UTF8Utils::convertToUTF8('éàa', 'ISO-8859-1');
        $this->assertEquals('Ã©Ã a', $out);
    }

    /**
     * @todo add tests for invalid codepoints
     */
    public function testCheckForIllegalCodepoints()
    {
        $smoke = 'Smoke test';
        $err = UTF8Utils::checkForIllegalCodepoints($smoke);
        $this->assertEmpty($err);

        $data = "Foo Bar \0 Baz";
        $errors = UTF8Utils::checkForIllegalCodepoints($data);
        $this->assertContains('null-character', $errors);
    }
}