02b2affdd5c58c93c97747d0aab1b905dfa17edf
[yaffs-website] / vendor / masterminds / html5 / src / HTML5 / Parser / Tokenizer.php
1 <?php
2 namespace Masterminds\HTML5\Parser;
3
4 use Masterminds\HTML5\Elements;
5
6 /**
7  * The HTML5 tokenizer.
8  *
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.
12  *
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
16  * patch.
17  *
18  * This tokenizer is implemented as a recursive descent parser.
19  *
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.
23  *
24  * @see http://www.w3.org/TR/2012/CR-html5-20121217/
25  */
26 class Tokenizer
27 {
28
29     protected $scanner;
30
31     protected $events;
32
33     protected $tok;
34
35     /**
36      * Buffer for text.
37      */
38     protected $text = '';
39
40     // When this goes to false, the parser stops.
41     protected $carryOn = true;
42
43     protected $textMode = 0; // TEXTMODE_NORMAL;
44     protected $untilTag = null;
45
46     const CONFORMANT_XML = 'xml';
47     const CONFORMANT_HTML = 'html';
48     protected $mode = self::CONFORMANT_HTML;
49
50     const WHITE = "\t\n\f ";
51
52     /**
53      * Create a new tokenizer.
54      *
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.`
58      *
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
63      *            events.
64      * @param string $mode
65      */
66     public function __construct($scanner, $eventHandler, $mode = self::CONFORMANT_HTML)
67     {
68         $this->scanner = $scanner;
69         $this->events = $eventHandler;
70         $this->mode = $mode;
71     }
72
73     /**
74      * Begin parsing.
75      *
76      * This will begin scanning the document, tokenizing as it goes.
77      * Tokens are emitted into the event handler.
78      *
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.
83      */
84     public function parse()
85     {
86         $p = 0;
87         do {
88             $p = $this->scanner->position();
89             $this->consumeData();
90
91             // FIXME: Add infinite loop protection.
92         } while ($this->carryOn);
93     }
94
95     /**
96      * Set the text mode for the character data reader.
97      *
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)
103      *
104      * This allows those modes to be set.
105      *
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.
108      *
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.
114      */
115     public function setTextMode($textmode, $untilTag = null)
116     {
117         $this->textMode = $textmode & (Elements::TEXT_RAW | Elements::TEXT_RCDATA);
118         $this->untilTag = $untilTag;
119     }
120
121     /**
122      * Consume a character and make a move.
123      * HTML5 8.2.4.1
124      */
125     protected function consumeData()
126     {
127         // Character Ref
128         /*
129          * $this->characterReference() || $this->tagOpen() || $this->eof() || $this->characterData();
130          */
131         $this->characterReference();
132         $this->tagOpen();
133         $this->eof();
134         $this->characterData();
135
136         return $this->carryOn;
137     }
138
139     /**
140      * Parse anything that looks like character data.
141      *
142      * Different rules apply based on the current text mode.
143      *
144      * @see Elements::TEXT_RAW Elements::TEXT_RCDATA.
145      */
146     protected function characterData()
147     {
148         if ($this->scanner->current() === false) {
149             return false;
150         }
151         switch ($this->textMode) {
152             case Elements::TEXT_RAW:
153                 return $this->rawText();
154             case Elements::TEXT_RCDATA:
155                 return $this->rcdata();
156             default:
157                 $tok = $this->scanner->current();
158                 if (strspn($tok, "<&")) {
159                     return false;
160                 }
161                 return $this->text();
162         }
163     }
164
165     /**
166      * This buffers the current token as character data.
167      */
168     protected function text()
169     {
170         $tok = $this->scanner->current();
171
172         // This should never happen...
173         if ($tok === false) {
174             return false;
175         }
176         // Null
177         if ($tok === "\00") {
178             $this->parseError("Received null character.");
179         }
180         // fprintf(STDOUT, "Writing '%s'", $tok);
181         $this->buffer($tok);
182         $this->scanner->next();
183         return true;
184     }
185
186     /**
187      * Read text in RAW mode.
188      */
189     protected function rawText()
190     {
191         if (is_null($this->untilTag)) {
192             return $this->text();
193         }
194         $sequence = '</' . $this->untilTag . '>';
195         $txt = $this->readUntilSequence($sequence);
196         $this->events->text($txt);
197         $this->setTextMode(0);
198         return $this->endTag();
199     }
200
201     /**
202      * Read text in RCDATA mode.
203      */
204     protected function rcdata()
205     {
206         if (is_null($this->untilTag)) {
207             return $this->text();
208         }
209         $sequence = '</' . $this->untilTag;
210         $txt = '';
211         $tok = $this->scanner->current();
212
213         $caseSensitive = !Elements::isHtml5Element($this->untilTag);
214         while ($tok !== false && ! ($tok == '<' && ($this->sequenceMatches($sequence, $caseSensitive)))) {
215             if ($tok == '&') {
216                 $txt .= $this->decodeCharacterReference();
217                 $tok = $this->scanner->current();
218             } else {
219                 $txt .= $tok;
220                 $tok = $this->scanner->next();
221             }
222         }
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");
228         }
229         $this->scanner->unconsume($len);
230         $this->events->text($txt);
231         $this->setTextMode(0);
232         return $this->endTag();
233     }
234
235     /**
236      * If the document is read, emit an EOF event.
237      */
238     protected function eof()
239     {
240         if ($this->scanner->current() === false) {
241             // fprintf(STDOUT, "EOF");
242             $this->flushBuffer();
243             $this->events->eof();
244             $this->carryOn = false;
245             return true;
246         }
247         return false;
248     }
249
250     /**
251      * Handle character references (aka entities).
252      *
253      * This version is specific to PCDATA, as it buffers data into the
254      * text buffer. For a generic version, see decodeCharacterReference().
255      *
256      * HTML5 8.2.4.2
257      */
258     protected function characterReference()
259     {
260         $ref = $this->decodeCharacterReference();
261         if ($ref !== false) {
262             $this->buffer($ref);
263             return true;
264         }
265         return false;
266     }
267
268     /**
269      * Emit a tagStart event on encountering a tag.
270      *
271      * 8.2.4.8
272      */
273     protected function tagOpen()
274     {
275         if ($this->scanner->current() != '<') {
276             return false;
277         }
278
279         // Any buffered text data can go out now.
280         $this->flushBuffer();
281
282         $this->scanner->next();
283
284         return $this->markupDeclaration() || $this->endTag() || $this->processingInstruction() || $this->tagName() ||
285           /*  This always returns false. */
286           $this->parseError("Illegal tag opening") || $this->characterData();
287     }
288
289     /**
290      * Look for markup.
291      */
292     protected function markupDeclaration()
293     {
294         if ($this->scanner->current() != '!') {
295             return false;
296         }
297
298         $tok = $this->scanner->next();
299
300         // Comment:
301         if ($tok == '-' && $this->scanner->peek() == '-') {
302             $this->scanner->next(); // Consume the other '-'
303             $this->scanner->next(); // Next char.
304             return $this->comment();
305         }
306
307         elseif ($tok == 'D' || $tok == 'd') { // Doctype
308             return $this->doctype();
309         }
310
311         elseif ($tok == '[') { // CDATA section
312             return $this->cdataSection();
313         }
314
315         // FINISH
316         $this->parseError("Expected <!--, <![CDATA[, or <!DOCTYPE. Got <!%s", $tok);
317         $this->bogusComment('<!');
318         return true;
319     }
320
321     /**
322      * Consume an end tag.
323      * 8.2.4.9
324      */
325     protected function endTag()
326     {
327         if ($this->scanner->current() != '/') {
328             return false;
329         }
330         $tok = $this->scanner->next();
331
332         // a-zA-Z -> tagname
333         // > -> parse error
334         // EOF -> parse error
335         // -> parse error
336         if (! ctype_alpha($tok)) {
337             $this->parseError("Expected tag name, got '%s'", $tok);
338             if ($tok == "\0" || $tok === false) {
339                 return false;
340             }
341             return $this->bogusComment('</');
342         }
343
344         $name = $this->scanner->charsUntil("\n\f \t>");
345         $name = $this->mode === self::CONFORMANT_XML ? $name: strtolower($name);
346         // Trash whitespace.
347         $this->scanner->whitespace();
348
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('>');
353         }
354
355         $this->events->endTag($name);
356         $this->scanner->next();
357         return true;
358     }
359
360     /**
361      * Consume a tag name and body.
362      * 8.2.4.10
363      */
364     protected function tagName()
365     {
366         $tok = $this->scanner->current();
367         if (! ctype_alpha($tok)) {
368             return false;
369         }
370
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();
375         $selfClose = false;
376
377         // Handle attribute parse exceptions here so that we can
378         // react by trying to build a sensible parse tree.
379         try {
380             do {
381                 $this->scanner->whitespace();
382                 $this->attribute($attributes);
383             } while (! $this->isTagEnd($selfClose));
384         } catch (ParseError $e) {
385             $selfClose = false;
386         }
387
388         $mode = $this->events->startTag($name, $attributes, $selfClose);
389         // Should we do this? What does this buy that selfClose doesn't?
390         if ($selfClose) {
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);
395         }
396
397         $this->scanner->next();
398
399         return true;
400     }
401
402     /**
403      * Check if the scanner has reached the end of a tag.
404      */
405     protected function isTagEnd(&$selfClose)
406     {
407         $tok = $this->scanner->current();
408         if ($tok == '/') {
409             $this->scanner->next();
410             $this->scanner->whitespace();
411             if ($this->scanner->current() == '>') {
412                 $selfClose = true;
413                 return true;
414             }
415             if ($this->scanner->current() === false) {
416                 $this->parseError("Unexpected EOF inside of tag.");
417                 return true;
418             }
419             // Basically, we skip the / token and go on.
420             // See 8.2.4.43.
421             $this->parseError("Unexpected '%s' inside of a tag.", $this->scanner->current());
422             return false;
423         }
424
425         if ($this->scanner->current() == '>') {
426             return true;
427         }
428         if ($this->scanner->current() === false) {
429             $this->parseError("Unexpected EOF inside of tag.");
430             return true;
431         }
432
433         return false;
434     }
435
436     /**
437      * Parse attributes from inside of a tag.
438      */
439     protected function attribute(&$attributes)
440     {
441         $tok = $this->scanner->current();
442         if ($tok == '/' || $tok == '>' || $tok === false) {
443             return false;
444         }
445
446         if ($tok == '<') {
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.");
452         }
453
454         $name = strtolower($this->scanner->charsUntil("/>=\n\f\t "));
455
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();
462         }
463
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
478         else
479             if (preg_match("/^[0-9.-]/u", $name)) {
480                 $this->parseError("Unexpected character at the begining of attribute name: %s", $name);
481                 $isValidAttribute = false;
482             }
483         // 8.1.2.3
484         $this->scanner->whitespace();
485
486         $val = $this->attributeValue();
487         if ($isValidAttribute) {
488             $attributes[$name] = $val;
489         }
490         return true;
491     }
492
493     /**
494      * Consume an attribute value.
495      * 8.2.4.37 and after.
496      */
497     protected function attributeValue()
498     {
499         if ($this->scanner->current() != '=') {
500             return null;
501         }
502         $this->scanner->next();
503         // 8.1.2.3
504         $this->scanner->whitespace();
505
506         $tok = $this->scanner->current();
507         switch ($tok) {
508             case "\n":
509             case "\f":
510             case " ":
511             case "\t":
512                 // Whitespace here indicates an empty value.
513                 return null;
514             case '"':
515             case "'":
516                 $this->scanner->next();
517                 return $this->quotedAttributeValue($tok);
518             case '>':
519                 // case '/': // 8.2.4.37 seems to allow foo=/ as a valid attr.
520                 $this->parseError("Expected attribute value, got tag end.");
521                 return null;
522             case '=':
523             case '`':
524                 $this->parseError("Expecting quotes, got %s.", $tok);
525                 return $this->unquotedAttributeValue();
526             default:
527                 return $this->unquotedAttributeValue();
528         }
529     }
530
531     /**
532      * Get an attribute value string.
533      *
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
537      *            ' or ".
538      * @return string The attribute value.
539      */
540     protected function quotedAttributeValue($quote)
541     {
542         $stoplist = "\f" . $quote;
543         $val = '';
544         $tok = $this->scanner->current();
545         while (strspn($tok, $stoplist) == 0 && $tok !== false) {
546             if ($tok == '&') {
547                 $val .= $this->decodeCharacterReference(true);
548                 $tok = $this->scanner->current();
549             } else {
550                 $val .= $tok;
551                 $tok = $this->scanner->next();
552             }
553         }
554         $this->scanner->next();
555         return $val;
556     }
557
558     protected function unquotedAttributeValue()
559     {
560         $stoplist = "\t\n\f >";
561         $val = '';
562         $tok = $this->scanner->current();
563         while (strspn($tok, $stoplist) == 0 && $tok !== false) {
564             if ($tok == '&') {
565                 $val .= $this->decodeCharacterReference(true);
566                 $tok = $this->scanner->current();
567             } else {
568                 if (strspn($tok, "\"'<=`") > 0) {
569                     $this->parseError("Unexpected chars in unquoted attribute value %s", $tok);
570                 }
571                 $val .= $tok;
572                 $tok = $this->scanner->next();
573             }
574         }
575         return $val;
576     }
577
578     /**
579      * Consume malformed markup as if it were a comment.
580      * 8.2.4.44
581      *
582      * The spec requires that the ENTIRE tag-like thing be enclosed inside of
583      * the comment. So this will generate comments like:
584      *
585      * &lt;!--&lt/+foo&gt;--&gt;
586      *
587      * @param string $leading
588      *            Prepend any leading characters. This essentially
589      *            negates the need to backtrack, but it's sort of
590      *            a hack.
591      */
592     protected function bogusComment($leading = '')
593     {
594
595         // TODO: This can be done more efficiently when the
596         // scanner exposes a readUntil() method.
597         $comment = $leading;
598         $tok = $this->scanner->current();
599         do {
600             $comment .= $tok;
601             $tok = $this->scanner->next();
602         } while ($tok !== false && $tok != '>');
603
604         $this->flushBuffer();
605         $this->events->comment($comment . $tok);
606         $this->scanner->next();
607
608         return true;
609     }
610
611     /**
612      * Read a comment.
613      *
614      * Expects the first tok to be inside of the comment.
615      */
616     protected function comment()
617     {
618         $tok = $this->scanner->current();
619         $comment = '';
620
621         // <!-->. Emit an empty comment because 8.2.4.46 says to.
622         if ($tok == '>') {
623             // Parse error. Emit the comment token.
624             $this->parseError("Expected comment data, got '>'");
625             $this->events->comment('');
626             $this->scanner->next();
627             return true;
628         }
629
630         // Replace NULL with the replacement char.
631         if ($tok == "\0") {
632             $tok = UTF8Utils::FFFD;
633         }
634         while (! $this->isCommentEnd()) {
635             $comment .= $tok;
636             $tok = $this->scanner->next();
637         }
638
639         $this->events->comment($comment);
640         $this->scanner->next();
641         return true;
642     }
643
644     /**
645      * Check if the scanner has reached the end of a comment.
646      */
647     protected function isCommentEnd()
648     {
649         // EOF
650         if ($this->scanner->current() === false) {
651             // Hit the end.
652             $this->parseError("Unexpected EOF in a comment.");
653             return true;
654         }
655
656         // If it doesn't start with -, not the end.
657         if ($this->scanner->current() != '-') {
658             return false;
659         }
660
661         // Advance one, and test for '->'
662         if ($this->scanner->next() == '-' && $this->scanner->peek() == '>') {
663             $this->scanner->next(); // Consume the last '>'
664             return true;
665         }
666         // Unread '-';
667         $this->scanner->unconsume(1);
668         return false;
669     }
670
671     /**
672      * Parse a DOCTYPE.
673      *
674      * Parse a DOCTYPE declaration. This method has strong bearing on whether or
675      * not Quirksmode is enabled on the event handler.
676      *
677      * @todo This method is a little long. Should probably refactor.
678      */
679     protected function doctype()
680     {
681         if (strcasecmp($this->scanner->current(), 'D')) {
682             return false;
683         }
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);
689         }
690
691         $this->scanner->whitespace();
692         $tok = $this->scanner->current();
693
694         // EOF: die.
695         if ($tok === false) {
696             $this->events->doctype('html5', EventHandler::DOCTYPE_NONE, '', true);
697             return $this->eof();
698         }
699
700         $doctypeName = '';
701
702         // NULL char: convert.
703         if ($tok === "\0") {
704             $this->parseError("Unexpected null character in DOCTYPE.");
705             $doctypeName .= UTF8::FFFD;
706             $tok = $this->scanner->next();
707         }
708
709         $stop = " \n\f>";
710         $doctypeName = $this->scanner->charsUntil($stop);
711         // Lowercase ASCII, replace \0 with FFFD
712         $doctypeName = strtolower(strtr($doctypeName, "\0", UTF8Utils::FFFD));
713
714         $tok = $this->scanner->current();
715
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);
720             return true;
721         }
722
723         // Short DOCTYPE, like <!DOCTYPE html>
724         if ($tok == '>') {
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();
730                 return true;
731             }
732             $this->events->doctype($doctypeName);
733             $this->scanner->next();
734             return true;
735         }
736         $this->scanner->whitespace();
737
738         $pub = strtoupper($this->scanner->getAsciiAlpha());
739         $white = strlen($this->scanner->whitespace());
740         $tok = $this->scanner->current();
741
742         // Get ID, and flag it as pub or system.
743         if (($pub == 'PUBLIC' || $pub == 'SYSTEM') && $white > 0) {
744             // Get the sys ID.
745             $type = $pub == 'PUBLIC' ? EventHandler::DOCTYPE_PUBLIC : EventHandler::DOCTYPE_SYSTEM;
746             $id = $this->quotedString("\0>");
747             if ($id === false) {
748                 $this->events->doctype($doctypeName, $type, $pub, false);
749                 return false;
750             }
751
752             // Premature EOF.
753             if ($this->scanner->current() === false) {
754                 $this->parseError("Unexpected EOF in DOCTYPE");
755                 $this->events->doctype($doctypeName, $type, $id, true);
756                 return true;
757             }
758
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();
764                 return true;
765             }
766
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();
773             return true;
774         }
775
776         // Else it's a bogus DOCTYPE.
777         // Consume to > and trash.
778         $this->scanner->charsUntil('>');
779
780         $this->parseError("Expected PUBLIC or SYSTEM. Got %s.", $pub);
781         $this->events->doctype($doctypeName, 0, null, true);
782         $this->scanner->next();
783         return true;
784     }
785
786     /**
787      * Utility for reading a quoted string.
788      *
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)
793      */
794     protected function quotedString($stopchars)
795     {
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();
802             } else {
803                 // Parse error because no close quote.
804                 $this->parseError("Expected %s, got %s", $tok, $this->scanner->current());
805             }
806             return $ret;
807         }
808         return false;
809     }
810
811     /**
812      * Handle a CDATA section.
813      */
814     protected function cdataSection()
815     {
816         if ($this->scanner->current() != '[') {
817             return false;
818         }
819         $cdata = '';
820         $this->scanner->next();
821
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);
826         }
827
828         $tok = $this->scanner->next();
829         do {
830             if ($tok === false) {
831                 $this->parseError('Unexpected EOF inside CDATA.');
832                 $this->bogusComment('<![CDATA[' . $cdata);
833                 return true;
834             }
835             $cdata .= $tok;
836             $tok = $this->scanner->next();
837         } while (! $this->sequenceMatches(']]>'));
838
839         // Consume ]]>
840         $this->scanner->consume(3);
841
842         $this->events->cdata($cdata);
843         return true;
844     }
845
846     // ================================================================
847     // Non-HTML5
848     // ================================================================
849     /**
850      * Handle a processing instruction.
851      *
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.
856      */
857     protected function processingInstruction()
858     {
859         if ($this->scanner->current() != '?') {
860             return false;
861         }
862
863         $tok = $this->scanner->next();
864         $procName = $this->scanner->getAsciiAlpha();
865         $white = strlen($this->scanner->whitespace());
866
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);
871             return true;
872         }
873
874         $data = '';
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();
878
879             $tok = $this->scanner->next();
880             if ($tok === false) {
881                 $this->parseError("Unexpected EOF in processing instruction.");
882                 $this->events->processingInstruction($procName, $data);
883                 return true;
884             }
885         }
886
887         $this->scanner->next(); // >
888         $this->scanner->next(); // Next token.
889         $this->events->processingInstruction($procName, $data);
890         return true;
891     }
892
893     // ================================================================
894     // UTILITY FUNCTIONS
895     // ================================================================
896
897     /**
898      * Read from the input stream until we get to the desired sequene
899      * or hit the end of the input stream.
900      */
901     protected function readUntilSequence($sequence)
902     {
903         $buffer = '';
904
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);
909
910             // Stop as soon as we hit the stopping condition.
911             if ($this->sequenceMatches($sequence, false)) {
912                 return $buffer;
913             }
914             $buffer .= $this->scanner->current();
915             $this->scanner->next();
916         }
917
918         // If we get here, we hit the EOF.
919         $this->parseError("Unexpected EOF during text read.");
920         return $buffer;
921     }
922
923     /**
924      * Check if upcomming chars match the given sequence.
925      *
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
930      * this returns true.
931      *
932      * Example: $this->sequenceMatches('</script>') will
933      * see if the input stream is at the start of a
934      * '</script>' string.
935      */
936     protected function sequenceMatches($sequence, $caseSensitive = true)
937     {
938         $len = strlen($sequence);
939         $buffer = '';
940         for ($i = 0; $i < $len; ++ $i) {
941             $buffer .= $this->scanner->current();
942
943             // EOF. Rewind and let the caller handle it.
944             if ($this->scanner->current() === false) {
945                 $this->scanner->unconsume($i);
946                 return false;
947             }
948             $this->scanner->next();
949         }
950
951         $this->scanner->unconsume($len);
952         return $caseSensitive ? $buffer == $sequence : strcasecmp($buffer, $sequence) === 0;
953     }
954
955     /**
956      * Send a TEXT event with the contents of the text buffer.
957      *
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.)
961      */
962     protected function flushBuffer()
963     {
964         if ($this->text === '') {
965             return;
966         }
967         $this->events->text($this->text);
968         $this->text = '';
969     }
970
971     /**
972      * Add text to the temporary buffer.
973      *
974      * @see flushBuffer()
975      */
976     protected function buffer($str)
977     {
978         $this->text .= $str;
979     }
980
981     /**
982      * Emit a parse error.
983      *
984      * A parse error always returns false because it never consumes any
985      * characters.
986      */
987     protected function parseError($msg)
988     {
989         $args = func_get_args();
990
991         if (count($args) > 1) {
992             array_shift($args);
993             $msg = vsprintf($msg, $args);
994         }
995
996         $line = $this->scanner->currentLine();
997         $col = $this->scanner->columnOffset();
998         $this->events->parseError($msg, $line, $col);
999         return false;
1000     }
1001
1002     /**
1003      * Decode a character reference and return the string.
1004      *
1005      * Returns false if the entity could not be found. If $inAttribute is set
1006      * to true, a bare & will be returned as-is.
1007      *
1008      * @param boolean $inAttribute
1009      *            Set to true if the text is inside of an attribute value.
1010      *            false otherwise.
1011      */
1012     protected function decodeCharacterReference($inAttribute = false)
1013     {
1014
1015         // If it fails this, it's definitely not an entity.
1016         if ($this->scanner->current() != '&') {
1017             return false;
1018         }
1019
1020         // Next char after &.
1021         $tok = $this->scanner->next();
1022         $entity = '';
1023         $start = $this->scanner->position();
1024
1025         if ($tok == false) {
1026             return '&';
1027         }
1028
1029         // These indicate not an entity. We return just
1030         // the &.
1031         if (strspn($tok, static::WHITE . "&<") == 1) {
1032             // $this->scanner->next();
1033             return '&';
1034         }
1035
1036         // Numeric entity
1037         if ($tok == '#') {
1038             $tok = $this->scanner->next();
1039
1040             // Hexidecimal encoding.
1041             // X[0-9a-fA-F]+;
1042             // x[0-9a-fA-F]+;
1043             if ($tok == 'x' || $tok == 'X') {
1044                 $tok = $this->scanner->next(); // Consume x
1045
1046                 // Convert from hex code to char.
1047                 $hex = $this->scanner->getHex();
1048                 if (empty($hex)) {
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);
1055                     return '&';
1056                 }
1057                 $entity = CharacterReference::lookupHex($hex);
1058             }             // Decimal encoding.
1059             // [0-9]+;
1060             else {
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);
1066                     return '&';
1067                 }
1068                 $entity = CharacterReference::lookupDecimal($numeric);
1069             }
1070         }         // String entity.
1071         else {
1072             // Attempt to consume a string up to a ';'.
1073             // [a-zA-Z0-9]+;
1074             $cname = $this->scanner->getAsciiAlpha();
1075             $entity = CharacterReference::lookupName($cname);
1076
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 &amp; elsewhere.
1080             if ($entity == null) {
1081                 $this->parseError("No match in entity table for '%s'", $cname);
1082                 $this->scanner->unconsume($this->scanner->position() - $start);
1083                 return '&';
1084             }
1085         }
1086
1087         // The scanner has advanced the cursor for us.
1088         $tok = $this->scanner->current();
1089
1090         // We have an entity. We're done here.
1091         if ($tok == ';') {
1092             $this->scanner->next();
1093             return $entity;
1094         }
1095
1096         // If in an attribute, then failing to match ; means unconsume the
1097         // entire string. Otherwise, failure to match is an error.
1098         if ($inAttribute) {
1099             $this->scanner->unconsume($this->scanner->position() - $start);
1100             return '&';
1101         }
1102
1103         $this->parseError("Expected &ENTITY;, got &ENTITY%s (no trailing ;) ", $tok);
1104         return '&' . $entity;
1105     }
1106 }