PluginProbe ʕ •ᴥ•ʔ
Independent Analytics – WordPress Analytics Plugin / 1.17
Independent Analytics – WordPress Analytics Plugin v1.17
2.15.0 2.14.10 trunk 1.1 1.10 1.10.1 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.17.1 1.17.2 1.17.3 1.17.4 1.18 1.18.1 1.19.0 1.19.1 1.2 1.20.0 1.21.0 1.22.0 1.22.1 1.23.0 1.23.1 1.24.0 1.24.1 1.25.0 1.25.1 1.26.0 1.27.0 1.28.0 1.28.1 1.28.2 1.28.3 1.29.0 1.3 1.30.0 1.30.1 1.4 1.5 1.6 1.7 1.8 1.9 2.0.0 2.0.1 2.1.4 2.1.5 2.1.6 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.10 2.11.2 2.11.3 2.11.4 2.11.5 2.11.6 2.11.7 2.11.8 2.11.9 2.12.0 2.12.1 2.12.2 2.13.1 2.13.2 2.13.5 2.13.6 2.14.0 2.14.1 2.14.2 2.14.4 2.14.6 2.14.7 2.14.8 2.14.9 2.2.0 2.2.1 2.3.1 2.3.2 2.4.2 2.4.3 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.7.1 2.7.2 2.7.3 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7
independent-analytics / vendor / masterminds / html5 / src / HTML5 / Parser / Tokenizer.php
independent-analytics / vendor / masterminds / html5 / src / HTML5 / Parser Last commit date
CharacterReference.php 3 years ago DOMTreeBuilder.php 3 years ago EventHandler.php 3 years ago FileInputStream.php 3 years ago InputStream.php 3 years ago ParseError.php 3 years ago Scanner.php 3 years ago StringInputStream.php 3 years ago Tokenizer.php 3 years ago TreeBuildingRules.php 3 years ago UTF8Utils.php 3 years ago
Tokenizer.php
1016 lines
1 <?php
2
3 namespace IAWP\Masterminds\HTML5\Parser;
4
5 use IAWP\Masterminds\HTML5\Elements;
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 protected $scanner;
29 protected $events;
30 protected $tok;
31 /**
32 * Buffer for text.
33 */
34 protected $text = '';
35 // When this goes to false, the parser stops.
36 protected $carryOn = \true;
37 protected $textMode = 0;
38 // TEXTMODE_NORMAL;
39 protected $untilTag = null;
40 const CONFORMANT_XML = 'xml';
41 const CONFORMANT_HTML = 'html';
42 protected $mode = self::CONFORMANT_HTML;
43 /**
44 * Create a new tokenizer.
45 *
46 * Typically, parsing a document involves creating a new tokenizer, giving
47 * it a scanner (input) and an event handler (output), and then calling
48 * the Tokenizer::parse() method.`
49 *
50 * @param Scanner $scanner A scanner initialized with an input stream.
51 * @param EventHandler $eventHandler An event handler, initialized and ready to receive events.
52 * @param string $mode
53 */
54 public function __construct($scanner, $eventHandler, $mode = self::CONFORMANT_HTML)
55 {
56 $this->scanner = $scanner;
57 $this->events = $eventHandler;
58 $this->mode = $mode;
59 }
60 /**
61 * Begin parsing.
62 *
63 * This will begin scanning the document, tokenizing as it goes.
64 * Tokens are emitted into the event handler.
65 *
66 * Tokenizing will continue until the document is completely
67 * read. Errors are emitted into the event handler, but
68 * the parser will attempt to continue parsing until the
69 * entire input stream is read.
70 */
71 public function parse()
72 {
73 do {
74 $this->consumeData();
75 // FIXME: Add infinite loop protection.
76 } while ($this->carryOn);
77 }
78 /**
79 * Set the text mode for the character data reader.
80 *
81 * HTML5 defines three different modes for reading text:
82 * - Normal: Read until a tag is encountered.
83 * - RCDATA: Read until a tag is encountered, but skip a few otherwise-
84 * special characters.
85 * - Raw: Read until a special closing tag is encountered (viz. pre, script)
86 *
87 * This allows those modes to be set.
88 *
89 * Normally, setting is done by the event handler via a special return code on
90 * startTag(), but it can also be set manually using this function.
91 *
92 * @param int $textmode One of Elements::TEXT_*.
93 * @param string $untilTag The tag that should stop RAW or RCDATA mode. Normal mode does not
94 * use this indicator.
95 */
96 public function setTextMode($textmode, $untilTag = null)
97 {
98 $this->textMode = $textmode & (Elements::TEXT_RAW | Elements::TEXT_RCDATA);
99 $this->untilTag = $untilTag;
100 }
101 /**
102 * Consume a character and make a move.
103 * HTML5 8.2.4.1.
104 */
105 protected function consumeData()
106 {
107 $tok = $this->scanner->current();
108 if ('&' === $tok) {
109 // Character reference
110 $ref = $this->decodeCharacterReference();
111 $this->buffer($ref);
112 $tok = $this->scanner->current();
113 }
114 // Parse tag
115 if ('<' === $tok) {
116 // Any buffered text data can go out now.
117 $this->flushBuffer();
118 $tok = $this->scanner->next();
119 if ('!' === $tok) {
120 $this->markupDeclaration();
121 } elseif ('/' === $tok) {
122 $this->endTag();
123 } elseif ('?' === $tok) {
124 $this->processingInstruction();
125 } elseif (\ctype_alpha($tok)) {
126 $this->tagName();
127 } else {
128 $this->parseError('Illegal tag opening');
129 // TODO is this necessary ?
130 $this->characterData();
131 }
132 $tok = $this->scanner->current();
133 }
134 if (\false === $tok) {
135 // Handle end of document
136 $this->eof();
137 } else {
138 // Parse character
139 switch ($this->textMode) {
140 case Elements::TEXT_RAW:
141 $this->rawText($tok);
142 break;
143 case Elements::TEXT_RCDATA:
144 $this->rcdata($tok);
145 break;
146 default:
147 if ('<' === $tok || '&' === $tok) {
148 break;
149 }
150 // NULL character
151 if ("\x00" === $tok) {
152 $this->parseError('Received null character.');
153 $this->text .= $tok;
154 $this->scanner->consume();
155 break;
156 }
157 $this->text .= $this->scanner->charsUntil("<&\x00");
158 }
159 }
160 return $this->carryOn;
161 }
162 /**
163 * Parse anything that looks like character data.
164 *
165 * Different rules apply based on the current text mode.
166 *
167 * @see Elements::TEXT_RAW Elements::TEXT_RCDATA.
168 */
169 protected function characterData()
170 {
171 $tok = $this->scanner->current();
172 if (\false === $tok) {
173 return \false;
174 }
175 switch ($this->textMode) {
176 case Elements::TEXT_RAW:
177 return $this->rawText($tok);
178 case Elements::TEXT_RCDATA:
179 return $this->rcdata($tok);
180 default:
181 if ('<' === $tok || '&' === $tok) {
182 return \false;
183 }
184 return $this->text($tok);
185 }
186 }
187 /**
188 * This buffers the current token as character data.
189 *
190 * @param string $tok The current token.
191 *
192 * @return bool
193 */
194 protected function text($tok)
195 {
196 // This should never happen...
197 if (\false === $tok) {
198 return \false;
199 }
200 // NULL character
201 if ("\x00" === $tok) {
202 $this->parseError('Received null character.');
203 }
204 $this->buffer($tok);
205 $this->scanner->consume();
206 return \true;
207 }
208 /**
209 * Read text in RAW mode.
210 *
211 * @param string $tok The current token.
212 *
213 * @return bool
214 */
215 protected function rawText($tok)
216 {
217 if (\is_null($this->untilTag)) {
218 return $this->text($tok);
219 }
220 $sequence = '</' . $this->untilTag . '>';
221 $txt = $this->readUntilSequence($sequence);
222 $this->events->text($txt);
223 $this->setTextMode(0);
224 return $this->endTag();
225 }
226 /**
227 * Read text in RCDATA mode.
228 *
229 * @param string $tok The current token.
230 *
231 * @return bool
232 */
233 protected function rcdata($tok)
234 {
235 if (\is_null($this->untilTag)) {
236 return $this->text($tok);
237 }
238 $sequence = '</' . $this->untilTag;
239 $txt = '';
240 $caseSensitive = !Elements::isHtml5Element($this->untilTag);
241 while (\false !== $tok && !('<' == $tok && $this->scanner->sequenceMatches($sequence, $caseSensitive))) {
242 if ('&' == $tok) {
243 $txt .= $this->decodeCharacterReference();
244 $tok = $this->scanner->current();
245 } else {
246 $txt .= $tok;
247 $tok = $this->scanner->next();
248 }
249 }
250 $len = \strlen($sequence);
251 $this->scanner->consume($len);
252 $len += $this->scanner->whitespace();
253 if ('>' !== $this->scanner->current()) {
254 $this->parseError('Unclosed RCDATA end tag');
255 }
256 $this->scanner->unconsume($len);
257 $this->events->text($txt);
258 $this->setTextMode(0);
259 return $this->endTag();
260 }
261 /**
262 * If the document is read, emit an EOF event.
263 */
264 protected function eof()
265 {
266 // fprintf(STDOUT, "EOF");
267 $this->flushBuffer();
268 $this->events->eof();
269 $this->carryOn = \false;
270 }
271 /**
272 * Look for markup.
273 */
274 protected function markupDeclaration()
275 {
276 $tok = $this->scanner->next();
277 // Comment:
278 if ('-' == $tok && '-' == $this->scanner->peek()) {
279 $this->scanner->consume(2);
280 return $this->comment();
281 } elseif ('D' == $tok || 'd' == $tok) {
282 // Doctype
283 return $this->doctype();
284 } elseif ('[' == $tok) {
285 // CDATA section
286 return $this->cdataSection();
287 }
288 // FINISH
289 $this->parseError('Expected <!--, <![CDATA[, or <!DOCTYPE. Got <!%s', $tok);
290 $this->bogusComment('<!');
291 return \true;
292 }
293 /**
294 * Consume an end tag. See section 8.2.4.9.
295 */
296 protected function endTag()
297 {
298 if ('/' != $this->scanner->current()) {
299 return \false;
300 }
301 $tok = $this->scanner->next();
302 // a-zA-Z -> tagname
303 // > -> parse error
304 // EOF -> parse error
305 // -> parse error
306 if (!\ctype_alpha($tok)) {
307 $this->parseError("Expected tag name, got '%s'", $tok);
308 if ("\x00" == $tok || \false === $tok) {
309 return \false;
310 }
311 return $this->bogusComment('</');
312 }
313 $name = $this->scanner->charsUntil("\n\f \t>");
314 $name = self::CONFORMANT_XML === $this->mode ? $name : \strtolower($name);
315 // Trash whitespace.
316 $this->scanner->whitespace();
317 $tok = $this->scanner->current();
318 if ('>' != $tok) {
319 $this->parseError("Expected >, got '%s'", $tok);
320 // We just trash stuff until we get to the next tag close.
321 $this->scanner->charsUntil('>');
322 }
323 $this->events->endTag($name);
324 $this->scanner->consume();
325 return \true;
326 }
327 /**
328 * Consume a tag name and body. See section 8.2.4.10.
329 */
330 protected function tagName()
331 {
332 // We know this is at least one char.
333 $name = $this->scanner->charsWhile(':_-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
334 $name = self::CONFORMANT_XML === $this->mode ? $name : \strtolower($name);
335 $attributes = array();
336 $selfClose = \false;
337 // Handle attribute parse exceptions here so that we can
338 // react by trying to build a sensible parse tree.
339 try {
340 do {
341 $this->scanner->whitespace();
342 $this->attribute($attributes);
343 } while (!$this->isTagEnd($selfClose));
344 } catch (ParseError $e) {
345 $selfClose = \false;
346 }
347 $mode = $this->events->startTag($name, $attributes, $selfClose);
348 if (\is_int($mode)) {
349 $this->setTextMode($mode, $name);
350 }
351 $this->scanner->consume();
352 return \true;
353 }
354 /**
355 * Check if the scanner has reached the end of a tag.
356 */
357 protected function isTagEnd(&$selfClose)
358 {
359 $tok = $this->scanner->current();
360 if ('/' == $tok) {
361 $this->scanner->consume();
362 $this->scanner->whitespace();
363 $tok = $this->scanner->current();
364 if ('>' == $tok) {
365 $selfClose = \true;
366 return \true;
367 }
368 if (\false === $tok) {
369 $this->parseError('Unexpected EOF inside of tag.');
370 return \true;
371 }
372 // Basically, we skip the / token and go on.
373 // See 8.2.4.43.
374 $this->parseError("Unexpected '%s' inside of a tag.", $tok);
375 return \false;
376 }
377 if ('>' == $tok) {
378 return \true;
379 }
380 if (\false === $tok) {
381 $this->parseError('Unexpected EOF inside of tag.');
382 return \true;
383 }
384 return \false;
385 }
386 /**
387 * Parse attributes from inside of a tag.
388 *
389 * @param string[] $attributes
390 *
391 * @return bool
392 *
393 * @throws ParseError
394 */
395 protected function attribute(&$attributes)
396 {
397 $tok = $this->scanner->current();
398 if ('/' == $tok || '>' == $tok || \false === $tok) {
399 return \false;
400 }
401 if ('<' == $tok) {
402 $this->parseError("Unexpected '<' inside of attributes list.");
403 // Push the < back onto the stack.
404 $this->scanner->unconsume();
405 // Let the caller figure out how to handle this.
406 throw new ParseError('Start tag inside of attribute.');
407 }
408 $name = \strtolower($this->scanner->charsUntil("/>=\n\f\t "));
409 if (0 == \strlen($name)) {
410 $tok = $this->scanner->current();
411 $this->parseError('Expected an attribute name, got %s.', $tok);
412 // Really, only '=' can be the char here. Everything else gets absorbed
413 // under one rule or another.
414 $name = $tok;
415 $this->scanner->consume();
416 }
417 $isValidAttribute = \true;
418 // Attribute names can contain most Unicode characters for HTML5.
419 // But method "DOMElement::setAttribute" is throwing exception
420 // because of it's own internal restriction so these have to be filtered.
421 // see issue #23: https://github.com/Masterminds/html5-php/issues/23
422 // and http://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name
423 if (\preg_match("/[\x01-,\\/;-@[-^`{-]/u", $name)) {
424 $this->parseError('Unexpected characters in attribute name: %s', $name);
425 $isValidAttribute = \false;
426 } elseif (\preg_match('/^[0-9.-]/u', $name)) {
427 $this->parseError('Unexpected character at the begining of attribute name: %s', $name);
428 $isValidAttribute = \false;
429 }
430 // 8.1.2.3
431 $this->scanner->whitespace();
432 $val = $this->attributeValue();
433 if ($isValidAttribute) {
434 $attributes[$name] = $val;
435 }
436 return \true;
437 }
438 /**
439 * Consume an attribute value. See section 8.2.4.37 and after.
440 *
441 * @return string|null
442 */
443 protected function attributeValue()
444 {
445 if ('=' != $this->scanner->current()) {
446 return null;
447 }
448 $this->scanner->consume();
449 // 8.1.2.3
450 $this->scanner->whitespace();
451 $tok = $this->scanner->current();
452 switch ($tok) {
453 case "\n":
454 case "\f":
455 case ' ':
456 case "\t":
457 // Whitespace here indicates an empty value.
458 return null;
459 case '"':
460 case "'":
461 $this->scanner->consume();
462 return $this->quotedAttributeValue($tok);
463 case '>':
464 // case '/': // 8.2.4.37 seems to allow foo=/ as a valid attr.
465 $this->parseError('Expected attribute value, got tag end.');
466 return null;
467 case '=':
468 case '`':
469 $this->parseError('Expecting quotes, got %s.', $tok);
470 return $this->unquotedAttributeValue();
471 default:
472 return $this->unquotedAttributeValue();
473 }
474 }
475 /**
476 * Get an attribute value string.
477 *
478 * @param string $quote IMPORTANT: This is a series of chars! Any one of which will be considered
479 * termination of an attribute's value. E.g. "\"'" will stop at either
480 * ' or ".
481 *
482 * @return string The attribute value.
483 */
484 protected function quotedAttributeValue($quote)
485 {
486 $stoplist = "\f" . $quote;
487 $val = '';
488 while (\true) {
489 $tokens = $this->scanner->charsUntil($stoplist . '&');
490 if (\false !== $tokens) {
491 $val .= $tokens;
492 } else {
493 break;
494 }
495 $tok = $this->scanner->current();
496 if ('&' == $tok) {
497 $val .= $this->decodeCharacterReference(\true);
498 continue;
499 }
500 break;
501 }
502 $this->scanner->consume();
503 return $val;
504 }
505 protected function unquotedAttributeValue()
506 {
507 $val = '';
508 $tok = $this->scanner->current();
509 while (\false !== $tok) {
510 switch ($tok) {
511 case "\n":
512 case "\f":
513 case ' ':
514 case "\t":
515 case '>':
516 break 2;
517 case '&':
518 $val .= $this->decodeCharacterReference(\true);
519 $tok = $this->scanner->current();
520 break;
521 case "'":
522 case '"':
523 case '<':
524 case '=':
525 case '`':
526 $this->parseError('Unexpected chars in unquoted attribute value %s', $tok);
527 $val .= $tok;
528 $tok = $this->scanner->next();
529 break;
530 default:
531 $val .= $this->scanner->charsUntil("\t\n\f >&\"'<=`");
532 $tok = $this->scanner->current();
533 }
534 }
535 return $val;
536 }
537 /**
538 * Consume malformed markup as if it were a comment.
539 * 8.2.4.44.
540 *
541 * The spec requires that the ENTIRE tag-like thing be enclosed inside of
542 * the comment. So this will generate comments like:
543 *
544 * &lt;!--&lt/+foo&gt;--&gt;
545 *
546 * @param string $leading Prepend any leading characters. This essentially
547 * negates the need to backtrack, but it's sort of a hack.
548 *
549 * @return bool
550 */
551 protected function bogusComment($leading = '')
552 {
553 $comment = $leading;
554 $tokens = $this->scanner->charsUntil('>');
555 if (\false !== $tokens) {
556 $comment .= $tokens;
557 }
558 $tok = $this->scanner->current();
559 if (\false !== $tok) {
560 $comment .= $tok;
561 }
562 $this->flushBuffer();
563 $this->events->comment($comment);
564 $this->scanner->consume();
565 return \true;
566 }
567 /**
568 * Read a comment.
569 * Expects the first tok to be inside of the comment.
570 *
571 * @return bool
572 */
573 protected function comment()
574 {
575 $tok = $this->scanner->current();
576 $comment = '';
577 // <!-->. Emit an empty comment because 8.2.4.46 says to.
578 if ('>' == $tok) {
579 // Parse error. Emit the comment token.
580 $this->parseError("Expected comment data, got '>'");
581 $this->events->comment('');
582 $this->scanner->consume();
583 return \true;
584 }
585 // Replace NULL with the replacement char.
586 if ("\x00" == $tok) {
587 $tok = UTF8Utils::FFFD;
588 }
589 while (!$this->isCommentEnd()) {
590 $comment .= $tok;
591 $tok = $this->scanner->next();
592 }
593 $this->events->comment($comment);
594 $this->scanner->consume();
595 return \true;
596 }
597 /**
598 * Check if the scanner has reached the end of a comment.
599 *
600 * @return bool
601 */
602 protected function isCommentEnd()
603 {
604 $tok = $this->scanner->current();
605 // EOF
606 if (\false === $tok) {
607 // Hit the end.
608 $this->parseError('Unexpected EOF in a comment.');
609 return \true;
610 }
611 // If next two tokens are not '--', not the end.
612 if ('-' != $tok || '-' != $this->scanner->peek()) {
613 return \false;
614 }
615 $this->scanner->consume(2);
616 // Consume '-' and one of '!' or '>'
617 // Test for '>'
618 if ('>' == $this->scanner->current()) {
619 return \true;
620 }
621 // Test for '!>'
622 if ('!' == $this->scanner->current() && '>' == $this->scanner->peek()) {
623 $this->scanner->consume();
624 // Consume the last '>'
625 return \true;
626 }
627 // Unread '-' and one of '!' or '>';
628 $this->scanner->unconsume(2);
629 return \false;
630 }
631 /**
632 * Parse a DOCTYPE.
633 *
634 * Parse a DOCTYPE declaration. This method has strong bearing on whether or
635 * not Quirksmode is enabled on the event handler.
636 *
637 * @todo This method is a little long. Should probably refactor.
638 *
639 * @return bool
640 */
641 protected function doctype()
642 {
643 // Check that string is DOCTYPE.
644 if ($this->scanner->sequenceMatches('DOCTYPE', \false)) {
645 $this->scanner->consume(7);
646 } else {
647 $chars = $this->scanner->charsWhile('DOCTYPEdoctype');
648 $this->parseError('Expected DOCTYPE, got %s', $chars);
649 return $this->bogusComment('<!' . $chars);
650 }
651 $this->scanner->whitespace();
652 $tok = $this->scanner->current();
653 // EOF: die.
654 if (\false === $tok) {
655 $this->events->doctype('html5', EventHandler::DOCTYPE_NONE, '', \true);
656 $this->eof();
657 return \true;
658 }
659 // NULL char: convert.
660 if ("\x00" === $tok) {
661 $this->parseError('Unexpected null character in DOCTYPE.');
662 }
663 $stop = " \n\f>";
664 $doctypeName = $this->scanner->charsUntil($stop);
665 // Lowercase ASCII, replace \0 with FFFD
666 $doctypeName = \strtolower(\strtr($doctypeName, "\x00", UTF8Utils::FFFD));
667 $tok = $this->scanner->current();
668 // If false, emit a parse error, DOCTYPE, and return.
669 if (\false === $tok) {
670 $this->parseError('Unexpected EOF in DOCTYPE declaration.');
671 $this->events->doctype($doctypeName, EventHandler::DOCTYPE_NONE, null, \true);
672 return \true;
673 }
674 // Short DOCTYPE, like <!DOCTYPE html>
675 if ('>' == $tok) {
676 // DOCTYPE without a name.
677 if (0 == \strlen($doctypeName)) {
678 $this->parseError('Expected a DOCTYPE name. Got nothing.');
679 $this->events->doctype($doctypeName, 0, null, \true);
680 $this->scanner->consume();
681 return \true;
682 }
683 $this->events->doctype($doctypeName);
684 $this->scanner->consume();
685 return \true;
686 }
687 $this->scanner->whitespace();
688 $pub = \strtoupper($this->scanner->getAsciiAlpha());
689 $white = $this->scanner->whitespace();
690 // Get ID, and flag it as pub or system.
691 if (('PUBLIC' == $pub || 'SYSTEM' == $pub) && $white > 0) {
692 // Get the sys ID.
693 $type = 'PUBLIC' == $pub ? EventHandler::DOCTYPE_PUBLIC : EventHandler::DOCTYPE_SYSTEM;
694 $id = $this->quotedString("\x00>");
695 if (\false === $id) {
696 $this->events->doctype($doctypeName, $type, $pub, \false);
697 return \true;
698 }
699 // Premature EOF.
700 if (\false === $this->scanner->current()) {
701 $this->parseError('Unexpected EOF in DOCTYPE');
702 $this->events->doctype($doctypeName, $type, $id, \true);
703 return \true;
704 }
705 // Well-formed complete DOCTYPE.
706 $this->scanner->whitespace();
707 if ('>' == $this->scanner->current()) {
708 $this->events->doctype($doctypeName, $type, $id, \false);
709 $this->scanner->consume();
710 return \true;
711 }
712 // If we get here, we have <!DOCTYPE foo PUBLIC "bar" SOME_JUNK
713 // Throw away the junk, parse error, quirks mode, return true.
714 $this->scanner->charsUntil('>');
715 $this->parseError('Malformed DOCTYPE.');
716 $this->events->doctype($doctypeName, $type, $id, \true);
717 $this->scanner->consume();
718 return \true;
719 }
720 // Else it's a bogus DOCTYPE.
721 // Consume to > and trash.
722 $this->scanner->charsUntil('>');
723 $this->parseError('Expected PUBLIC or SYSTEM. Got %s.', $pub);
724 $this->events->doctype($doctypeName, 0, null, \true);
725 $this->scanner->consume();
726 return \true;
727 }
728 /**
729 * Utility for reading a quoted string.
730 *
731 * @param string $stopchars Characters (in addition to a close-quote) that should stop the string.
732 * E.g. sometimes '>' is higher precedence than '"' or "'".
733 *
734 * @return mixed String if one is found (quotations omitted).
735 */
736 protected function quotedString($stopchars)
737 {
738 $tok = $this->scanner->current();
739 if ('"' == $tok || "'" == $tok) {
740 $this->scanner->consume();
741 $ret = $this->scanner->charsUntil($tok . $stopchars);
742 if ($this->scanner->current() == $tok) {
743 $this->scanner->consume();
744 } else {
745 // Parse error because no close quote.
746 $this->parseError('Expected %s, got %s', $tok, $this->scanner->current());
747 }
748 return $ret;
749 }
750 return \false;
751 }
752 /**
753 * Handle a CDATA section.
754 *
755 * @return bool
756 */
757 protected function cdataSection()
758 {
759 $cdata = '';
760 $this->scanner->consume();
761 $chars = $this->scanner->charsWhile('CDAT');
762 if ('CDATA' != $chars || '[' != $this->scanner->current()) {
763 $this->parseError('Expected [CDATA[, got %s', $chars);
764 return $this->bogusComment('<![' . $chars);
765 }
766 $tok = $this->scanner->next();
767 do {
768 if (\false === $tok) {
769 $this->parseError('Unexpected EOF inside CDATA.');
770 $this->bogusComment('<![CDATA[' . $cdata);
771 return \true;
772 }
773 $cdata .= $tok;
774 $tok = $this->scanner->next();
775 } while (!$this->scanner->sequenceMatches(']]>'));
776 // Consume ]]>
777 $this->scanner->consume(3);
778 $this->events->cdata($cdata);
779 return \true;
780 }
781 // ================================================================
782 // Non-HTML5
783 // ================================================================
784 /**
785 * Handle a processing instruction.
786 *
787 * XML processing instructions are supposed to be ignored in HTML5,
788 * treated as "bogus comments". However, since we're not a user
789 * agent, we allow them. We consume until ?> and then issue a
790 * EventListener::processingInstruction() event.
791 *
792 * @return bool
793 */
794 protected function processingInstruction()
795 {
796 if ('?' != $this->scanner->current()) {
797 return \false;
798 }
799 $tok = $this->scanner->next();
800 $procName = $this->scanner->getAsciiAlpha();
801 $white = $this->scanner->whitespace();
802 // If not a PI, send to bogusComment.
803 if (0 == \strlen($procName) || 0 == $white || \false == $this->scanner->current()) {
804 $this->parseError("Expected processing instruction name, got {$tok}");
805 $this->bogusComment('<?' . $tok . $procName);
806 return \true;
807 }
808 $data = '';
809 // As long as it's not the case that the next two chars are ? and >.
810 while (!('?' == $this->scanner->current() && '>' == $this->scanner->peek())) {
811 $data .= $this->scanner->current();
812 $tok = $this->scanner->next();
813 if (\false === $tok) {
814 $this->parseError('Unexpected EOF in processing instruction.');
815 $this->events->processingInstruction($procName, $data);
816 return \true;
817 }
818 }
819 $this->scanner->consume(2);
820 // Consume the closing tag
821 $this->events->processingInstruction($procName, $data);
822 return \true;
823 }
824 // ================================================================
825 // UTILITY FUNCTIONS
826 // ================================================================
827 /**
828 * Read from the input stream until we get to the desired sequene
829 * or hit the end of the input stream.
830 *
831 * @param string $sequence
832 *
833 * @return string
834 */
835 protected function readUntilSequence($sequence)
836 {
837 $buffer = '';
838 // Optimization for reading larger blocks faster.
839 $first = \substr($sequence, 0, 1);
840 while (\false !== $this->scanner->current()) {
841 $buffer .= $this->scanner->charsUntil($first);
842 // Stop as soon as we hit the stopping condition.
843 if ($this->scanner->sequenceMatches($sequence, \false)) {
844 return $buffer;
845 }
846 $buffer .= $this->scanner->current();
847 $this->scanner->consume();
848 }
849 // If we get here, we hit the EOF.
850 $this->parseError('Unexpected EOF during text read.');
851 return $buffer;
852 }
853 /**
854 * Check if upcomming chars match the given sequence.
855 *
856 * This will read the stream for the $sequence. If it's
857 * found, this will return true. If not, return false.
858 * Since this unconsumes any chars it reads, the caller
859 * will still need to read the next sequence, even if
860 * this returns true.
861 *
862 * Example: $this->scanner->sequenceMatches('</script>') will
863 * see if the input stream is at the start of a
864 * '</script>' string.
865 *
866 * @param string $sequence
867 * @param bool $caseSensitive
868 *
869 * @return bool
870 */
871 protected function sequenceMatches($sequence, $caseSensitive = \true)
872 {
873 @\trigger_error(__METHOD__ . ' method is deprecated since version 2.4 and will be removed in 3.0. Use Scanner::sequenceMatches() instead.', \E_USER_DEPRECATED);
874 return $this->scanner->sequenceMatches($sequence, $caseSensitive);
875 }
876 /**
877 * Send a TEXT event with the contents of the text buffer.
878 *
879 * This emits an EventHandler::text() event with the current contents of the
880 * temporary text buffer. (The buffer is used to group as much PCDATA
881 * as we can instead of emitting lots and lots of TEXT events.)
882 */
883 protected function flushBuffer()
884 {
885 if ('' === $this->text) {
886 return;
887 }
888 $this->events->text($this->text);
889 $this->text = '';
890 }
891 /**
892 * Add text to the temporary buffer.
893 *
894 * @see flushBuffer()
895 *
896 * @param string $str
897 */
898 protected function buffer($str)
899 {
900 $this->text .= $str;
901 }
902 /**
903 * Emit a parse error.
904 *
905 * A parse error always returns false because it never consumes any
906 * characters.
907 *
908 * @param string $msg
909 *
910 * @return string
911 */
912 protected function parseError($msg)
913 {
914 $args = \func_get_args();
915 if (\count($args) > 1) {
916 \array_shift($args);
917 $msg = \vsprintf($msg, $args);
918 }
919 $line = $this->scanner->currentLine();
920 $col = $this->scanner->columnOffset();
921 $this->events->parseError($msg, $line, $col);
922 return \false;
923 }
924 /**
925 * Decode a character reference and return the string.
926 *
927 * If $inAttribute is set to true, a bare & will be returned as-is.
928 *
929 * @param bool $inAttribute Set to true if the text is inside of an attribute value.
930 * false otherwise.
931 *
932 * @return string
933 */
934 protected function decodeCharacterReference($inAttribute = \false)
935 {
936 // Next char after &.
937 $tok = $this->scanner->next();
938 $start = $this->scanner->position();
939 if (\false === $tok) {
940 return '&';
941 }
942 // These indicate not an entity. We return just
943 // the &.
944 if ("\t" === $tok || "\n" === $tok || "\f" === $tok || ' ' === $tok || '&' === $tok || '<' === $tok) {
945 // $this->scanner->next();
946 return '&';
947 }
948 // Numeric entity
949 if ('#' === $tok) {
950 $tok = $this->scanner->next();
951 if (\false === $tok) {
952 $this->parseError('Expected &#DEC; &#HEX;, got EOF');
953 $this->scanner->unconsume(1);
954 return '&';
955 }
956 // Hexidecimal encoding.
957 // X[0-9a-fA-F]+;
958 // x[0-9a-fA-F]+;
959 if ('x' === $tok || 'X' === $tok) {
960 $tok = $this->scanner->next();
961 // Consume x
962 // Convert from hex code to char.
963 $hex = $this->scanner->getHex();
964 if (empty($hex)) {
965 $this->parseError('Expected &#xHEX;, got &#x%s', $tok);
966 // We unconsume because we don't know what parser rules might
967 // be in effect for the remaining chars. For example. '&#>'
968 // might result in a specific parsing rule inside of tag
969 // contexts, while not inside of pcdata context.
970 $this->scanner->unconsume(2);
971 return '&';
972 }
973 $entity = CharacterReference::lookupHex($hex);
974 } else {
975 // Convert from decimal to char.
976 $numeric = $this->scanner->getNumeric();
977 if (\false === $numeric) {
978 $this->parseError('Expected &#DIGITS;, got &#%s', $tok);
979 $this->scanner->unconsume(2);
980 return '&';
981 }
982 $entity = CharacterReference::lookupDecimal($numeric);
983 }
984 } elseif ('=' === $tok && $inAttribute) {
985 return '&';
986 } else {
987 // String entity.
988 // Attempt to consume a string up to a ';'.
989 // [a-zA-Z0-9]+;
990 $cname = $this->scanner->getAsciiAlphaNum();
991 $entity = CharacterReference::lookupName($cname);
992 // When no entity is found provide the name of the unmatched string
993 // and continue on as the & is not part of an entity. The & will
994 // be converted to &amp; elsewhere.
995 if (null === $entity) {
996 if (!$inAttribute || '' === $cname) {
997 $this->parseError("No match in entity table for '%s'", $cname);
998 }
999 $this->scanner->unconsume($this->scanner->position() - $start);
1000 return '&';
1001 }
1002 }
1003 // The scanner has advanced the cursor for us.
1004 $tok = $this->scanner->current();
1005 // We have an entity. We're done here.
1006 if (';' === $tok) {
1007 $this->scanner->consume();
1008 return $entity;
1009 }
1010 // Failing to match ; means unconsume the entire string.
1011 $this->scanner->unconsume($this->scanner->position() - $start);
1012 $this->parseError('Expected &ENTITY;, got &ENTITY%s (no trailing ;) ', $tok);
1013 return '&';
1014 }
1015 }
1016