2 namespace Masterminds\HTML5\Parser;
4 use Masterminds\HTML5\Elements;
9 * The tokenizer's role is reading data from the scanner and gathering it into
10 * semantic units. From the tokenizer, data is emitted to an event handler,
11 * which may (for example) create a DOM tree.
13 * The HTML5 specification has a detailed explanation of tokenizing HTML5. We
14 * follow that specification to the maximum extent that we can. If you find
15 * a discrepancy that is not documented, please file a bug and/or submit a
18 * This tokenizer is implemented as a recursive descent parser.
20 * Within the API documentation, you may see references to the specific section
21 * of the HTML5 spec that the code attempts to reproduce. Example: 8.2.4.1.
22 * This refers to section 8.2.4.1 of the HTML5 CR specification.
24 * @see http://www.w3.org/TR/2012/CR-html5-20121217/
40 // When this goes to false, the parser stops.
41 protected $carryOn = true;
43 protected $textMode = 0; // TEXTMODE_NORMAL;
44 protected $untilTag = null;
46 const CONFORMANT_XML = 'xml';
47 const CONFORMANT_HTML = 'html';
48 protected $mode = self::CONFORMANT_HTML;
50 const WHITE = "\t\n\f ";
53 * Create a new tokenizer.
55 * Typically, parsing a document involves creating a new tokenizer, giving
56 * it a scanner (input) and an event handler (output), and then calling
57 * the Tokenizer::parse() method.`
59 * @param \Masterminds\HTML5\Parser\Scanner $scanner
60 * A scanner initialized with an input stream.
61 * @param \Masterminds\HTML5\Parser\EventHandler $eventHandler
62 * An event handler, initialized and ready to receive
66 public function __construct($scanner, $eventHandler, $mode = self::CONFORMANT_HTML)
68 $this->scanner = $scanner;
69 $this->events = $eventHandler;
76 * This will begin scanning the document, tokenizing as it goes.
77 * Tokens are emitted into the event handler.
79 * Tokenizing will continue until the document is completely
80 * read. Errors are emitted into the event handler, but
81 * the parser will attempt to continue parsing until the
82 * entire input stream is read.
84 public function parse()
88 $p = $this->scanner->position();
91 // FIXME: Add infinite loop protection.
92 } while ($this->carryOn);
96 * Set the text mode for the character data reader.
98 * HTML5 defines three different modes for reading text:
99 * - Normal: Read until a tag is encountered.
100 * - RCDATA: Read until a tag is encountered, but skip a few otherwise-
101 * special characters.
102 * - Raw: Read until a special closing tag is encountered (viz. pre, script)
104 * This allows those modes to be set.
106 * Normally, setting is done by the event handler via a special return code on
107 * startTag(), but it can also be set manually using this function.
109 * @param integer $textmode
110 * One of Elements::TEXT_*
111 * @param string $untilTag
112 * The tag that should stop RAW or RCDATA mode. Normal mode does not
113 * use this indicator.
115 public function setTextMode($textmode, $untilTag = null)
117 $this->textMode = $textmode & (Elements::TEXT_RAW | Elements::TEXT_RCDATA);
118 $this->untilTag = $untilTag;
122 * Consume a character and make a move.
125 protected function consumeData()
129 * $this->characterReference() || $this->tagOpen() || $this->eof() || $this->characterData();
131 $this->characterReference();
134 $this->characterData();
136 return $this->carryOn;
140 * Parse anything that looks like character data.
142 * Different rules apply based on the current text mode.
144 * @see Elements::TEXT_RAW Elements::TEXT_RCDATA.
146 protected function characterData()
148 if ($this->scanner->current() === false) {
151 switch ($this->textMode) {
152 case Elements::TEXT_RAW:
153 return $this->rawText();
154 case Elements::TEXT_RCDATA:
155 return $this->rcdata();
157 $tok = $this->scanner->current();
158 if (strspn($tok, "<&")) {
161 return $this->text();
166 * This buffers the current token as character data.
168 protected function text()
170 $tok = $this->scanner->current();
172 // This should never happen...
173 if ($tok === false) {
177 if ($tok === "\00") {
178 $this->parseError("Received null character.");
180 // fprintf(STDOUT, "Writing '%s'", $tok);
182 $this->scanner->next();
187 * Read text in RAW mode.
189 protected function rawText()
191 if (is_null($this->untilTag)) {
192 return $this->text();
194 $sequence = '</' . $this->untilTag . '>';
195 $txt = $this->readUntilSequence($sequence);
196 $this->events->text($txt);
197 $this->setTextMode(0);
198 return $this->endTag();
202 * Read text in RCDATA mode.
204 protected function rcdata()
206 if (is_null($this->untilTag)) {
207 return $this->text();
209 $sequence = '</' . $this->untilTag;
211 $tok = $this->scanner->current();
213 $caseSensitive = !Elements::isHtml5Element($this->untilTag);
214 while ($tok !== false && ! ($tok == '<' && ($this->sequenceMatches($sequence, $caseSensitive)))) {
216 $txt .= $this->decodeCharacterReference();
217 $tok = $this->scanner->current();
220 $tok = $this->scanner->next();
223 $len = strlen($sequence);
224 $this->scanner->consume($len);
225 $len += strlen($this->scanner->whitespace());
226 if ($this->scanner->current() !== '>') {
227 $this->parseError("Unclosed RCDATA end tag");
229 $this->scanner->unconsume($len);
230 $this->events->text($txt);
231 $this->setTextMode(0);
232 return $this->endTag();
236 * If the document is read, emit an EOF event.
238 protected function eof()
240 if ($this->scanner->current() === false) {
241 // fprintf(STDOUT, "EOF");
242 $this->flushBuffer();
243 $this->events->eof();
244 $this->carryOn = false;
251 * Handle character references (aka entities).
253 * This version is specific to PCDATA, as it buffers data into the
254 * text buffer. For a generic version, see decodeCharacterReference().
258 protected function characterReference()
260 $ref = $this->decodeCharacterReference();
261 if ($ref !== false) {
269 * Emit a tagStart event on encountering a tag.
273 protected function tagOpen()
275 if ($this->scanner->current() != '<') {
279 // Any buffered text data can go out now.
280 $this->flushBuffer();
282 $this->scanner->next();
284 return $this->markupDeclaration() || $this->endTag() || $this->processingInstruction() || $this->tagName() ||
285 /* This always returns false. */
286 $this->parseError("Illegal tag opening") || $this->characterData();
292 protected function markupDeclaration()
294 if ($this->scanner->current() != '!') {
298 $tok = $this->scanner->next();
301 if ($tok == '-' && $this->scanner->peek() == '-') {
302 $this->scanner->next(); // Consume the other '-'
303 $this->scanner->next(); // Next char.
304 return $this->comment();
307 elseif ($tok == 'D' || $tok == 'd') { // Doctype
308 return $this->doctype();
311 elseif ($tok == '[') { // CDATA section
312 return $this->cdataSection();
316 $this->parseError("Expected <!--, <![CDATA[, or <!DOCTYPE. Got <!%s", $tok);
317 $this->bogusComment('<!');
322 * Consume an end tag.
325 protected function endTag()
327 if ($this->scanner->current() != '/') {
330 $tok = $this->scanner->next();
334 // EOF -> parse error
336 if (! ctype_alpha($tok)) {
337 $this->parseError("Expected tag name, got '%s'", $tok);
338 if ($tok == "\0" || $tok === false) {
341 return $this->bogusComment('</');
344 $name = $this->scanner->charsUntil("\n\f \t>");
345 $name = $this->mode === self::CONFORMANT_XML ? $name: strtolower($name);
347 $this->scanner->whitespace();
349 if ($this->scanner->current() != '>') {
350 $this->parseError("Expected >, got '%s'", $this->scanner->current());
351 // We just trash stuff until we get to the next tag close.
352 $this->scanner->charsUntil('>');
355 $this->events->endTag($name);
356 $this->scanner->next();
361 * Consume a tag name and body.
364 protected function tagName()
366 $tok = $this->scanner->current();
367 if (! ctype_alpha($tok)) {
371 // We know this is at least one char.
372 $name = $this->scanner->charsWhile(":_-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
373 $name = $this->mode === self::CONFORMANT_XML ? $name : strtolower($name);
374 $attributes = array();
377 // Handle attribute parse exceptions here so that we can
378 // react by trying to build a sensible parse tree.
381 $this->scanner->whitespace();
382 $this->attribute($attributes);
383 } while (! $this->isTagEnd($selfClose));
384 } catch (ParseError $e) {
388 $mode = $this->events->startTag($name, $attributes, $selfClose);
389 // Should we do this? What does this buy that selfClose doesn't?
391 $this->events->endTag($name);
392 } elseif (is_int($mode)) {
393 // fprintf(STDOUT, "Event response says move into mode %d for tag %s", $mode, $name);
394 $this->setTextMode($mode, $name);
397 $this->scanner->next();
403 * Check if the scanner has reached the end of a tag.
405 protected function isTagEnd(&$selfClose)
407 $tok = $this->scanner->current();
409 $this->scanner->next();
410 $this->scanner->whitespace();
411 if ($this->scanner->current() == '>') {
415 if ($this->scanner->current() === false) {
416 $this->parseError("Unexpected EOF inside of tag.");
419 // Basically, we skip the / token and go on.
421 $this->parseError("Unexpected '%s' inside of a tag.", $this->scanner->current());
425 if ($this->scanner->current() == '>') {
428 if ($this->scanner->current() === false) {
429 $this->parseError("Unexpected EOF inside of tag.");
437 * Parse attributes from inside of a tag.
439 protected function attribute(&$attributes)
441 $tok = $this->scanner->current();
442 if ($tok == '/' || $tok == '>' || $tok === false) {
447 $this->parseError("Unexepcted '<' inside of attributes list.");
448 // Push the < back onto the stack.
449 $this->scanner->unconsume();
450 // Let the caller figure out how to handle this.
451 throw new ParseError("Start tag inside of attribute.");
454 $name = strtolower($this->scanner->charsUntil("/>=\n\f\t "));
456 if (strlen($name) == 0) {
457 $this->parseError("Expected an attribute name, got %s.", $this->scanner->current());
458 // Really, only '=' can be the char here. Everything else gets absorbed
459 // under one rule or another.
460 $name = $this->scanner->current();
461 $this->scanner->next();
464 $isValidAttribute = true;
465 // Attribute names can contain most Unicode characters for HTML5.
466 // But method "DOMElement::setAttribute" is throwing exception
467 // because of it's own internal restriction so these have to be filtered.
468 // see issue #23: https://github.com/Masterminds/html5-php/issues/23
469 // and http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name
470 if (preg_match("/[\x1-\x2C\\/\x3B-\x40\x5B-\x5E\x60\x7B-\x7F]/u", $name)) {
471 $this->parseError("Unexpected characters in attribute name: %s", $name);
472 $isValidAttribute = false;
473 } // There is no limitation for 1st character in HTML5.
474 // But method "DOMElement::setAttribute" is throwing exception for the
475 // characters below so they have to be filtered.
476 // see issue #23: https://github.com/Masterminds/html5-php/issues/23
477 // and http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name
479 if (preg_match("/^[0-9.-]/u", $name)) {
480 $this->parseError("Unexpected character at the begining of attribute name: %s", $name);
481 $isValidAttribute = false;
484 $this->scanner->whitespace();
486 $val = $this->attributeValue();
487 if ($isValidAttribute) {
488 $attributes[$name] = $val;
494 * Consume an attribute value.
495 * 8.2.4.37 and after.
497 protected function attributeValue()
499 if ($this->scanner->current() != '=') {
502 $this->scanner->next();
504 $this->scanner->whitespace();
506 $tok = $this->scanner->current();
512 // Whitespace here indicates an empty value.
516 $this->scanner->next();
517 return $this->quotedAttributeValue($tok);
519 // case '/': // 8.2.4.37 seems to allow foo=/ as a valid attr.
520 $this->parseError("Expected attribute value, got tag end.");
524 $this->parseError("Expecting quotes, got %s.", $tok);
525 return $this->unquotedAttributeValue();
527 return $this->unquotedAttributeValue();
532 * Get an attribute value string.
534 * @param string $quote
535 * IMPORTANT: This is a series of chars! Any one of which will be considered
536 * termination of an attribute's value. E.g. "\"'" will stop at either
538 * @return string The attribute value.
540 protected function quotedAttributeValue($quote)
542 $stoplist = "\f" . $quote;
544 $tok = $this->scanner->current();
545 while (strspn($tok, $stoplist) == 0 && $tok !== false) {
547 $val .= $this->decodeCharacterReference(true);
548 $tok = $this->scanner->current();
551 $tok = $this->scanner->next();
554 $this->scanner->next();
558 protected function unquotedAttributeValue()
560 $stoplist = "\t\n\f >";
562 $tok = $this->scanner->current();
563 while (strspn($tok, $stoplist) == 0 && $tok !== false) {
565 $val .= $this->decodeCharacterReference(true);
566 $tok = $this->scanner->current();
568 if (strspn($tok, "\"'<=`") > 0) {
569 $this->parseError("Unexpected chars in unquoted attribute value %s", $tok);
572 $tok = $this->scanner->next();
579 * Consume malformed markup as if it were a comment.
582 * The spec requires that the ENTIRE tag-like thing be enclosed inside of
583 * the comment. So this will generate comments like:
585 * <!--</+foo>-->
587 * @param string $leading
588 * Prepend any leading characters. This essentially
589 * negates the need to backtrack, but it's sort of
592 protected function bogusComment($leading = '')
595 // TODO: This can be done more efficiently when the
596 // scanner exposes a readUntil() method.
598 $tok = $this->scanner->current();
601 $tok = $this->scanner->next();
602 } while ($tok !== false && $tok != '>');
604 $this->flushBuffer();
605 $this->events->comment($comment . $tok);
606 $this->scanner->next();
614 * Expects the first tok to be inside of the comment.
616 protected function comment()
618 $tok = $this->scanner->current();
621 // <!-->. Emit an empty comment because 8.2.4.46 says to.
623 // Parse error. Emit the comment token.
624 $this->parseError("Expected comment data, got '>'");
625 $this->events->comment('');
626 $this->scanner->next();
630 // Replace NULL with the replacement char.
632 $tok = UTF8Utils::FFFD;
634 while (! $this->isCommentEnd()) {
636 $tok = $this->scanner->next();
639 $this->events->comment($comment);
640 $this->scanner->next();
645 * Check if the scanner has reached the end of a comment.
647 protected function isCommentEnd()
650 if ($this->scanner->current() === false) {
652 $this->parseError("Unexpected EOF in a comment.");
656 // If it doesn't start with -, not the end.
657 if ($this->scanner->current() != '-') {
661 // Advance one, and test for '->'
662 if ($this->scanner->next() == '-' && $this->scanner->peek() == '>') {
663 $this->scanner->next(); // Consume the last '>'
667 $this->scanner->unconsume(1);
674 * Parse a DOCTYPE declaration. This method has strong bearing on whether or
675 * not Quirksmode is enabled on the event handler.
677 * @todo This method is a little long. Should probably refactor.
679 protected function doctype()
681 if (strcasecmp($this->scanner->current(), 'D')) {
684 // Check that string is DOCTYPE.
685 $chars = $this->scanner->charsWhile("DOCTYPEdoctype");
686 if (strcasecmp($chars, 'DOCTYPE')) {
687 $this->parseError('Expected DOCTYPE, got %s', $chars);
688 return $this->bogusComment('<!' . $chars);
691 $this->scanner->whitespace();
692 $tok = $this->scanner->current();
695 if ($tok === false) {
696 $this->events->doctype('html5', EventHandler::DOCTYPE_NONE, '', true);
702 // NULL char: convert.
704 $this->parseError("Unexpected null character in DOCTYPE.");
705 $doctypeName .= UTF8::FFFD;
706 $tok = $this->scanner->next();
710 $doctypeName = $this->scanner->charsUntil($stop);
711 // Lowercase ASCII, replace \0 with FFFD
712 $doctypeName = strtolower(strtr($doctypeName, "\0", UTF8Utils::FFFD));
714 $tok = $this->scanner->current();
716 // If false, emit a parse error, DOCTYPE, and return.
717 if ($tok === false) {
718 $this->parseError('Unexpected EOF in DOCTYPE declaration.');
719 $this->events->doctype($doctypeName, EventHandler::DOCTYPE_NONE, null, true);
723 // Short DOCTYPE, like <!DOCTYPE html>
725 // DOCTYPE without a name.
726 if (strlen($doctypeName) == 0) {
727 $this->parseError("Expected a DOCTYPE name. Got nothing.");
728 $this->events->doctype($doctypeName, 0, null, true);
729 $this->scanner->next();
732 $this->events->doctype($doctypeName);
733 $this->scanner->next();
736 $this->scanner->whitespace();
738 $pub = strtoupper($this->scanner->getAsciiAlpha());
739 $white = strlen($this->scanner->whitespace());
740 $tok = $this->scanner->current();
742 // Get ID, and flag it as pub or system.
743 if (($pub == 'PUBLIC' || $pub == 'SYSTEM') && $white > 0) {
745 $type = $pub == 'PUBLIC' ? EventHandler::DOCTYPE_PUBLIC : EventHandler::DOCTYPE_SYSTEM;
746 $id = $this->quotedString("\0>");
748 $this->events->doctype($doctypeName, $type, $pub, false);
753 if ($this->scanner->current() === false) {
754 $this->parseError("Unexpected EOF in DOCTYPE");
755 $this->events->doctype($doctypeName, $type, $id, true);
759 // Well-formed complete DOCTYPE.
760 $this->scanner->whitespace();
761 if ($this->scanner->current() == '>') {
762 $this->events->doctype($doctypeName, $type, $id, false);
763 $this->scanner->next();
767 // If we get here, we have <!DOCTYPE foo PUBLIC "bar" SOME_JUNK
768 // Throw away the junk, parse error, quirks mode, return true.
769 $this->scanner->charsUntil(">");
770 $this->parseError("Malformed DOCTYPE.");
771 $this->events->doctype($doctypeName, $type, $id, true);
772 $this->scanner->next();
776 // Else it's a bogus DOCTYPE.
777 // Consume to > and trash.
778 $this->scanner->charsUntil('>');
780 $this->parseError("Expected PUBLIC or SYSTEM. Got %s.", $pub);
781 $this->events->doctype($doctypeName, 0, null, true);
782 $this->scanner->next();
787 * Utility for reading a quoted string.
789 * @param string $stopchars
790 * Characters (in addition to a close-quote) that should stop the string.
791 * E.g. sometimes '>' is higher precedence than '"' or "'".
792 * @return mixed String if one is found (quotations omitted)
794 protected function quotedString($stopchars)
796 $tok = $this->scanner->current();
797 if ($tok == '"' || $tok == "'") {
798 $this->scanner->next();
799 $ret = $this->scanner->charsUntil($tok . $stopchars);
800 if ($this->scanner->current() == $tok) {
801 $this->scanner->next();
803 // Parse error because no close quote.
804 $this->parseError("Expected %s, got %s", $tok, $this->scanner->current());
812 * Handle a CDATA section.
814 protected function cdataSection()
816 if ($this->scanner->current() != '[') {
820 $this->scanner->next();
822 $chars = $this->scanner->charsWhile('CDAT');
823 if ($chars != 'CDATA' || $this->scanner->current() != '[') {
824 $this->parseError('Expected [CDATA[, got %s', $chars);
825 return $this->bogusComment('<![' . $chars);
828 $tok = $this->scanner->next();
830 if ($tok === false) {
831 $this->parseError('Unexpected EOF inside CDATA.');
832 $this->bogusComment('<![CDATA[' . $cdata);
836 $tok = $this->scanner->next();
837 } while (! $this->sequenceMatches(']]>'));
840 $this->scanner->consume(3);
842 $this->events->cdata($cdata);
846 // ================================================================
848 // ================================================================
850 * Handle a processing instruction.
852 * XML processing instructions are supposed to be ignored in HTML5,
853 * treated as "bogus comments". However, since we're not a user
854 * agent, we allow them. We consume until ?> and then issue a
855 * EventListener::processingInstruction() event.
857 protected function processingInstruction()
859 if ($this->scanner->current() != '?') {
863 $tok = $this->scanner->next();
864 $procName = $this->scanner->getAsciiAlpha();
865 $white = strlen($this->scanner->whitespace());
867 // If not a PI, send to bogusComment.
868 if (strlen($procName) == 0 || $white == 0 || $this->scanner->current() == false) {
869 $this->parseError("Expected processing instruction name, got $tok");
870 $this->bogusComment('<?' . $tok . $procName);
875 // As long as it's not the case that the next two chars are ? and >.
876 while (! ($this->scanner->current() == '?' && $this->scanner->peek() == '>')) {
877 $data .= $this->scanner->current();
879 $tok = $this->scanner->next();
880 if ($tok === false) {
881 $this->parseError("Unexpected EOF in processing instruction.");
882 $this->events->processingInstruction($procName, $data);
887 $this->scanner->next(); // >
888 $this->scanner->next(); // Next token.
889 $this->events->processingInstruction($procName, $data);
893 // ================================================================
895 // ================================================================
898 * Read from the input stream until we get to the desired sequene
899 * or hit the end of the input stream.
901 protected function readUntilSequence($sequence)
905 // Optimization for reading larger blocks faster.
906 $first = substr($sequence, 0, 1);
907 while ($this->scanner->current() !== false) {
908 $buffer .= $this->scanner->charsUntil($first);
910 // Stop as soon as we hit the stopping condition.
911 if ($this->sequenceMatches($sequence, false)) {
914 $buffer .= $this->scanner->current();
915 $this->scanner->next();
918 // If we get here, we hit the EOF.
919 $this->parseError("Unexpected EOF during text read.");
924 * Check if upcomming chars match the given sequence.
926 * This will read the stream for the $sequence. If it's
927 * found, this will return true. If not, return false.
928 * Since this unconsumes any chars it reads, the caller
929 * will still need to read the next sequence, even if
932 * Example: $this->sequenceMatches('</script>') will
933 * see if the input stream is at the start of a
934 * '</script>' string.
936 protected function sequenceMatches($sequence, $caseSensitive = true)
938 $len = strlen($sequence);
940 for ($i = 0; $i < $len; ++ $i) {
941 $buffer .= $this->scanner->current();
943 // EOF. Rewind and let the caller handle it.
944 if ($this->scanner->current() === false) {
945 $this->scanner->unconsume($i);
948 $this->scanner->next();
951 $this->scanner->unconsume($len);
952 return $caseSensitive ? $buffer == $sequence : strcasecmp($buffer, $sequence) === 0;
956 * Send a TEXT event with the contents of the text buffer.
958 * This emits an EventHandler::text() event with the current contents of the
959 * temporary text buffer. (The buffer is used to group as much PCDATA
960 * as we can instead of emitting lots and lots of TEXT events.)
962 protected function flushBuffer()
964 if ($this->text === '') {
967 $this->events->text($this->text);
972 * Add text to the temporary buffer.
976 protected function buffer($str)
982 * Emit a parse error.
984 * A parse error always returns false because it never consumes any
987 protected function parseError($msg)
989 $args = func_get_args();
991 if (count($args) > 1) {
993 $msg = vsprintf($msg, $args);
996 $line = $this->scanner->currentLine();
997 $col = $this->scanner->columnOffset();
998 $this->events->parseError($msg, $line, $col);
1003 * Decode a character reference and return the string.
1005 * Returns false if the entity could not be found. If $inAttribute is set
1006 * to true, a bare & will be returned as-is.
1008 * @param boolean $inAttribute
1009 * Set to true if the text is inside of an attribute value.
1012 protected function decodeCharacterReference($inAttribute = false)
1015 // If it fails this, it's definitely not an entity.
1016 if ($this->scanner->current() != '&') {
1020 // Next char after &.
1021 $tok = $this->scanner->next();
1023 $start = $this->scanner->position();
1025 if ($tok == false) {
1029 // These indicate not an entity. We return just
1031 if (strspn($tok, static::WHITE . "&<") == 1) {
1032 // $this->scanner->next();
1038 $tok = $this->scanner->next();
1040 // Hexidecimal encoding.
1043 if ($tok == 'x' || $tok == 'X') {
1044 $tok = $this->scanner->next(); // Consume x
1046 // Convert from hex code to char.
1047 $hex = $this->scanner->getHex();
1049 $this->parseError("Expected &#xHEX;, got &#x%s", $tok);
1050 // We unconsume because we don't know what parser rules might
1051 // be in effect for the remaining chars. For example. '&#>'
1052 // might result in a specific parsing rule inside of tag
1053 // contexts, while not inside of pcdata context.
1054 $this->scanner->unconsume(2);
1057 $entity = CharacterReference::lookupHex($hex);
1058 } // Decimal encoding.
1061 // Convert from decimal to char.
1062 $numeric = $this->scanner->getNumeric();
1063 if ($numeric === false) {
1064 $this->parseError("Expected &#DIGITS;, got &#%s", $tok);
1065 $this->scanner->unconsume(2);
1068 $entity = CharacterReference::lookupDecimal($numeric);
1072 // Attempt to consume a string up to a ';'.
1074 $cname = $this->scanner->getAsciiAlpha();
1075 $entity = CharacterReference::lookupName($cname);
1077 // When no entity is found provide the name of the unmatched string
1078 // and continue on as the & is not part of an entity. The & will
1079 // be converted to & elsewhere.
1080 if ($entity == null) {
1081 $this->parseError("No match in entity table for '%s'", $cname);
1082 $this->scanner->unconsume($this->scanner->position() - $start);
1087 // The scanner has advanced the cursor for us.
1088 $tok = $this->scanner->current();
1090 // We have an entity. We're done here.
1092 $this->scanner->next();
1096 // If in an attribute, then failing to match ; means unconsume the
1097 // entire string. Otherwise, failure to match is an error.
1099 $this->scanner->unconsume($this->scanner->position() - $start);
1103 $this->parseError("Expected &ENTITY;, got &ENTITY%s (no trailing ;) ", $tok);
1104 return '&' . $entity;