summaryrefslogtreecommitdiff
path: root/vendor/masterminds/html5/src/HTML5/Parser
diff options
context:
space:
mode:
authorAndrew Dolgov <[email protected]>2022-12-13 20:00:46 +0300
committerAndrew Dolgov <[email protected]>2022-12-13 20:00:46 +0300
commit0226e0ca0dc70f9a0310b3eef045ee1c1e0ca3ac (patch)
tree254c0e880693c2636107308b5931951cfa501311 /vendor/masterminds/html5/src/HTML5/Parser
split into a separate repo
Diffstat (limited to 'vendor/masterminds/html5/src/HTML5/Parser')
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/CharacterReference.php61
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php705
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/EventHandler.php114
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/FileInputStream.php33
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/InputStream.php87
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/ParseError.php10
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/README.md53
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/Scanner.php416
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/StringInputStream.php336
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php1191
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php127
-rw-r--r--vendor/masterminds/html5/src/HTML5/Parser/UTF8Utils.php183
12 files changed, 3316 insertions, 0 deletions
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/CharacterReference.php b/vendor/masterminds/html5/src/HTML5/Parser/CharacterReference.php
new file mode 100644
index 0000000..490b548
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/CharacterReference.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+use Masterminds\HTML5\Entities;
+
+/**
+ * Manage entity references.
+ *
+ * This is a simple resolver for HTML5 character reference entitites. See Entities for the list of supported entities.
+ */
+class CharacterReference
+{
+ protected static $numeric_mask = array(
+ 0x0,
+ 0x2FFFF,
+ 0,
+ 0xFFFF,
+ );
+
+ /**
+ * Given a name (e.g. 'amp'), lookup the UTF-8 character ('&').
+ *
+ * @param string $name The name to look up.
+ *
+ * @return string The character sequence. In UTF-8 this may be more than one byte.
+ */
+ public static function lookupName($name)
+ {
+ // Do we really want to return NULL here? or FFFD
+ return isset(Entities::$byName[$name]) ? Entities::$byName[$name] : null;
+ }
+
+ /**
+ * Given a decimal number, return the UTF-8 character.
+ *
+ * @param $int
+ *
+ * @return false|string|string[]|null
+ */
+ public static function lookupDecimal($int)
+ {
+ $entity = '&#' . $int . ';';
+
+ // UNTESTED: This may fail on some planes. Couldn't find full documentation
+ // on the value of the mask array.
+ return mb_decode_numericentity($entity, static::$numeric_mask, 'utf-8');
+ }
+
+ /**
+ * Given a hexidecimal number, return the UTF-8 character.
+ *
+ * @param $hexdec
+ *
+ * @return false|string|string[]|null
+ */
+ public static function lookupHex($hexdec)
+ {
+ return static::lookupDecimal(hexdec($hexdec));
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php b/vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php
new file mode 100644
index 0000000..293d83e
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php
@@ -0,0 +1,705 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+use Masterminds\HTML5\Elements;
+use Masterminds\HTML5\InstructionProcessor;
+
+/**
+ * Create an HTML5 DOM tree from events.
+ *
+ * This attempts to create a DOM from events emitted by a parser. This
+ * attempts (but does not guarantee) to up-convert older HTML documents
+ * to HTML5. It does this by applying HTML5's rules, but it will not
+ * change the architecture of the document itself.
+ *
+ * Many of the error correction and quirks features suggested in the specification
+ * are implemented herein; however, not all of them are. Since we do not
+ * assume a graphical user agent, no presentation-specific logic is conducted
+ * during tree building.
+ *
+ * FIXME: The present tree builder does not exactly follow the state machine rules
+ * for insert modes as outlined in the HTML5 spec. The processor needs to be
+ * re-written to accomodate this. See, for example, the Go language HTML5
+ * parser.
+ */
+class DOMTreeBuilder implements EventHandler
+{
+ /**
+ * 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/';
+
+ const OPT_DISABLE_HTML_NS = 'disable_html_ns';
+
+ const OPT_TARGET_DOC = 'target_document';
+
+ const OPT_IMPLICIT_NS = 'implicit_namespaces';
+
+ /**
+ * Holds the HTML5 element names that causes a namespace switch.
+ *
+ * @var array
+ */
+ protected $nsRoots = array(
+ 'html' => self::NAMESPACE_HTML,
+ 'svg' => self::NAMESPACE_SVG,
+ 'math' => self::NAMESPACE_MATHML,
+ );
+
+ /**
+ * Holds the always available namespaces (which does not require the XMLNS declaration).
+ *
+ * @var array
+ */
+ protected $implicitNamespaces = array(
+ 'xml' => self::NAMESPACE_XML,
+ 'xmlns' => self::NAMESPACE_XMLNS,
+ 'xlink' => self::NAMESPACE_XLINK,
+ );
+
+ /**
+ * Holds a stack of currently active namespaces.
+ *
+ * @var array
+ */
+ protected $nsStack = array();
+
+ /**
+ * Holds the number of namespaces declared by a node.
+ *
+ * @var array
+ */
+ protected $pushes = array();
+
+ /**
+ * Defined in 8.2.5.
+ */
+ const IM_INITIAL = 0;
+
+ const IM_BEFORE_HTML = 1;
+
+ const IM_BEFORE_HEAD = 2;
+
+ const IM_IN_HEAD = 3;
+
+ const IM_IN_HEAD_NOSCRIPT = 4;
+
+ const IM_AFTER_HEAD = 5;
+
+ const IM_IN_BODY = 6;
+
+ const IM_TEXT = 7;
+
+ const IM_IN_TABLE = 8;
+
+ const IM_IN_TABLE_TEXT = 9;
+
+ const IM_IN_CAPTION = 10;
+
+ const IM_IN_COLUMN_GROUP = 11;
+
+ const IM_IN_TABLE_BODY = 12;
+
+ const IM_IN_ROW = 13;
+
+ const IM_IN_CELL = 14;
+
+ const IM_IN_SELECT = 15;
+
+ const IM_IN_SELECT_IN_TABLE = 16;
+
+ const IM_AFTER_BODY = 17;
+
+ const IM_IN_FRAMESET = 18;
+
+ const IM_AFTER_FRAMESET = 19;
+
+ const IM_AFTER_AFTER_BODY = 20;
+
+ const IM_AFTER_AFTER_FRAMESET = 21;
+
+ const IM_IN_SVG = 22;
+
+ const IM_IN_MATHML = 23;
+
+ protected $options = array();
+
+ protected $stack = array();
+
+ protected $current; // Pointer in the tag hierarchy.
+ protected $rules;
+ protected $doc;
+
+ protected $frag;
+
+ protected $processor;
+
+ protected $insertMode = 0;
+
+ /**
+ * Track if we are in an element that allows only inline child nodes.
+ *
+ * @var string|null
+ */
+ protected $onlyInline;
+
+ /**
+ * Quirks mode is enabled by default.
+ * Any document that is missing the DT will be considered to be in quirks mode.
+ */
+ protected $quirks = true;
+
+ protected $errors = array();
+
+ public function __construct($isFragment = false, array $options = array())
+ {
+ $this->options = $options;
+
+ if (isset($options[self::OPT_TARGET_DOC])) {
+ $this->doc = $options[self::OPT_TARGET_DOC];
+ } else {
+ $impl = new \DOMImplementation();
+ // XXX:
+ // Create the doctype. For now, we are always creating HTML5
+ // documents, and attempting to up-convert any older DTDs to HTML5.
+ $dt = $impl->createDocumentType('html');
+ // $this->doc = \DOMImplementation::createDocument(NULL, 'html', $dt);
+ $this->doc = $impl->createDocument(null, '', $dt);
+ $this->doc->encoding = !empty($options['encoding']) ? $options['encoding'] : 'UTF-8';
+ }
+
+ $this->errors = array();
+
+ $this->current = $this->doc; // ->documentElement;
+
+ // Create a rules engine for tags.
+ $this->rules = new TreeBuildingRules();
+
+ $implicitNS = array();
+ if (isset($this->options[self::OPT_IMPLICIT_NS])) {
+ $implicitNS = $this->options[self::OPT_IMPLICIT_NS];
+ } elseif (isset($this->options['implicitNamespaces'])) {
+ $implicitNS = $this->options['implicitNamespaces'];
+ }
+
+ // Fill $nsStack with the defalut HTML5 namespaces, plus the "implicitNamespaces" array taken form $options
+ array_unshift($this->nsStack, $implicitNS + array('' => self::NAMESPACE_HTML) + $this->implicitNamespaces);
+
+ if ($isFragment) {
+ $this->insertMode = static::IM_IN_BODY;
+ $this->frag = $this->doc->createDocumentFragment();
+ $this->current = $this->frag;
+ }
+ }
+
+ /**
+ * Get the document.
+ */
+ public function document()
+ {
+ return $this->doc;
+ }
+
+ /**
+ * Get the DOM fragment for the body.
+ *
+ * This returns a DOMNodeList because a fragment may have zero or more
+ * DOMNodes at its root.
+ *
+ * @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#concept-frag-parse-context
+ *
+ * @return \DOMDocumentFragment
+ */
+ public function fragment()
+ {
+ return $this->frag;
+ }
+
+ /**
+ * Provide an instruction processor.
+ *
+ * This is used for handling Processor Instructions as they are
+ * inserted. If omitted, PI's are inserted directly into the DOM tree.
+ *
+ * @param InstructionProcessor $proc
+ */
+ public function setInstructionProcessor(InstructionProcessor $proc)
+ {
+ $this->processor = $proc;
+ }
+
+ public function doctype($name, $idType = 0, $id = null, $quirks = false)
+ {
+ // This is used solely for setting quirks mode. Currently we don't
+ // try to preserve the inbound DT. We convert it to HTML5.
+ $this->quirks = $quirks;
+
+ if ($this->insertMode > static::IM_INITIAL) {
+ $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name);
+
+ return;
+ }
+
+ $this->insertMode = static::IM_BEFORE_HTML;
+ }
+
+ /**
+ * Process the start tag.
+ *
+ * @todo - XMLNS namespace handling (we need to parse, even if it's not valid)
+ * - XLink, MathML and SVG namespace handling
+ * - Omission rules: 8.1.2.4 Optional tags
+ *
+ * @param string $name
+ * @param array $attributes
+ * @param bool $selfClosing
+ *
+ * @return int
+ */
+ public function startTag($name, $attributes = array(), $selfClosing = false)
+ {
+ $lname = $this->normalizeTagName($name);
+
+ // Make sure we have an html element.
+ if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) {
+ $this->startTag('html');
+ }
+
+ // Set quirks mode if we're at IM_INITIAL with no doctype.
+ if ($this->insertMode === static::IM_INITIAL) {
+ $this->quirks = true;
+ $this->parseError('No DOCTYPE specified.');
+ }
+
+ // SPECIAL TAG HANDLING:
+ // Spec says do this, and "don't ask."
+ // find the spec where this is defined... looks problematic
+ if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) {
+ $name = 'img';
+ }
+
+ // Autoclose p tags where appropriate.
+ if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) {
+ $this->autoclose('p');
+ }
+
+ // Set insert mode:
+ switch ($name) {
+ case 'html':
+ $this->insertMode = static::IM_BEFORE_HEAD;
+ break;
+ case 'head':
+ if ($this->insertMode > static::IM_BEFORE_HEAD) {
+ $this->parseError('Unexpected head tag outside of head context.');
+ } else {
+ $this->insertMode = static::IM_IN_HEAD;
+ }
+ break;
+ case 'body':
+ $this->insertMode = static::IM_IN_BODY;
+ break;
+ case 'svg':
+ $this->insertMode = static::IM_IN_SVG;
+ break;
+ case 'math':
+ $this->insertMode = static::IM_IN_MATHML;
+ break;
+ case 'noscript':
+ if ($this->insertMode === static::IM_IN_HEAD) {
+ $this->insertMode = static::IM_IN_HEAD_NOSCRIPT;
+ }
+ break;
+ }
+
+ // Special case handling for SVG.
+ if ($this->insertMode === static::IM_IN_SVG) {
+ $lname = Elements::normalizeSvgElement($lname);
+ }
+
+ $pushes = 0;
+ // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace
+ if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) {
+ array_unshift($this->nsStack, array(
+ '' => $this->nsRoots[$lname],
+ ) + $this->nsStack[0]);
+ ++$pushes;
+ }
+ $needsWorkaround = false;
+ if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) {
+ // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack
+ foreach ($attributes as $aName => $aVal) {
+ if ('xmlns' === $aName) {
+ $needsWorkaround = $aVal;
+ array_unshift($this->nsStack, array(
+ '' => $aVal,
+ ) + $this->nsStack[0]);
+ ++$pushes;
+ } elseif ('xmlns' === (($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : '')) {
+ array_unshift($this->nsStack, array(
+ substr($aName, $pos + 1) => $aVal,
+ ) + $this->nsStack[0]);
+ ++$pushes;
+ }
+ }
+ }
+
+ if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) {
+ $this->autoclose($this->onlyInline);
+ $this->onlyInline = null;
+ }
+
+ try {
+ $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : '';
+
+ if (false !== $needsWorkaround) {
+ $xml = "<$lname xmlns=\"$needsWorkaround\" " . (strlen($prefix) && isset($this->nsStack[0][$prefix]) ? ("xmlns:$prefix=\"" . $this->nsStack[0][$prefix] . '"') : '') . '/>';
+
+ $frag = new \DOMDocument('1.0', 'UTF-8');
+ $frag->loadXML($xml);
+
+ $ele = $this->doc->importNode($frag->documentElement, true);
+ } else {
+ if (!isset($this->nsStack[0][$prefix]) || ('' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS])) {
+ $ele = $this->doc->createElement($lname);
+ } else {
+ $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname);
+ }
+ }
+ } catch (\DOMException $e) {
+ $this->parseError("Illegal tag name: <$lname>. Replaced with <invalid>.");
+ $ele = $this->doc->createElement('invalid');
+ }
+
+ if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) {
+ $this->onlyInline = $lname;
+ }
+
+ // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them.
+ // When we are on a void tag, we do not need to care about namesapce nesting.
+ if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) {
+ // PHP tends to free the memory used by DOM,
+ // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes
+ // see https://bugs.php.net/bug.php?id=67459
+ $this->pushes[spl_object_hash($ele)] = array($pushes, $ele);
+ }
+
+ foreach ($attributes as $aName => $aVal) {
+ // xmlns attributes can't be set
+ if ('xmlns' === $aName) {
+ continue;
+ }
+
+ if ($this->insertMode === static::IM_IN_SVG) {
+ $aName = Elements::normalizeSvgAttribute($aName);
+ } elseif ($this->insertMode === static::IM_IN_MATHML) {
+ $aName = Elements::normalizeMathMlAttribute($aName);
+ }
+
+ $aVal = (string) $aVal;
+
+ try {
+ $prefix = ($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : false;
+
+ if ('xmlns' === $prefix) {
+ $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal);
+ } elseif (false !== $prefix && isset($this->nsStack[0][$prefix])) {
+ $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal);
+ } else {
+ $ele->setAttribute($aName, $aVal);
+ }
+ } catch (\DOMException $e) {
+ $this->parseError("Illegal attribute name for tag $name. Ignoring: $aName");
+ continue;
+ }
+
+ // This is necessary on a non-DTD schema, like HTML5.
+ if ('id' === $aName) {
+ $ele->setIdAttribute('id', true);
+ }
+ }
+
+ if ($this->frag !== $this->current && $this->rules->hasRules($name)) {
+ // Some elements have special processing rules. Handle those separately.
+ $this->current = $this->rules->evaluate($ele, $this->current);
+ } else {
+ // Otherwise, it's a standard element.
+ $this->current->appendChild($ele);
+
+ if (!Elements::isA($name, Elements::VOID_TAG)) {
+ $this->current = $ele;
+ }
+
+ // Self-closing tags should only be respected on foreign elements
+ // (and are implied on void elements)
+ // See: https://www.w3.org/TR/html5/syntax.html#start-tags
+ if (Elements::isHtml5Element($name)) {
+ $selfClosing = false;
+ }
+ }
+
+ // This is sort of a last-ditch attempt to correct for cases where no head/body
+ // elements are provided.
+ if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) {
+ $this->insertMode = static::IM_IN_BODY;
+ }
+
+ // When we are on a void tag, we do not need to care about namesapce nesting,
+ // but we have to remove the namespaces pushed to $nsStack.
+ if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) {
+ // remove the namespaced definded by current node
+ for ($i = 0; $i < $pushes; ++$i) {
+ array_shift($this->nsStack);
+ }
+ }
+
+ if ($selfClosing) {
+ $this->endTag($name);
+ }
+
+ // Return the element mask, which the tokenizer can then use to set
+ // various processing rules.
+ return Elements::element($name);
+ }
+
+ public function endTag($name)
+ {
+ $lname = $this->normalizeTagName($name);
+
+ // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag
+ if ('br' === $name) {
+ $this->parseError('Closing tag encountered for void element br.');
+
+ $this->startTag('br');
+ }
+ // Ignore closing tags for other unary elements.
+ elseif (Elements::isA($name, Elements::VOID_TAG)) {
+ return;
+ }
+
+ if ($this->insertMode <= static::IM_BEFORE_HTML) {
+ // 8.2.5.4.2
+ if (in_array($name, array(
+ 'html',
+ 'br',
+ 'head',
+ 'title',
+ ))) {
+ $this->startTag('html');
+ $this->endTag($name);
+ $this->insertMode = static::IM_BEFORE_HEAD;
+
+ return;
+ }
+
+ // Ignore the tag.
+ $this->parseError('Illegal closing tag at global scope.');
+
+ return;
+ }
+
+ // Special case handling for SVG.
+ if ($this->insertMode === static::IM_IN_SVG) {
+ $lname = Elements::normalizeSvgElement($lname);
+ }
+
+ $cid = spl_object_hash($this->current);
+
+ // XXX: HTML has no parent. What do we do, though,
+ // if this element appears in the wrong place?
+ if ('html' === $lname) {
+ return;
+ }
+
+ // remove the namespaced definded by current node
+ if (isset($this->pushes[$cid])) {
+ for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) {
+ array_shift($this->nsStack);
+ }
+ unset($this->pushes[$cid]);
+ }
+
+ if (!$this->autoclose($lname)) {
+ $this->parseError('Could not find closing tag for ' . $lname);
+ }
+
+ switch ($lname) {
+ case 'head':
+ $this->insertMode = static::IM_AFTER_HEAD;
+ break;
+ case 'body':
+ $this->insertMode = static::IM_AFTER_BODY;
+ break;
+ case 'svg':
+ case 'mathml':
+ $this->insertMode = static::IM_IN_BODY;
+ break;
+ }
+ }
+
+ public function comment($cdata)
+ {
+ // TODO: Need to handle case where comment appears outside of the HTML tag.
+ $node = $this->doc->createComment($cdata);
+ $this->current->appendChild($node);
+ }
+
+ public function text($data)
+ {
+ // XXX: Hmmm.... should we really be this strict?
+ if ($this->insertMode < static::IM_IN_HEAD) {
+ // Per '8.2.5.4.3 The "before head" insertion mode' the characters
+ // " \t\n\r\f" should be ignored but no mention of a parse error. This is
+ // practical as most documents contain these characters. Other text is not
+ // expected here so recording a parse error is necessary.
+ $dataTmp = trim($data, " \t\n\r\f");
+ if (!empty($dataTmp)) {
+ // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode);
+ $this->parseError('Unexpected text. Ignoring: ' . $dataTmp);
+ }
+
+ return;
+ }
+ // fprintf(STDOUT, "Appending text %s.", $data);
+ $node = $this->doc->createTextNode($data);
+ $this->current->appendChild($node);
+ }
+
+ public function eof()
+ {
+ // If the $current isn't the $root, do we need to do anything?
+ }
+
+ public function parseError($msg, $line = 0, $col = 0)
+ {
+ $this->errors[] = sprintf('Line %d, Col %d: %s', $line, $col, $msg);
+ }
+
+ public function getErrors()
+ {
+ return $this->errors;
+ }
+
+ public function cdata($data)
+ {
+ $node = $this->doc->createCDATASection($data);
+ $this->current->appendChild($node);
+ }
+
+ public function processingInstruction($name, $data = null)
+ {
+ // XXX: Ignore initial XML declaration, per the spec.
+ if ($this->insertMode === static::IM_INITIAL && 'xml' === strtolower($name)) {
+ return;
+ }
+
+ // Important: The processor may modify the current DOM tree however it sees fit.
+ if ($this->processor instanceof InstructionProcessor) {
+ $res = $this->processor->process($this->current, $name, $data);
+ if (!empty($res)) {
+ $this->current = $res;
+ }
+
+ return;
+ }
+
+ // Otherwise, this is just a dumb PI element.
+ $node = $this->doc->createProcessingInstruction($name, $data);
+
+ $this->current->appendChild($node);
+ }
+
+ // ==========================================================================
+ // UTILITIES
+ // ==========================================================================
+
+ /**
+ * Apply normalization rules to a tag name.
+ * See sections 2.9 and 8.1.2.
+ *
+ * @param string $tagName
+ *
+ * @return string The normalized tag name.
+ */
+ protected function normalizeTagName($tagName)
+ {
+ /*
+ * Section 2.9 suggests that we should not do this. if (strpos($name, ':') !== false) { // We know from the grammar that there must be at least one other // char besides :, since : is not a legal tag start. $parts = explode(':', $name); return array_pop($parts); }
+ */
+ return $tagName;
+ }
+
+ protected function quirksTreeResolver($name)
+ {
+ throw new \Exception('Not implemented.');
+ }
+
+ /**
+ * Automatically climb the tree and close the closest node with the matching $tag.
+ *
+ * @param string $tagName
+ *
+ * @return bool
+ */
+ protected function autoclose($tagName)
+ {
+ $working = $this->current;
+ do {
+ if (XML_ELEMENT_NODE !== $working->nodeType) {
+ return false;
+ }
+ if ($working->tagName === $tagName) {
+ $this->current = $working->parentNode;
+
+ return true;
+ }
+ } while ($working = $working->parentNode);
+
+ return false;
+ }
+
+ /**
+ * Checks if the given tagname is an ancestor of the present candidate.
+ *
+ * If $this->current or anything above $this->current matches the given tag
+ * name, this returns true.
+ *
+ * @param string $tagName
+ *
+ * @return bool
+ */
+ protected function isAncestor($tagName)
+ {
+ $candidate = $this->current;
+ while (XML_ELEMENT_NODE === $candidate->nodeType) {
+ if ($candidate->tagName === $tagName) {
+ return true;
+ }
+ $candidate = $candidate->parentNode;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns true if the immediate parent element is of the given tagname.
+ *
+ * @param string $tagName
+ *
+ * @return bool
+ */
+ protected function isParent($tagName)
+ {
+ return $this->current->tagName === $tagName;
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/EventHandler.php b/vendor/masterminds/html5/src/HTML5/Parser/EventHandler.php
new file mode 100644
index 0000000..9893a71
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/EventHandler.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+/**
+ * Standard events for HTML5.
+ *
+ * This is roughly analogous to a SAX2 or expat-style interface.
+ * However, it is tuned specifically for HTML5, according to section 8
+ * of the HTML5 specification.
+ *
+ * An event handler receives parser events. For a concrete
+ * implementation, see DOMTreeBuilder.
+ *
+ * Quirks support in the parser is limited to close-in syntax (malformed
+ * tags or attributes). Higher order syntax and semantic issues with a
+ * document (e.g. mismatched tags, illegal nesting, etc.) are the
+ * responsibility of the event handler implementation.
+ *
+ * See HTML5 spec section 8.2.4
+ */
+interface EventHandler
+{
+ const DOCTYPE_NONE = 0;
+
+ const DOCTYPE_PUBLIC = 1;
+
+ const DOCTYPE_SYSTEM = 2;
+
+ /**
+ * A doctype declaration.
+ *
+ * @param string $name The name of the root element.
+ * @param int $idType One of DOCTYPE_NONE, DOCTYPE_PUBLIC, or DOCTYPE_SYSTEM
+ * @param string $id The identifier. For DOCTYPE_PUBLIC, this is the public ID. If DOCTYPE_SYSTEM,
+ * then this is a system ID.
+ * @param bool $quirks Indicates whether the builder should enter quirks mode.
+ */
+ public function doctype($name, $idType = 0, $id = null, $quirks = false);
+
+ /**
+ * A start tag.
+ *
+ * IMPORTANT: The parser watches the return value of this event. If this returns
+ * an integer, the parser will switch TEXTMODE patters according to the int.
+ *
+ * This is how the Tree Builder can tell the Tokenizer when a certain tag should
+ * cause the parser to go into RAW text mode.
+ *
+ * The HTML5 standard requires that the builder is the one that initiates this
+ * step, and this is the only way short of a circular reference that we can
+ * do that.
+ *
+ * Example: if a startTag even for a `script` name is fired, and the startTag()
+ * implementation returns Tokenizer::TEXTMODE_RAW, then the tokenizer will
+ * switch into RAW text mode and consume data until it reaches a closing
+ * `script` tag.
+ *
+ * The textmode is automatically reset to Tokenizer::TEXTMODE_NORMAL when the
+ * closing tag is encounter. **This behavior may change.**
+ *
+ * @param string $name The tag name.
+ * @param array $attributes An array with all of the tag's attributes.
+ * @param bool $selfClosing An indicator of whether or not this tag is self-closing (<foo/>).
+ *
+ * @return int one of the Tokenizer::TEXTMODE_* constants
+ */
+ public function startTag($name, $attributes = array(), $selfClosing = false);
+
+ /**
+ * An end-tag.
+ */
+ public function endTag($name);
+
+ /**
+ * A comment section (unparsed character data).
+ */
+ public function comment($cdata);
+
+ /**
+ * A unit of parsed character data.
+ *
+ * Entities in this text are *already decoded*.
+ */
+ public function text($cdata);
+
+ /**
+ * Indicates that the document has been entirely processed.
+ */
+ public function eof();
+
+ /**
+ * Emitted when the parser encounters an error condition.
+ */
+ public function parseError($msg, $line, $col);
+
+ /**
+ * A CDATA section.
+ *
+ * @param string $data
+ * The unparsed character data
+ */
+ public function cdata($data);
+
+ /**
+ * This is a holdover from the XML spec.
+ *
+ * While user agents don't get PIs, server-side does.
+ *
+ * @param string $name The name of the processor (e.g. 'php').
+ * @param string $data The unparsed data.
+ */
+ public function processingInstruction($name, $data = null);
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/FileInputStream.php b/vendor/masterminds/html5/src/HTML5/Parser/FileInputStream.php
new file mode 100644
index 0000000..b081ed9
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/FileInputStream.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+/**
+ * The FileInputStream loads a file to be parsed.
+ *
+ * So right now we read files into strings and then process the
+ * string. We chose to do this largely for the sake of expediency of
+ * development, and also because we could optimize toward processing
+ * arbitrarily large chunks of the input. But in the future, we'd
+ * really like to rewrite this class to efficiently handle lower level
+ * stream reads (and thus efficiently handle large documents).
+ *
+ * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
+ */
+class FileInputStream extends StringInputStream implements InputStream
+{
+ /**
+ * Load a file input stream.
+ *
+ * @param string $data The file or url path to load.
+ * @param string $encoding The encoding to use for the data.
+ * @param string $debug A fprintf format to use to echo the data on stdout.
+ */
+ public function __construct($data, $encoding = 'UTF-8', $debug = '')
+ {
+ // Get the contents of the file.
+ $content = file_get_contents($data);
+
+ parent::__construct($content, $encoding, $debug);
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/InputStream.php b/vendor/masterminds/html5/src/HTML5/Parser/InputStream.php
new file mode 100644
index 0000000..ff4ef22
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/InputStream.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+/**
+ * Interface for stream readers.
+ *
+ * The parser only reads from streams. Various input sources can write
+ * an adapater to this InputStream.
+ *
+ * Currently provided InputStream implementations include
+ * FileInputStream and StringInputStream.
+ *
+ * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
+ */
+interface InputStream extends \Iterator
+{
+ /**
+ * Returns the current line that is being consumed.
+ *
+ * TODO: Move this to the scanner.
+ */
+ public function currentLine();
+
+ /**
+ * Returns the current column of the current line that the tokenizer is at.
+ *
+ * Newlines are column 0. The first char after a newline is column 1.
+ *
+ * @TODO Move this to the scanner.
+ *
+ * @return int The column number.
+ */
+ public function columnOffset();
+
+ /**
+ * Get all characters until EOF.
+ *
+ * This consumes characters until the EOF.
+ */
+ public function remainingChars();
+
+ /**
+ * Read to a particular match (or until $max bytes are consumed).
+ *
+ * This operates on byte sequences, not characters.
+ *
+ * Matches as far as possible until we reach a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @see strcspn
+ *
+ * @param string $bytes Bytes to match.
+ * @param int $max Maximum number of bytes to scan.
+ *
+ * @return mixed Index or false if no match is found. You should use strong
+ * equality when checking the result, since index could be 0.
+ */
+ public function charsUntil($bytes, $max = null);
+
+ /**
+ * Returns the string so long as $bytes matches.
+ *
+ * Matches as far as possible with a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @see strspn
+ *
+ * @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the
+ * current char, the pointer advances and the char is part of the
+ * substring.
+ * @param int $max The max number of chars to read.
+ */
+ public function charsWhile($bytes, $max = null);
+
+ /**
+ * Unconsume one character.
+ *
+ * @param int $howMany The number of characters to move the pointer back.
+ */
+ public function unconsume($howMany = 1);
+
+ /**
+ * Retrieve the next character without advancing the pointer.
+ */
+ public function peek();
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/ParseError.php b/vendor/masterminds/html5/src/HTML5/Parser/ParseError.php
new file mode 100644
index 0000000..640e516
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/ParseError.php
@@ -0,0 +1,10 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+/**
+ * Emit when the parser has an error.
+ */
+class ParseError extends \Exception
+{
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/README.md b/vendor/masterminds/html5/src/HTML5/Parser/README.md
new file mode 100644
index 0000000..9f92957
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/README.md
@@ -0,0 +1,53 @@
+# The Parser Model
+
+The parser model here follows the model in section
+[8.2.1](http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#parsing)
+of the HTML5 specification, though we do not assume a networking layer.
+
+ [ InputStream ] // Generic support for reading input.
+ ||
+ [ Scanner ] // Breaks down the stream into characters.
+ ||
+ [ Tokenizer ] // Groups characters into syntactic
+ ||
+ [ Tree Builder ] // Organizes units into a tree of objects
+ ||
+ [ DOM Document ] // The final state of the parsed document.
+
+
+## InputStream
+
+This is an interface with at least two concrete implementations:
+
+- StringInputStream: Reads an HTML5 string.
+- FileInputStream: Reads an HTML5 file.
+
+## Scanner
+
+This is a mechanical piece of the parser.
+
+## Tokenizer
+
+This follows section 8.4 of the HTML5 spec. It is (roughly) a recursive
+descent parser. (Though there are plenty of optimizations that are less
+than purely functional.
+
+## EventHandler and DOMTree
+
+EventHandler is the interface for tree builders. Since not all
+implementations will necessarily build trees, we've chosen a more
+generic name.
+
+The event handler emits tokens during tokenization.
+
+The DOMTree is an event handler that builds a DOM tree. The output of
+the DOMTree builder is a DOMDocument.
+
+## DOMDocument
+
+PHP has a DOMDocument class built-in (technically, it's part of libxml.)
+We use that, thus rendering the output of this process compatible with
+SimpleXML, QueryPath, and many other XML/HTML processing tools.
+
+For cases where the HTML5 is a fragment of a HTML5 document a
+DOMDocumentFragment is returned instead. This is another built-in class.
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/Scanner.php b/vendor/masterminds/html5/src/HTML5/Parser/Scanner.php
new file mode 100644
index 0000000..1b52983
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/Scanner.php
@@ -0,0 +1,416 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+use Masterminds\HTML5\Exception;
+
+/**
+ * The scanner scans over a given data input to react appropriately to characters.
+ */
+class Scanner
+{
+ const CHARS_HEX = 'abcdefABCDEF01234567890';
+ const CHARS_ALNUM = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890';
+ const CHARS_ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+ /**
+ * The string data we're parsing.
+ */
+ private $data;
+
+ /**
+ * The current integer byte position we are in $data.
+ */
+ private $char;
+
+ /**
+ * Length of $data; when $char === $data, we are at the end-of-file.
+ */
+ private $EOF;
+
+ /**
+ * Parse errors.
+ */
+ public $errors = array();
+
+ /**
+ * Create a new Scanner.
+ *
+ * @param string $data Data to parse.
+ * @param string $encoding The encoding to use for the data.
+ *
+ * @throws Exception If the given data cannot be encoded to UTF-8.
+ */
+ public function __construct($data, $encoding = 'UTF-8')
+ {
+ if ($data instanceof InputStream) {
+ @trigger_error('InputStream objects are deprecated since version 2.4 and will be removed in 3.0. Use strings instead.', E_USER_DEPRECATED);
+ $data = (string) $data;
+ }
+
+ $data = UTF8Utils::convertToUTF8($data, $encoding);
+
+ // There is good reason to question whether it makes sense to
+ // do this here, since most of these checks are done during
+ // parsing, and since this check doesn't actually *do* anything.
+ $this->errors = UTF8Utils::checkForIllegalCodepoints($data);
+
+ $data = $this->replaceLinefeeds($data);
+
+ $this->data = $data;
+ $this->char = 0;
+ $this->EOF = strlen($data);
+ }
+
+ /**
+ * Check if upcomming chars match the given sequence.
+ *
+ * This will read the stream for the $sequence. If it's
+ * found, this will return true. If not, return false.
+ * Since this unconsumes any chars it reads, the caller
+ * will still need to read the next sequence, even if
+ * this returns true.
+ *
+ * Example: $this->scanner->sequenceMatches('</script>') will
+ * see if the input stream is at the start of a
+ * '</script>' string.
+ *
+ * @param string $sequence
+ * @param bool $caseSensitive
+ *
+ * @return bool
+ */
+ public function sequenceMatches($sequence, $caseSensitive = true)
+ {
+ $portion = substr($this->data, $this->char, strlen($sequence));
+
+ return $caseSensitive ? $portion === $sequence : 0 === strcasecmp($portion, $sequence);
+ }
+
+ /**
+ * Get the current position.
+ *
+ * @return int The current intiger byte position.
+ */
+ public function position()
+ {
+ return $this->char;
+ }
+
+ /**
+ * Take a peek at the next character in the data.
+ *
+ * @return string The next character.
+ */
+ public function peek()
+ {
+ if (($this->char + 1) <= $this->EOF) {
+ return $this->data[$this->char + 1];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the next character.
+ * Note: This advances the pointer.
+ *
+ * @return string The next character.
+ */
+ public function next()
+ {
+ ++$this->char;
+
+ if ($this->char < $this->EOF) {
+ return $this->data[$this->char];
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the current character.
+ * Note, this does not advance the pointer.
+ *
+ * @return string The current character.
+ */
+ public function current()
+ {
+ if ($this->char < $this->EOF) {
+ return $this->data[$this->char];
+ }
+
+ return false;
+ }
+
+ /**
+ * Silently consume N chars.
+ *
+ * @param int $count
+ */
+ public function consume($count = 1)
+ {
+ $this->char += $count;
+ }
+
+ /**
+ * Unconsume some of the data.
+ * This moves the data pointer backwards.
+ *
+ * @param int $howMany The number of characters to move the pointer back.
+ */
+ public function unconsume($howMany = 1)
+ {
+ if (($this->char - $howMany) >= 0) {
+ $this->char -= $howMany;
+ }
+ }
+
+ /**
+ * Get the next group of that contains hex characters.
+ * Note, along with getting the characters the pointer in the data will be
+ * moved as well.
+ *
+ * @return string The next group that is hex characters.
+ */
+ public function getHex()
+ {
+ return $this->doCharsWhile(static::CHARS_HEX);
+ }
+
+ /**
+ * Get the next group of characters that are ASCII Alpha characters.
+ * Note, along with getting the characters the pointer in the data will be
+ * moved as well.
+ *
+ * @return string The next group of ASCII alpha characters.
+ */
+ public function getAsciiAlpha()
+ {
+ return $this->doCharsWhile(static::CHARS_ALPHA);
+ }
+
+ /**
+ * Get the next group of characters that are ASCII Alpha characters and numbers.
+ * Note, along with getting the characters the pointer in the data will be
+ * moved as well.
+ *
+ * @return string The next group of ASCII alpha characters and numbers.
+ */
+ public function getAsciiAlphaNum()
+ {
+ return $this->doCharsWhile(static::CHARS_ALNUM);
+ }
+
+ /**
+ * Get the next group of numbers.
+ * Note, along with getting the characters the pointer in the data will be
+ * moved as well.
+ *
+ * @return string The next group of numbers.
+ */
+ public function getNumeric()
+ {
+ return $this->doCharsWhile('0123456789');
+ }
+
+ /**
+ * Consume whitespace.
+ * Whitespace in HTML5 is: formfeed, tab, newline, space.
+ *
+ * @return int The length of the matched whitespaces.
+ */
+ public function whitespace()
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ $len = strspn($this->data, "\n\t\f ", $this->char);
+
+ $this->char += $len;
+
+ return $len;
+ }
+
+ /**
+ * Returns the current line that is being consumed.
+ *
+ * @return int The current line number.
+ */
+ public function currentLine()
+ {
+ if (empty($this->EOF) || 0 === $this->char) {
+ return 1;
+ }
+
+ // Add one to $this->char because we want the number for the next
+ // byte to be processed.
+ return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
+ }
+
+ /**
+ * Read chars until something in the mask is encountered.
+ *
+ * @param string $mask
+ *
+ * @return mixed
+ */
+ public function charsUntil($mask)
+ {
+ return $this->doCharsUntil($mask);
+ }
+
+ /**
+ * Read chars as long as the mask matches.
+ *
+ * @param string $mask
+ *
+ * @return int
+ */
+ public function charsWhile($mask)
+ {
+ return $this->doCharsWhile($mask);
+ }
+
+ /**
+ * Returns the current column of the current line that the tokenizer is at.
+ *
+ * Newlines are column 0. The first char after a newline is column 1.
+ *
+ * @return int The column number.
+ */
+ public function columnOffset()
+ {
+ // Short circuit for the first char.
+ if (0 === $this->char) {
+ return 0;
+ }
+
+ // strrpos is weird, and the offset needs to be negative for what we
+ // want (i.e., the last \n before $this->char). This needs to not have
+ // one (to make it point to the next character, the one we want the
+ // position of) added to it because strrpos's behaviour includes the
+ // final offset byte.
+ $backwardFrom = $this->char - 1 - strlen($this->data);
+ $lastLine = strrpos($this->data, "\n", $backwardFrom);
+
+ // However, for here we want the length up until the next byte to be
+ // processed, so add one to the current byte ($this->char).
+ if (false !== $lastLine) {
+ $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
+ } else {
+ // After a newline.
+ $findLengthOf = substr($this->data, 0, $this->char);
+ }
+
+ return UTF8Utils::countChars($findLengthOf);
+ }
+
+ /**
+ * Get all characters until EOF.
+ *
+ * This consumes characters until the EOF.
+ *
+ * @return int The number of characters remaining.
+ */
+ public function remainingChars()
+ {
+ if ($this->char < $this->EOF) {
+ $data = substr($this->data, $this->char);
+ $this->char = $this->EOF;
+
+ return $data;
+ }
+
+ return ''; // false;
+ }
+
+ /**
+ * Replace linefeed characters according to the spec.
+ *
+ * @param $data
+ *
+ * @return string
+ */
+ private function replaceLinefeeds($data)
+ {
+ /*
+ * U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially.
+ * Any CR characters that are followed by LF characters must be removed, and any CR characters not
+ * followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are
+ * represented by LF characters, and there are never any CR characters in the input to the tokenization
+ * stage.
+ */
+ $crlfTable = array(
+ "\0" => "\xEF\xBF\xBD",
+ "\r\n" => "\n",
+ "\r" => "\n",
+ );
+
+ return strtr($data, $crlfTable);
+ }
+
+ /**
+ * Read to a particular match (or until $max bytes are consumed).
+ *
+ * This operates on byte sequences, not characters.
+ *
+ * Matches as far as possible until we reach a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @param string $bytes Bytes to match.
+ * @param int $max Maximum number of bytes to scan.
+ *
+ * @return mixed Index or false if no match is found. You should use strong
+ * equality when checking the result, since index could be 0.
+ */
+ private function doCharsUntil($bytes, $max = null)
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ if (0 === $max || $max) {
+ $len = strcspn($this->data, $bytes, $this->char, $max);
+ } else {
+ $len = strcspn($this->data, $bytes, $this->char);
+ }
+
+ $string = (string) substr($this->data, $this->char, $len);
+ $this->char += $len;
+
+ return $string;
+ }
+
+ /**
+ * Returns the string so long as $bytes matches.
+ *
+ * Matches as far as possible with a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the
+ * current char, the pointer advances and the char is part of the
+ * substring.
+ * @param int $max The max number of chars to read.
+ *
+ * @return string
+ */
+ private function doCharsWhile($bytes, $max = null)
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ if (0 === $max || $max) {
+ $len = strspn($this->data, $bytes, $this->char, $max);
+ } else {
+ $len = strspn($this->data, $bytes, $this->char);
+ }
+
+ $string = (string) substr($this->data, $this->char, $len);
+ $this->char += $len;
+
+ return $string;
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/StringInputStream.php b/vendor/masterminds/html5/src/HTML5/Parser/StringInputStream.php
new file mode 100644
index 0000000..75b0886
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/StringInputStream.php
@@ -0,0 +1,336 @@
+<?php
+/**
+ * Loads a string to be parsed.
+ */
+
+namespace Masterminds\HTML5\Parser;
+
+/*
+ *
+* Based on code from html5lib:
+
+Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+// Some conventions:
+// - /* */ indicates verbatim text from the HTML 5 specification
+// MPB: Not sure which version of the spec. Moving from HTML5lib to
+// HTML5-PHP, I have been using this version:
+// http://www.w3.org/TR/2012/CR-html5-20121217/Overview.html#contents
+//
+// - // indicates regular comments
+
+/**
+ * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
+ */
+class StringInputStream implements InputStream
+{
+ /**
+ * The string data we're parsing.
+ */
+ private $data;
+
+ /**
+ * The current integer byte position we are in $data.
+ */
+ private $char;
+
+ /**
+ * Length of $data; when $char === $data, we are at the end-of-file.
+ */
+ private $EOF;
+
+ /**
+ * Parse errors.
+ */
+ public $errors = array();
+
+ /**
+ * Create a new InputStream wrapper.
+ *
+ * @param string $data Data to parse.
+ * @param string $encoding The encoding to use for the data.
+ * @param string $debug A fprintf format to use to echo the data on stdout.
+ */
+ public function __construct($data, $encoding = 'UTF-8', $debug = '')
+ {
+ $data = UTF8Utils::convertToUTF8($data, $encoding);
+ if ($debug) {
+ fprintf(STDOUT, $debug, $data, strlen($data));
+ }
+
+ // There is good reason to question whether it makes sense to
+ // do this here, since most of these checks are done during
+ // parsing, and since this check doesn't actually *do* anything.
+ $this->errors = UTF8Utils::checkForIllegalCodepoints($data);
+
+ $data = $this->replaceLinefeeds($data);
+
+ $this->data = $data;
+ $this->char = 0;
+ $this->EOF = strlen($data);
+ }
+
+ public function __toString()
+ {
+ return $this->data;
+ }
+
+ /**
+ * Replace linefeed characters according to the spec.
+ */
+ protected function replaceLinefeeds($data)
+ {
+ /*
+ * U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially.
+ * Any CR characters that are followed by LF characters must be removed, and any CR characters not
+ * followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are
+ * represented by LF characters, and there are never any CR characters in the input to the tokenization
+ * stage.
+ */
+ $crlfTable = array(
+ "\0" => "\xEF\xBF\xBD",
+ "\r\n" => "\n",
+ "\r" => "\n",
+ );
+
+ return strtr($data, $crlfTable);
+ }
+
+ /**
+ * Returns the current line that the tokenizer is at.
+ */
+ public function currentLine()
+ {
+ if (empty($this->EOF) || 0 === $this->char) {
+ return 1;
+ }
+ // Add one to $this->char because we want the number for the next
+ // byte to be processed.
+ return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
+ }
+
+ /**
+ * @deprecated
+ */
+ public function getCurrentLine()
+ {
+ return $this->currentLine();
+ }
+
+ /**
+ * Returns the current column of the current line that the tokenizer is at.
+ * Newlines are column 0. The first char after a newline is column 1.
+ *
+ * @return int The column number.
+ */
+ public function columnOffset()
+ {
+ // Short circuit for the first char.
+ if (0 === $this->char) {
+ return 0;
+ }
+ // strrpos is weird, and the offset needs to be negative for what we
+ // want (i.e., the last \n before $this->char). This needs to not have
+ // one (to make it point to the next character, the one we want the
+ // position of) added to it because strrpos's behaviour includes the
+ // final offset byte.
+ $backwardFrom = $this->char - 1 - strlen($this->data);
+ $lastLine = strrpos($this->data, "\n", $backwardFrom);
+
+ // However, for here we want the length up until the next byte to be
+ // processed, so add one to the current byte ($this->char).
+ if (false !== $lastLine) {
+ $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
+ } else {
+ // After a newline.
+ $findLengthOf = substr($this->data, 0, $this->char);
+ }
+
+ return UTF8Utils::countChars($findLengthOf);
+ }
+
+ /**
+ * @deprecated
+ */
+ public function getColumnOffset()
+ {
+ return $this->columnOffset();
+ }
+
+ /**
+ * Get the current character.
+ *
+ * @return string The current character.
+ */
+ #[\ReturnTypeWillChange]
+ public function current()
+ {
+ return $this->data[$this->char];
+ }
+
+ /**
+ * Advance the pointer.
+ * This is part of the Iterator interface.
+ */
+ #[\ReturnTypeWillChange]
+ public function next()
+ {
+ ++$this->char;
+ }
+
+ /**
+ * Rewind to the start of the string.
+ */
+ #[\ReturnTypeWillChange]
+ public function rewind()
+ {
+ $this->char = 0;
+ }
+
+ /**
+ * Is the current pointer location valid.
+ *
+ * @return bool Whether the current pointer location is valid.
+ */
+ #[\ReturnTypeWillChange]
+ public function valid()
+ {
+ return $this->char < $this->EOF;
+ }
+
+ /**
+ * Get all characters until EOF.
+ *
+ * This reads to the end of the file, and sets the read marker at the
+ * end of the file.
+ *
+ * Note this performs bounds checking.
+ *
+ * @return string Returns the remaining text. If called when the InputStream is
+ * already exhausted, it returns an empty string.
+ */
+ public function remainingChars()
+ {
+ if ($this->char < $this->EOF) {
+ $data = substr($this->data, $this->char);
+ $this->char = $this->EOF;
+
+ return $data;
+ }
+
+ return ''; // false;
+ }
+
+ /**
+ * Read to a particular match (or until $max bytes are consumed).
+ *
+ * This operates on byte sequences, not characters.
+ *
+ * Matches as far as possible until we reach a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @param string $bytes Bytes to match.
+ * @param int $max Maximum number of bytes to scan.
+ *
+ * @return mixed Index or false if no match is found. You should use strong
+ * equality when checking the result, since index could be 0.
+ */
+ public function charsUntil($bytes, $max = null)
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ if (0 === $max || $max) {
+ $len = strcspn($this->data, $bytes, $this->char, $max);
+ } else {
+ $len = strcspn($this->data, $bytes, $this->char);
+ }
+
+ $string = (string) substr($this->data, $this->char, $len);
+ $this->char += $len;
+
+ return $string;
+ }
+
+ /**
+ * Returns the string so long as $bytes matches.
+ *
+ * Matches as far as possible with a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the
+ * current char, the pointer advances and the char is part of the
+ * substring.
+ * @param int $max The max number of chars to read.
+ *
+ * @return string
+ */
+ public function charsWhile($bytes, $max = null)
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ if (0 === $max || $max) {
+ $len = strspn($this->data, $bytes, $this->char, $max);
+ } else {
+ $len = strspn($this->data, $bytes, $this->char);
+ }
+ $string = (string) substr($this->data, $this->char, $len);
+ $this->char += $len;
+
+ return $string;
+ }
+
+ /**
+ * Unconsume characters.
+ *
+ * @param int $howMany The number of characters to unconsume.
+ */
+ public function unconsume($howMany = 1)
+ {
+ if (($this->char - $howMany) >= 0) {
+ $this->char -= $howMany;
+ }
+ }
+
+ /**
+ * Look ahead without moving cursor.
+ */
+ public function peek()
+ {
+ if (($this->char + 1) <= $this->EOF) {
+ return $this->data[$this->char + 1];
+ }
+
+ return false;
+ }
+
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->char;
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php b/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php
new file mode 100644
index 0000000..300a446
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/Tokenizer.php
@@ -0,0 +1,1191 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+use Masterminds\HTML5\Elements;
+
+/**
+ * The HTML5 tokenizer.
+ *
+ * The tokenizer's role is reading data from the scanner and gathering it into
+ * semantic units. From the tokenizer, data is emitted to an event handler,
+ * which may (for example) create a DOM tree.
+ *
+ * The HTML5 specification has a detailed explanation of tokenizing HTML5. We
+ * follow that specification to the maximum extent that we can. If you find
+ * a discrepancy that is not documented, please file a bug and/or submit a
+ * patch.
+ *
+ * This tokenizer is implemented as a recursive descent parser.
+ *
+ * Within the API documentation, you may see references to the specific section
+ * of the HTML5 spec that the code attempts to reproduce. Example: 8.2.4.1.
+ * This refers to section 8.2.4.1 of the HTML5 CR specification.
+ *
+ * @see http://www.w3.org/TR/2012/CR-html5-20121217/
+ */
+class Tokenizer
+{
+ protected $scanner;
+
+ protected $events;
+
+ protected $tok;
+
+ /**
+ * Buffer for text.
+ */
+ protected $text = '';
+
+ // When this goes to false, the parser stops.
+ protected $carryOn = true;
+
+ protected $textMode = 0; // TEXTMODE_NORMAL;
+ protected $untilTag = null;
+
+ const CONFORMANT_XML = 'xml';
+ const CONFORMANT_HTML = 'html';
+ protected $mode = self::CONFORMANT_HTML;
+
+ /**
+ * Create a new tokenizer.
+ *
+ * Typically, parsing a document involves creating a new tokenizer, giving
+ * it a scanner (input) and an event handler (output), and then calling
+ * the Tokenizer::parse() method.`
+ *
+ * @param Scanner $scanner A scanner initialized with an input stream.
+ * @param EventHandler $eventHandler An event handler, initialized and ready to receive events.
+ * @param string $mode
+ */
+ public function __construct($scanner, $eventHandler, $mode = self::CONFORMANT_HTML)
+ {
+ $this->scanner = $scanner;
+ $this->events = $eventHandler;
+ $this->mode = $mode;
+ }
+
+ /**
+ * Begin parsing.
+ *
+ * This will begin scanning the document, tokenizing as it goes.
+ * Tokens are emitted into the event handler.
+ *
+ * Tokenizing will continue until the document is completely
+ * read. Errors are emitted into the event handler, but
+ * the parser will attempt to continue parsing until the
+ * entire input stream is read.
+ */
+ public function parse()
+ {
+ do {
+ $this->consumeData();
+ // FIXME: Add infinite loop protection.
+ } while ($this->carryOn);
+ }
+
+ /**
+ * Set the text mode for the character data reader.
+ *
+ * HTML5 defines three different modes for reading text:
+ * - Normal: Read until a tag is encountered.
+ * - RCDATA: Read until a tag is encountered, but skip a few otherwise-
+ * special characters.
+ * - Raw: Read until a special closing tag is encountered (viz. pre, script)
+ *
+ * This allows those modes to be set.
+ *
+ * Normally, setting is done by the event handler via a special return code on
+ * startTag(), but it can also be set manually using this function.
+ *
+ * @param int $textmode One of Elements::TEXT_*.
+ * @param string $untilTag The tag that should stop RAW or RCDATA mode. Normal mode does not
+ * use this indicator.
+ */
+ public function setTextMode($textmode, $untilTag = null)
+ {
+ $this->textMode = $textmode & (Elements::TEXT_RAW | Elements::TEXT_RCDATA);
+ $this->untilTag = $untilTag;
+ }
+
+ /**
+ * Consume a character and make a move.
+ * HTML5 8.2.4.1.
+ */
+ protected function consumeData()
+ {
+ $tok = $this->scanner->current();
+
+ if ('&' === $tok) {
+ // Character reference
+ $ref = $this->decodeCharacterReference();
+ $this->buffer($ref);
+
+ $tok = $this->scanner->current();
+ }
+
+ // Parse tag
+ if ('<' === $tok) {
+ // Any buffered text data can go out now.
+ $this->flushBuffer();
+
+ $tok = $this->scanner->next();
+
+ if ('!' === $tok) {
+ $this->markupDeclaration();
+ } elseif ('/' === $tok) {
+ $this->endTag();
+ } elseif ('?' === $tok) {
+ $this->processingInstruction();
+ } elseif (ctype_alpha($tok)) {
+ $this->tagName();
+ } else {
+ $this->parseError('Illegal tag opening');
+ // TODO is this necessary ?
+ $this->characterData();
+ }
+
+ $tok = $this->scanner->current();
+ }
+
+ if (false === $tok) {
+ // Handle end of document
+ $this->eof();
+ } else {
+ // Parse character
+ switch ($this->textMode) {
+ case Elements::TEXT_RAW:
+ $this->rawText($tok);
+ break;
+
+ case Elements::TEXT_RCDATA:
+ $this->rcdata($tok);
+ break;
+
+ default:
+ if ('<' === $tok || '&' === $tok) {
+ break;
+ }
+
+ // NULL character
+ if ("\00" === $tok) {
+ $this->parseError('Received null character.');
+
+ $this->text .= $tok;
+ $this->scanner->consume();
+
+ break;
+ }
+
+ $this->text .= $this->scanner->charsUntil("<&\0");
+ }
+ }
+
+ return $this->carryOn;
+ }
+
+ /**
+ * Parse anything that looks like character data.
+ *
+ * Different rules apply based on the current text mode.
+ *
+ * @see Elements::TEXT_RAW Elements::TEXT_RCDATA.
+ */
+ protected function characterData()
+ {
+ $tok = $this->scanner->current();
+ if (false === $tok) {
+ return false;
+ }
+ switch ($this->textMode) {
+ case Elements::TEXT_RAW:
+ return $this->rawText($tok);
+ case Elements::TEXT_RCDATA:
+ return $this->rcdata($tok);
+ default:
+ if ('<' === $tok || '&' === $tok) {
+ return false;
+ }
+
+ return $this->text($tok);
+ }
+ }
+
+ /**
+ * This buffers the current token as character data.
+ *
+ * @param string $tok The current token.
+ *
+ * @return bool
+ */
+ protected function text($tok)
+ {
+ // This should never happen...
+ if (false === $tok) {
+ return false;
+ }
+
+ // NULL character
+ if ("\00" === $tok) {
+ $this->parseError('Received null character.');
+ }
+
+ $this->buffer($tok);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ /**
+ * Read text in RAW mode.
+ *
+ * @param string $tok The current token.
+ *
+ * @return bool
+ */
+ protected function rawText($tok)
+ {
+ if (is_null($this->untilTag)) {
+ return $this->text($tok);
+ }
+
+ $sequence = '</' . $this->untilTag . '>';
+ $txt = $this->readUntilSequence($sequence);
+ $this->events->text($txt);
+ $this->setTextMode(0);
+
+ return $this->endTag();
+ }
+
+ /**
+ * Read text in RCDATA mode.
+ *
+ * @param string $tok The current token.
+ *
+ * @return bool
+ */
+ protected function rcdata($tok)
+ {
+ if (is_null($this->untilTag)) {
+ return $this->text($tok);
+ }
+
+ $sequence = '</' . $this->untilTag;
+ $txt = '';
+
+ $caseSensitive = !Elements::isHtml5Element($this->untilTag);
+ while (false !== $tok && !('<' == $tok && ($this->scanner->sequenceMatches($sequence, $caseSensitive)))) {
+ if ('&' == $tok) {
+ $txt .= $this->decodeCharacterReference();
+ $tok = $this->scanner->current();
+ } else {
+ $txt .= $tok;
+ $tok = $this->scanner->next();
+ }
+ }
+ $len = strlen($sequence);
+ $this->scanner->consume($len);
+ $len += $this->scanner->whitespace();
+ if ('>' !== $this->scanner->current()) {
+ $this->parseError('Unclosed RCDATA end tag');
+ }
+
+ $this->scanner->unconsume($len);
+ $this->events->text($txt);
+ $this->setTextMode(0);
+
+ return $this->endTag();
+ }
+
+ /**
+ * If the document is read, emit an EOF event.
+ */
+ protected function eof()
+ {
+ // fprintf(STDOUT, "EOF");
+ $this->flushBuffer();
+ $this->events->eof();
+ $this->carryOn = false;
+ }
+
+ /**
+ * Look for markup.
+ */
+ protected function markupDeclaration()
+ {
+ $tok = $this->scanner->next();
+
+ // Comment:
+ if ('-' == $tok && '-' == $this->scanner->peek()) {
+ $this->scanner->consume(2);
+
+ return $this->comment();
+ } elseif ('D' == $tok || 'd' == $tok) { // Doctype
+ return $this->doctype();
+ } elseif ('[' == $tok) { // CDATA section
+ return $this->cdataSection();
+ }
+
+ // FINISH
+ $this->parseError('Expected <!--, <![CDATA[, or <!DOCTYPE. Got <!%s', $tok);
+ $this->bogusComment('<!');
+
+ return true;
+ }
+
+ /**
+ * Consume an end tag. See section 8.2.4.9.
+ */
+ protected function endTag()
+ {
+ if ('/' != $this->scanner->current()) {
+ return false;
+ }
+ $tok = $this->scanner->next();
+
+ // a-zA-Z -> tagname
+ // > -> parse error
+ // EOF -> parse error
+ // -> parse error
+ if (!ctype_alpha($tok)) {
+ $this->parseError("Expected tag name, got '%s'", $tok);
+ if ("\0" == $tok || false === $tok) {
+ return false;
+ }
+
+ return $this->bogusComment('</');
+ }
+
+ $name = $this->scanner->charsUntil("\n\f \t>");
+ $name = self::CONFORMANT_XML === $this->mode ? $name : strtolower($name);
+ // Trash whitespace.
+ $this->scanner->whitespace();
+
+ $tok = $this->scanner->current();
+ if ('>' != $tok) {
+ $this->parseError("Expected >, got '%s'", $tok);
+ // We just trash stuff until we get to the next tag close.
+ $this->scanner->charsUntil('>');
+ }
+
+ $this->events->endTag($name);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ /**
+ * Consume a tag name and body. See section 8.2.4.10.
+ */
+ protected function tagName()
+ {
+ // We know this is at least one char.
+ $name = $this->scanner->charsWhile(':_-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
+ $name = self::CONFORMANT_XML === $this->mode ? $name : strtolower($name);
+ $attributes = array();
+ $selfClose = false;
+
+ // Handle attribute parse exceptions here so that we can
+ // react by trying to build a sensible parse tree.
+ try {
+ do {
+ $this->scanner->whitespace();
+ $this->attribute($attributes);
+ } while (!$this->isTagEnd($selfClose));
+ } catch (ParseError $e) {
+ $selfClose = false;
+ }
+
+ $mode = $this->events->startTag($name, $attributes, $selfClose);
+
+ if (is_int($mode)) {
+ $this->setTextMode($mode, $name);
+ }
+
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ /**
+ * Check if the scanner has reached the end of a tag.
+ */
+ protected function isTagEnd(&$selfClose)
+ {
+ $tok = $this->scanner->current();
+ if ('/' == $tok) {
+ $this->scanner->consume();
+ $this->scanner->whitespace();
+ $tok = $this->scanner->current();
+
+ if ('>' == $tok) {
+ $selfClose = true;
+
+ return true;
+ }
+ if (false === $tok) {
+ $this->parseError('Unexpected EOF inside of tag.');
+
+ return true;
+ }
+ // Basically, we skip the / token and go on.
+ // See 8.2.4.43.
+ $this->parseError("Unexpected '%s' inside of a tag.", $tok);
+
+ return false;
+ }
+
+ if ('>' == $tok) {
+ return true;
+ }
+ if (false === $tok) {
+ $this->parseError('Unexpected EOF inside of tag.');
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse attributes from inside of a tag.
+ *
+ * @param string[] $attributes
+ *
+ * @return bool
+ *
+ * @throws ParseError
+ */
+ protected function attribute(&$attributes)
+ {
+ $tok = $this->scanner->current();
+ if ('/' == $tok || '>' == $tok || false === $tok) {
+ return false;
+ }
+
+ if ('<' == $tok) {
+ $this->parseError("Unexpected '<' inside of attributes list.");
+ // Push the < back onto the stack.
+ $this->scanner->unconsume();
+ // Let the caller figure out how to handle this.
+ throw new ParseError('Start tag inside of attribute.');
+ }
+
+ $name = strtolower($this->scanner->charsUntil("/>=\n\f\t "));
+
+ if (0 == strlen($name)) {
+ $tok = $this->scanner->current();
+ $this->parseError('Expected an attribute name, got %s.', $tok);
+ // Really, only '=' can be the char here. Everything else gets absorbed
+ // under one rule or another.
+ $name = $tok;
+ $this->scanner->consume();
+ }
+
+ $isValidAttribute = true;
+ // Attribute names can contain most Unicode characters for HTML5.
+ // But method "DOMElement::setAttribute" is throwing exception
+ // because of it's own internal restriction so these have to be filtered.
+ // see issue #23: https://github.com/Masterminds/html5-php/issues/23
+ // and http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name
+ if (preg_match("/[\x1-\x2C\\/\x3B-\x40\x5B-\x5E\x60\x7B-\x7F]/u", $name)) {
+ $this->parseError('Unexpected characters in attribute name: %s', $name);
+ $isValidAttribute = false;
+ } // There is no limitation for 1st character in HTML5.
+ // But method "DOMElement::setAttribute" is throwing exception for the
+ // characters below so they have to be filtered.
+ // see issue #23: https://github.com/Masterminds/html5-php/issues/23
+ // and http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name
+ elseif (preg_match('/^[0-9.-]/u', $name)) {
+ $this->parseError('Unexpected character at the begining of attribute name: %s', $name);
+ $isValidAttribute = false;
+ }
+ // 8.1.2.3
+ $this->scanner->whitespace();
+
+ $val = $this->attributeValue();
+ if ($isValidAttribute) {
+ $attributes[$name] = $val;
+ }
+
+ return true;
+ }
+
+ /**
+ * Consume an attribute value. See section 8.2.4.37 and after.
+ *
+ * @return string|null
+ */
+ protected function attributeValue()
+ {
+ if ('=' != $this->scanner->current()) {
+ return null;
+ }
+ $this->scanner->consume();
+ // 8.1.2.3
+ $this->scanner->whitespace();
+
+ $tok = $this->scanner->current();
+ switch ($tok) {
+ case "\n":
+ case "\f":
+ case ' ':
+ case "\t":
+ // Whitespace here indicates an empty value.
+ return null;
+ case '"':
+ case "'":
+ $this->scanner->consume();
+
+ return $this->quotedAttributeValue($tok);
+ case '>':
+ // case '/': // 8.2.4.37 seems to allow foo=/ as a valid attr.
+ $this->parseError('Expected attribute value, got tag end.');
+
+ return null;
+ case '=':
+ case '`':
+ $this->parseError('Expecting quotes, got %s.', $tok);
+
+ return $this->unquotedAttributeValue();
+ default:
+ return $this->unquotedAttributeValue();
+ }
+ }
+
+ /**
+ * Get an attribute value string.
+ *
+ * @param string $quote IMPORTANT: This is a series of chars! Any one of which will be considered
+ * termination of an attribute's value. E.g. "\"'" will stop at either
+ * ' or ".
+ *
+ * @return string The attribute value.
+ */
+ protected function quotedAttributeValue($quote)
+ {
+ $stoplist = "\f" . $quote;
+ $val = '';
+
+ while (true) {
+ $tokens = $this->scanner->charsUntil($stoplist . '&');
+ if (false !== $tokens) {
+ $val .= $tokens;
+ } else {
+ break;
+ }
+
+ $tok = $this->scanner->current();
+ if ('&' == $tok) {
+ $val .= $this->decodeCharacterReference(true);
+ continue;
+ }
+ break;
+ }
+ $this->scanner->consume();
+
+ return $val;
+ }
+
+ protected function unquotedAttributeValue()
+ {
+ $val = '';
+ $tok = $this->scanner->current();
+ while (false !== $tok) {
+ switch ($tok) {
+ case "\n":
+ case "\f":
+ case ' ':
+ case "\t":
+ case '>':
+ break 2;
+
+ case '&':
+ $val .= $this->decodeCharacterReference(true);
+ $tok = $this->scanner->current();
+
+ break;
+
+ case "'":
+ case '"':
+ case '<':
+ case '=':
+ case '`':
+ $this->parseError('Unexpected chars in unquoted attribute value %s', $tok);
+ $val .= $tok;
+ $tok = $this->scanner->next();
+ break;
+
+ default:
+ $val .= $this->scanner->charsUntil("\t\n\f >&\"'<=`");
+
+ $tok = $this->scanner->current();
+ }
+ }
+
+ return $val;
+ }
+
+ /**
+ * Consume malformed markup as if it were a comment.
+ * 8.2.4.44.
+ *
+ * The spec requires that the ENTIRE tag-like thing be enclosed inside of
+ * the comment. So this will generate comments like:
+ *
+ * &lt;!--&lt/+foo&gt;--&gt;
+ *
+ * @param string $leading Prepend any leading characters. This essentially
+ * negates the need to backtrack, but it's sort of a hack.
+ *
+ * @return bool
+ */
+ protected function bogusComment($leading = '')
+ {
+ $comment = $leading;
+ $tokens = $this->scanner->charsUntil('>');
+ if (false !== $tokens) {
+ $comment .= $tokens;
+ }
+ $tok = $this->scanner->current();
+ if (false !== $tok) {
+ $comment .= $tok;
+ }
+
+ $this->flushBuffer();
+ $this->events->comment($comment);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ /**
+ * Read a comment.
+ * Expects the first tok to be inside of the comment.
+ *
+ * @return bool
+ */
+ protected function comment()
+ {
+ $tok = $this->scanner->current();
+ $comment = '';
+
+ // <!-->. Emit an empty comment because 8.2.4.46 says to.
+ if ('>' == $tok) {
+ // Parse error. Emit the comment token.
+ $this->parseError("Expected comment data, got '>'");
+ $this->events->comment('');
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ // Replace NULL with the replacement char.
+ if ("\0" == $tok) {
+ $tok = UTF8Utils::FFFD;
+ }
+ while (!$this->isCommentEnd()) {
+ $comment .= $tok;
+ $tok = $this->scanner->next();
+ }
+
+ $this->events->comment($comment);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ /**
+ * Check if the scanner has reached the end of a comment.
+ *
+ * @return bool
+ */
+ protected function isCommentEnd()
+ {
+ $tok = $this->scanner->current();
+
+ // EOF
+ if (false === $tok) {
+ // Hit the end.
+ $this->parseError('Unexpected EOF in a comment.');
+
+ return true;
+ }
+
+ // If it doesn't start with -, not the end.
+ if ('-' != $tok) {
+ return false;
+ }
+
+ // Advance one, and test for '->'
+ if ('-' == $this->scanner->next() && '>' == $this->scanner->peek()) {
+ $this->scanner->consume(); // Consume the last '>'
+ return true;
+ }
+ // Unread '-';
+ $this->scanner->unconsume(1);
+
+ return false;
+ }
+
+ /**
+ * Parse a DOCTYPE.
+ *
+ * Parse a DOCTYPE declaration. This method has strong bearing on whether or
+ * not Quirksmode is enabled on the event handler.
+ *
+ * @todo This method is a little long. Should probably refactor.
+ *
+ * @return bool
+ */
+ protected function doctype()
+ {
+ // Check that string is DOCTYPE.
+ if ($this->scanner->sequenceMatches('DOCTYPE', false)) {
+ $this->scanner->consume(7);
+ } else {
+ $chars = $this->scanner->charsWhile('DOCTYPEdoctype');
+ $this->parseError('Expected DOCTYPE, got %s', $chars);
+
+ return $this->bogusComment('<!' . $chars);
+ }
+
+ $this->scanner->whitespace();
+ $tok = $this->scanner->current();
+
+ // EOF: die.
+ if (false === $tok) {
+ $this->events->doctype('html5', EventHandler::DOCTYPE_NONE, '', true);
+ $this->eof();
+
+ return true;
+ }
+
+ // NULL char: convert.
+ if ("\0" === $tok) {
+ $this->parseError('Unexpected null character in DOCTYPE.');
+ }
+
+ $stop = " \n\f>";
+ $doctypeName = $this->scanner->charsUntil($stop);
+ // Lowercase ASCII, replace \0 with FFFD
+ $doctypeName = strtolower(strtr($doctypeName, "\0", UTF8Utils::FFFD));
+
+ $tok = $this->scanner->current();
+
+ // If false, emit a parse error, DOCTYPE, and return.
+ if (false === $tok) {
+ $this->parseError('Unexpected EOF in DOCTYPE declaration.');
+ $this->events->doctype($doctypeName, EventHandler::DOCTYPE_NONE, null, true);
+
+ return true;
+ }
+
+ // Short DOCTYPE, like <!DOCTYPE html>
+ if ('>' == $tok) {
+ // DOCTYPE without a name.
+ if (0 == strlen($doctypeName)) {
+ $this->parseError('Expected a DOCTYPE name. Got nothing.');
+ $this->events->doctype($doctypeName, 0, null, true);
+ $this->scanner->consume();
+
+ return true;
+ }
+ $this->events->doctype($doctypeName);
+ $this->scanner->consume();
+
+ return true;
+ }
+ $this->scanner->whitespace();
+
+ $pub = strtoupper($this->scanner->getAsciiAlpha());
+ $white = $this->scanner->whitespace();
+
+ // Get ID, and flag it as pub or system.
+ if (('PUBLIC' == $pub || 'SYSTEM' == $pub) && $white > 0) {
+ // Get the sys ID.
+ $type = 'PUBLIC' == $pub ? EventHandler::DOCTYPE_PUBLIC : EventHandler::DOCTYPE_SYSTEM;
+ $id = $this->quotedString("\0>");
+ if (false === $id) {
+ $this->events->doctype($doctypeName, $type, $pub, false);
+
+ return true;
+ }
+
+ // Premature EOF.
+ if (false === $this->scanner->current()) {
+ $this->parseError('Unexpected EOF in DOCTYPE');
+ $this->events->doctype($doctypeName, $type, $id, true);
+
+ return true;
+ }
+
+ // Well-formed complete DOCTYPE.
+ $this->scanner->whitespace();
+ if ('>' == $this->scanner->current()) {
+ $this->events->doctype($doctypeName, $type, $id, false);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ // If we get here, we have <!DOCTYPE foo PUBLIC "bar" SOME_JUNK
+ // Throw away the junk, parse error, quirks mode, return true.
+ $this->scanner->charsUntil('>');
+ $this->parseError('Malformed DOCTYPE.');
+ $this->events->doctype($doctypeName, $type, $id, true);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ // Else it's a bogus DOCTYPE.
+ // Consume to > and trash.
+ $this->scanner->charsUntil('>');
+
+ $this->parseError('Expected PUBLIC or SYSTEM. Got %s.', $pub);
+ $this->events->doctype($doctypeName, 0, null, true);
+ $this->scanner->consume();
+
+ return true;
+ }
+
+ /**
+ * Utility for reading a quoted string.
+ *
+ * @param string $stopchars Characters (in addition to a close-quote) that should stop the string.
+ * E.g. sometimes '>' is higher precedence than '"' or "'".
+ *
+ * @return mixed String if one is found (quotations omitted).
+ */
+ protected function quotedString($stopchars)
+ {
+ $tok = $this->scanner->current();
+ if ('"' == $tok || "'" == $tok) {
+ $this->scanner->consume();
+ $ret = $this->scanner->charsUntil($tok . $stopchars);
+ if ($this->scanner->current() == $tok) {
+ $this->scanner->consume();
+ } else {
+ // Parse error because no close quote.
+ $this->parseError('Expected %s, got %s', $tok, $this->scanner->current());
+ }
+
+ return $ret;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle a CDATA section.
+ *
+ * @return bool
+ */
+ protected function cdataSection()
+ {
+ $cdata = '';
+ $this->scanner->consume();
+
+ $chars = $this->scanner->charsWhile('CDAT');
+ if ('CDATA' != $chars || '[' != $this->scanner->current()) {
+ $this->parseError('Expected [CDATA[, got %s', $chars);
+
+ return $this->bogusComment('<![' . $chars);
+ }
+
+ $tok = $this->scanner->next();
+ do {
+ if (false === $tok) {
+ $this->parseError('Unexpected EOF inside CDATA.');
+ $this->bogusComment('<![CDATA[' . $cdata);
+
+ return true;
+ }
+ $cdata .= $tok;
+ $tok = $this->scanner->next();
+ } while (!$this->scanner->sequenceMatches(']]>'));
+
+ // Consume ]]>
+ $this->scanner->consume(3);
+
+ $this->events->cdata($cdata);
+
+ return true;
+ }
+
+ // ================================================================
+ // Non-HTML5
+ // ================================================================
+
+ /**
+ * Handle a processing instruction.
+ *
+ * XML processing instructions are supposed to be ignored in HTML5,
+ * treated as "bogus comments". However, since we're not a user
+ * agent, we allow them. We consume until ?> and then issue a
+ * EventListener::processingInstruction() event.
+ *
+ * @return bool
+ */
+ protected function processingInstruction()
+ {
+ if ('?' != $this->scanner->current()) {
+ return false;
+ }
+
+ $tok = $this->scanner->next();
+ $procName = $this->scanner->getAsciiAlpha();
+ $white = $this->scanner->whitespace();
+
+ // If not a PI, send to bogusComment.
+ if (0 == strlen($procName) || 0 == $white || false == $this->scanner->current()) {
+ $this->parseError("Expected processing instruction name, got $tok");
+ $this->bogusComment('<?' . $tok . $procName);
+
+ return true;
+ }
+
+ $data = '';
+ // As long as it's not the case that the next two chars are ? and >.
+ while (!('?' == $this->scanner->current() && '>' == $this->scanner->peek())) {
+ $data .= $this->scanner->current();
+
+ $tok = $this->scanner->next();
+ if (false === $tok) {
+ $this->parseError('Unexpected EOF in processing instruction.');
+ $this->events->processingInstruction($procName, $data);
+
+ return true;
+ }
+ }
+
+ $this->scanner->consume(2); // Consume the closing tag
+ $this->events->processingInstruction($procName, $data);
+
+ return true;
+ }
+
+ // ================================================================
+ // UTILITY FUNCTIONS
+ // ================================================================
+
+ /**
+ * Read from the input stream until we get to the desired sequene
+ * or hit the end of the input stream.
+ *
+ * @param string $sequence
+ *
+ * @return string
+ */
+ protected function readUntilSequence($sequence)
+ {
+ $buffer = '';
+
+ // Optimization for reading larger blocks faster.
+ $first = substr($sequence, 0, 1);
+ while (false !== $this->scanner->current()) {
+ $buffer .= $this->scanner->charsUntil($first);
+
+ // Stop as soon as we hit the stopping condition.
+ if ($this->scanner->sequenceMatches($sequence, false)) {
+ return $buffer;
+ }
+ $buffer .= $this->scanner->current();
+ $this->scanner->consume();
+ }
+
+ // If we get here, we hit the EOF.
+ $this->parseError('Unexpected EOF during text read.');
+
+ return $buffer;
+ }
+
+ /**
+ * Check if upcomming chars match the given sequence.
+ *
+ * This will read the stream for the $sequence. If it's
+ * found, this will return true. If not, return false.
+ * Since this unconsumes any chars it reads, the caller
+ * will still need to read the next sequence, even if
+ * this returns true.
+ *
+ * Example: $this->scanner->sequenceMatches('</script>') will
+ * see if the input stream is at the start of a
+ * '</script>' string.
+ *
+ * @param string $sequence
+ * @param bool $caseSensitive
+ *
+ * @return bool
+ */
+ protected function sequenceMatches($sequence, $caseSensitive = true)
+ {
+ @trigger_error(__METHOD__ . ' method is deprecated since version 2.4 and will be removed in 3.0. Use Scanner::sequenceMatches() instead.', E_USER_DEPRECATED);
+
+ return $this->scanner->sequenceMatches($sequence, $caseSensitive);
+ }
+
+ /**
+ * Send a TEXT event with the contents of the text buffer.
+ *
+ * This emits an EventHandler::text() event with the current contents of the
+ * temporary text buffer. (The buffer is used to group as much PCDATA
+ * as we can instead of emitting lots and lots of TEXT events.)
+ */
+ protected function flushBuffer()
+ {
+ if ('' === $this->text) {
+ return;
+ }
+ $this->events->text($this->text);
+ $this->text = '';
+ }
+
+ /**
+ * Add text to the temporary buffer.
+ *
+ * @see flushBuffer()
+ *
+ * @param string $str
+ */
+ protected function buffer($str)
+ {
+ $this->text .= $str;
+ }
+
+ /**
+ * Emit a parse error.
+ *
+ * A parse error always returns false because it never consumes any
+ * characters.
+ *
+ * @param string $msg
+ *
+ * @return string
+ */
+ protected function parseError($msg)
+ {
+ $args = func_get_args();
+
+ if (count($args) > 1) {
+ array_shift($args);
+ $msg = vsprintf($msg, $args);
+ }
+
+ $line = $this->scanner->currentLine();
+ $col = $this->scanner->columnOffset();
+ $this->events->parseError($msg, $line, $col);
+
+ return false;
+ }
+
+ /**
+ * Decode a character reference and return the string.
+ *
+ * If $inAttribute is set to true, a bare & will be returned as-is.
+ *
+ * @param bool $inAttribute Set to true if the text is inside of an attribute value.
+ * false otherwise.
+ *
+ * @return string
+ */
+ protected function decodeCharacterReference($inAttribute = false)
+ {
+ // Next char after &.
+ $tok = $this->scanner->next();
+ $start = $this->scanner->position();
+
+ if (false === $tok) {
+ return '&';
+ }
+
+ // These indicate not an entity. We return just
+ // the &.
+ if ("\t" === $tok || "\n" === $tok || "\f" === $tok || ' ' === $tok || '&' === $tok || '<' === $tok) {
+ // $this->scanner->next();
+ return '&';
+ }
+
+ // Numeric entity
+ if ('#' === $tok) {
+ $tok = $this->scanner->next();
+
+ if (false === $tok) {
+ $this->parseError('Expected &#DEC; &#HEX;, got EOF');
+ $this->scanner->unconsume(1);
+
+ return '&';
+ }
+
+ // Hexidecimal encoding.
+ // X[0-9a-fA-F]+;
+ // x[0-9a-fA-F]+;
+ if ('x' === $tok || 'X' === $tok) {
+ $tok = $this->scanner->next(); // Consume x
+
+ // Convert from hex code to char.
+ $hex = $this->scanner->getHex();
+ if (empty($hex)) {
+ $this->parseError('Expected &#xHEX;, got &#x%s', $tok);
+ // We unconsume because we don't know what parser rules might
+ // be in effect for the remaining chars. For example. '&#>'
+ // might result in a specific parsing rule inside of tag
+ // contexts, while not inside of pcdata context.
+ $this->scanner->unconsume(2);
+
+ return '&';
+ }
+ $entity = CharacterReference::lookupHex($hex);
+ } // Decimal encoding.
+ // [0-9]+;
+ else {
+ // Convert from decimal to char.
+ $numeric = $this->scanner->getNumeric();
+ if (false === $numeric) {
+ $this->parseError('Expected &#DIGITS;, got &#%s', $tok);
+ $this->scanner->unconsume(2);
+
+ return '&';
+ }
+ $entity = CharacterReference::lookupDecimal($numeric);
+ }
+ } elseif ('=' === $tok && $inAttribute) {
+ return '&';
+ } else { // String entity.
+ // Attempt to consume a string up to a ';'.
+ // [a-zA-Z0-9]+;
+ $cname = $this->scanner->getAsciiAlphaNum();
+ $entity = CharacterReference::lookupName($cname);
+
+ // When no entity is found provide the name of the unmatched string
+ // and continue on as the & is not part of an entity. The & will
+ // be converted to &amp; elsewhere.
+ if (null === $entity) {
+ if (!$inAttribute || '' === $cname) {
+ $this->parseError("No match in entity table for '%s'", $cname);
+ }
+ $this->scanner->unconsume($this->scanner->position() - $start);
+
+ return '&';
+ }
+ }
+
+ // The scanner has advanced the cursor for us.
+ $tok = $this->scanner->current();
+
+ // We have an entity. We're done here.
+ if (';' === $tok) {
+ $this->scanner->consume();
+
+ return $entity;
+ }
+
+ // Failing to match ; means unconsume the entire string.
+ $this->scanner->unconsume($this->scanner->position() - $start);
+
+ $this->parseError('Expected &ENTITY;, got &ENTITY%s (no trailing ;) ', $tok);
+
+ return '&';
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php b/vendor/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php
new file mode 100644
index 0000000..00d3951
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+/**
+ * Handles special-case rules for the DOM tree builder.
+ *
+ * Many tags have special rules that need to be accomodated on an
+ * individual basis. This class handles those rules.
+ *
+ * See section 8.1.2.4 of the spec.
+ *
+ * @todo - colgroup and col special behaviors
+ * - body and head special behaviors
+ */
+class TreeBuildingRules
+{
+ protected static $tags = array(
+ 'li' => 1,
+ 'dd' => 1,
+ 'dt' => 1,
+ 'rt' => 1,
+ 'rp' => 1,
+ 'tr' => 1,
+ 'th' => 1,
+ 'td' => 1,
+ 'thead' => 1,
+ 'tfoot' => 1,
+ 'tbody' => 1,
+ 'table' => 1,
+ 'optgroup' => 1,
+ 'option' => 1,
+ );
+
+ /**
+ * Returns true if the given tagname has special processing rules.
+ */
+ public function hasRules($tagname)
+ {
+ return isset(static::$tags[$tagname]);
+ }
+
+ /**
+ * Evaluate the rule for the current tag name.
+ *
+ * This may modify the existing DOM.
+ *
+ * @return \DOMElement The new Current DOM element.
+ */
+ public function evaluate($new, $current)
+ {
+ switch ($new->tagName) {
+ case 'li':
+ return $this->handleLI($new, $current);
+ case 'dt':
+ case 'dd':
+ return $this->handleDT($new, $current);
+ case 'rt':
+ case 'rp':
+ return $this->handleRT($new, $current);
+ case 'optgroup':
+ return $this->closeIfCurrentMatches($new, $current, array(
+ 'optgroup',
+ ));
+ case 'option':
+ return $this->closeIfCurrentMatches($new, $current, array(
+ 'option',
+ ));
+ case 'tr':
+ return $this->closeIfCurrentMatches($new, $current, array(
+ 'tr',
+ ));
+ case 'td':
+ case 'th':
+ return $this->closeIfCurrentMatches($new, $current, array(
+ 'th',
+ 'td',
+ ));
+ case 'tbody':
+ case 'thead':
+ case 'tfoot':
+ case 'table': // Spec isn't explicit about this, but it's necessary.
+
+ return $this->closeIfCurrentMatches($new, $current, array(
+ 'thead',
+ 'tfoot',
+ 'tbody',
+ ));
+ }
+
+ return $current;
+ }
+
+ protected function handleLI($ele, $current)
+ {
+ return $this->closeIfCurrentMatches($ele, $current, array(
+ 'li',
+ ));
+ }
+
+ protected function handleDT($ele, $current)
+ {
+ return $this->closeIfCurrentMatches($ele, $current, array(
+ 'dt',
+ 'dd',
+ ));
+ }
+
+ protected function handleRT($ele, $current)
+ {
+ return $this->closeIfCurrentMatches($ele, $current, array(
+ 'rt',
+ 'rp',
+ ));
+ }
+
+ protected function closeIfCurrentMatches($ele, $current, $match)
+ {
+ if (in_array($current->tagName, $match, true)) {
+ $current->parentNode->appendChild($ele);
+ } else {
+ $current->appendChild($ele);
+ }
+
+ return $ele;
+ }
+}
diff --git a/vendor/masterminds/html5/src/HTML5/Parser/UTF8Utils.php b/vendor/masterminds/html5/src/HTML5/Parser/UTF8Utils.php
new file mode 100644
index 0000000..f6a70bf
--- /dev/null
+++ b/vendor/masterminds/html5/src/HTML5/Parser/UTF8Utils.php
@@ -0,0 +1,183 @@
+<?php
+
+namespace Masterminds\HTML5\Parser;
+
+/*
+Portions based on code from html5lib files with the following copyright:
+
+Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+use Masterminds\HTML5\Exception;
+
+class UTF8Utils
+{
+ /**
+ * The Unicode replacement character.
+ */
+ const FFFD = "\xEF\xBF\xBD";
+
+ /**
+ * Count the number of characters in a string.
+ * UTF-8 aware. This will try (in order) iconv, MB, libxml, and finally a custom counter.
+ *
+ * @param string $string
+ *
+ * @return int
+ */
+ public static function countChars($string)
+ {
+ // Get the length for the string we need.
+ if (function_exists('mb_strlen')) {
+ return mb_strlen($string, 'utf-8');
+ }
+
+ if (function_exists('iconv_strlen')) {
+ return iconv_strlen($string, 'utf-8');
+ }
+
+ if (function_exists('utf8_decode')) {
+ // MPB: Will this work? Won't certain decodes lead to two chars
+ // extrapolated out of 2-byte chars?
+ return strlen(utf8_decode($string));
+ }
+
+ $count = count_chars($string);
+
+ // 0x80 = 0x7F - 0 + 1 (one added to get inclusive range)
+ // 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range)
+ return array_sum(array_slice($count, 0, 0x80)) + array_sum(array_slice($count, 0xC2, 0x33));
+ }
+
+ /**
+ * Convert data from the given encoding to UTF-8.
+ *
+ * This has not yet been tested with charactersets other than UTF-8.
+ * It should work with ISO-8859-1/-13 and standard Latin Win charsets.
+ *
+ * @param string $data The data to convert
+ * @param string $encoding A valid encoding. Examples: http://www.php.net/manual/en/mbstring.supported-encodings.php
+ *
+ * @return string
+ */
+ public static function convertToUTF8($data, $encoding = 'UTF-8')
+ {
+ /*
+ * From the HTML5 spec: Given an encoding, the bytes in the input stream must be converted
+ * to Unicode characters for the tokeniser, as described by the rules for that encoding,
+ * except that the leading U+FEFF BYTE ORDER MARK character, if any, must not be stripped
+ * by the encoding layer (it is stripped by the rule below). Bytes or sequences of bytes
+ * in the original byte stream that could not be converted to Unicode characters must be
+ * converted to U+FFFD REPLACEMENT CHARACTER code points.
+ */
+
+ // mb_convert_encoding is chosen over iconv because of a bug. The best
+ // details for the bug are on http://us1.php.net/manual/en/function.iconv.php#108643
+ // which contains links to the actual but reports as well as work around
+ // details.
+ if (function_exists('mb_convert_encoding')) {
+ // mb library has the following behaviors:
+ // - UTF-16 surrogates result in false.
+ // - Overlongs and outside Plane 16 result in empty strings.
+
+ // Before we run mb_convert_encoding we need to tell it what to do with
+ // characters it does not know. This could be different than the parent
+ // application executing this library so we store the value, change it
+ // to our needs, and then change it back when we are done. This feels
+ // a little excessive and it would be great if there was a better way.
+ $save = mb_substitute_character();
+ mb_substitute_character('none');
+ $data = mb_convert_encoding($data, 'UTF-8', $encoding);
+ mb_substitute_character($save);
+ }
+ // @todo Get iconv running in at least some environments if that is possible.
+ elseif (function_exists('iconv') && 'auto' !== $encoding) {
+ // fprintf(STDOUT, "iconv found\n");
+ // iconv has the following behaviors:
+ // - Overlong representations are ignored.
+ // - Beyond Plane 16 is replaced with a lower char.
+ // - Incomplete sequences generate a warning.
+ $data = @iconv($encoding, 'UTF-8//IGNORE', $data);
+ } else {
+ throw new Exception('Not implemented, please install mbstring or iconv');
+ }
+
+ /*
+ * One leading U+FEFF BYTE ORDER MARK character must be ignored if any are present.
+ */
+ if ("\xEF\xBB\xBF" === substr($data, 0, 3)) {
+ $data = substr($data, 3);
+ }
+
+ return $data;
+ }
+
+ /**
+ * Checks for Unicode code points that are not valid in a document.
+ *
+ * @param string $data A string to analyze
+ *
+ * @return array An array of (string) error messages produced by the scanning
+ */
+ public static function checkForIllegalCodepoints($data)
+ {
+ // Vestigal error handling.
+ $errors = array();
+
+ /*
+ * All U+0000 null characters in the input must be replaced by U+FFFD REPLACEMENT CHARACTERs.
+ * Any occurrences of such characters is a parse error.
+ */
+ for ($i = 0, $count = substr_count($data, "\0"); $i < $count; ++$i) {
+ $errors[] = 'null-character';
+ }
+
+ /*
+ * Any occurrences of any characters in the ranges U+0001 to U+0008, U+000B, U+000E to U+001F, U+007F
+ * to U+009F, U+D800 to U+DFFF , U+FDD0 to U+FDEF, and characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF,
+ * U+2FFFE, U+2FFFF, U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, U+6FFFF, U+7FFFE,
+ * U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF,
+ * U+DFFFE, U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and U+10FFFF are parse errors.
+ * (These are all control characters or permanently undefined Unicode characters.)
+ */
+ // Check PCRE is loaded.
+ $count = preg_match_all(
+ '/(?:
+ [\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F
+ |
+ \xC2[\x80-\x9F] # U+0080 to U+009F
+ |
+ \xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF
+ |
+ \xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF
+ |
+ \xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF
+ |
+ [\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16})
+ )/x', $data, $matches);
+ for ($i = 0; $i < $count; ++$i) {
+ $errors[] = 'invalid-codepoint';
+ }
+
+ return $errors;
+ }
+}