5 Copyright 2007 Jeroen van der Meer <http://jero.net/>
6 Copyright 2009 Edward Z. Yang <edwardzyang@thewritingpot.com>
8 Permission is hereby granted, free of charge, to any person obtaining a
9 copy of this software and associated documentation files (the
10 "Software"), to deal in the Software without restriction, including
11 without limitation the rights to use, copy, modify, merge, publish,
12 distribute, sublicense, and/or sell copies of the Software, and to
13 permit persons to whom the Software is furnished to do so, subject to
14 the following conditions:
16 The above copyright notice and this permission notice shall be included
17 in all copies or substantial portions of the Software.
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20 OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
22 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
23 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
24 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
25 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 // Tags for FIX ME!!!: (in order of priority)
30 // XXX - should be fixed NAO!
31 // XERROR - with regards to parse errors
32 // XSCRIPT - with regards to scripting mode
33 // XENCODING - with regards to encoding (for reparsing tests)
35 class HTML5_TreeBuilder {
36 public $stack = array();
37 public $content_model;
40 private $original_mode;
41 private $secondary_mode;
43 // Whether or not normal insertion of nodes should actually foster
44 // parent (used in one case in spec)
45 private $foster_parent = false;
46 private $a_formatting = array();
48 private $head_pointer = null;
49 private $form_pointer = null;
51 private $flag_frameset_ok = true;
52 private $flag_force_quirks = false;
53 private $ignored = false;
54 private $quirks_mode = null;
55 // this gets to 2 when we want to ignore the next lf character, and
56 // is decrement at the beginning of each processed token (this way,
57 // code can check for (bool)$ignore_lf_token, but it phases out
59 private $ignore_lf_token = 0;
60 private $fragment = false;
63 private $scoping = array('applet','button','caption','html','marquee','object','table','td','th', 'svg:foreignObject');
64 private $formatting = array('a','b','big','code','em','font','i','nobr','s','small','strike','strong','tt','u');
65 private $special = array('address','area','article','aside','base','basefont','bgsound',
66 'blockquote','body','br','center','col','colgroup','command','dd','details','dialog','dir','div','dl',
67 'dt','embed','fieldset','figure','footer','form','frame','frameset','h1','h2','h3','h4','h5',
68 'h6','head','header','hgroup','hr','iframe','img','input','isindex','li','link',
69 'listing','menu','meta','nav','noembed','noframes','noscript','ol',
70 'p','param','plaintext','pre','script','select','spacer','style',
71 'tbody','textarea','tfoot','thead','title','tr','ul','wbr');
73 // Tree construction modes
75 const BEFORE_HTML = 1;
76 const BEFORE_HEAD = 2;
78 const IN_HEAD_NOSCRIPT = 4;
81 const IN_CDATA_RCDATA = 7;
84 const IN_COLUMN_GROUP = 10;
85 const IN_TABLE_BODY = 11;
89 const IN_SELECT_IN_TABLE= 15;
90 const IN_FOREIGN_CONTENT= 16;
91 const AFTER_BODY = 17;
92 const IN_FRAMESET = 18;
93 const AFTER_FRAMESET = 19;
94 const AFTER_AFTER_BODY = 20;
95 const AFTER_AFTER_FRAMESET = 21;
98 * Converts a magic number to a readable name. Use for debugging.
100 private function strConst($number) {
103 $r = new ReflectionClass('HTML5_TreeBuilder');
104 $lookup = array_flip($r->getConstants());
106 return $lookup[$number];
109 // The different types of elements.
112 const FORMATTING = 102;
113 const PHRASING = 103;
115 // Quirks modes in $quirks_mode
116 const NO_QUIRKS = 200;
117 const QUIRKS_MODE = 201;
118 const LIMITED_QUIRKS_MODE = 202;
120 // Marker to be placed in $a_formatting
123 // Namespaces for foreign content
124 const NS_HTML = null; // to prevent DOM from requiring NS on everything
125 const NS_MATHML = 'http://www.w3.org/1998/Math/MathML';
126 const NS_SVG = 'http://www.w3.org/2000/svg';
127 const NS_XLINK = 'http://www.w3.org/1999/xlink';
128 const NS_XML = 'http://www.w3.org/XML/1998/namespace';
129 const NS_XMLNS = 'http://www.w3.org/2000/xmlns/';
130 const NS_GOOGLE = 'http://base.google.com/ns/1.0';
132 public function __construct() {
133 $this->mode = self::INITIAL;
134 $this->dom = new DOMDocument;
136 $this->dom->encoding = 'UTF-8';
137 $this->dom->preserveWhiteSpace = true;
138 $this->dom->substituteEntities = true;
139 $this->dom->strictErrorChecking = false;
142 // Process tag tokens
143 public function emitToken($token, $mode = null) {
144 // XXX: ignore parse errors... why are we emitting them, again?
145 if ($token['type'] === HTML5_Tokenizer::PARSEERROR) return;
146 if ($mode === null) $mode = $this->mode;
149 $backtrace = debug_backtrace();
150 if ($backtrace[1]['class'] !== 'HTML5_TreeBuilder') echo "--\n";
151 echo $this->strConst($mode);
152 if ($this->original_mode) echo " (originally ".$this->strConst($this->original_mode).")";
156 $this->printActiveFormattingElements();
157 if ($this->foster_parent) echo " -> this is a foster parent mode\n";
160 if ($this->ignore_lf_token) $this->ignore_lf_token--;
161 $this->ignored = false;
162 // indenting is a little wonky, this can be changed later on
167 /* A character token that is one of U+0009 CHARACTER TABULATION,
168 * U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020 SPACE */
169 if ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
170 /* Ignore the token. */
171 $this->ignored = true;
172 } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
174 $token['name'] !== 'html' || !empty($token['public']) ||
175 !empty($token['system']) || $token !== 'about:legacy-compat'
177 /* If the DOCTYPE token's name is not a case-sensitive match
178 * for the string "html", or if the token's public identifier
179 * is not missing, or if the token's system identifier is
180 * neither missing nor a case-sensitive match for the string
181 * "about:legacy-compat", then there is a parse error (this
182 * is the DOCTYPE parse error). */
183 // DOCTYPE parse error
185 /* Append a DocumentType node to the Document node, with the name
186 * attribute set to the name given in the DOCTYPE token, or the
187 * empty string if the name was missing; the publicId attribute
188 * set to the public identifier given in the DOCTYPE token, or
189 * the empty string if the public identifier was missing; the
190 * systemId attribute set to the system identifier given in the
191 * DOCTYPE token, or the empty string if the system identifier
192 * was missing; and the other attributes specific to
193 * DocumentType objects set to null and empty lists as
194 * appropriate. Associate the DocumentType node with the
195 * Document object so that it is returned as the value of the
196 * doctype attribute of the Document object. */
197 if (!isset($token['public'])) $token['public'] = null;
198 if (!isset($token['system'])) $token['system'] = null;
199 // Yes this is hacky. I'm kind of annoyed that I can't appendChild
200 // a doctype to DOMDocument. Maybe I haven't chanted the right
202 $impl = new DOMImplementation();
203 // This call can fail for particularly pathological cases (namely,
204 // the qualifiedName parameter ($token['name']) could be missing.
205 if ($token['name']) {
206 $doctype = $impl->createDocumentType($token['name'], $token['public'], $token['system']);
207 $this->dom->appendChild($doctype);
209 // It looks like libxml's not actually *able* to express this case.
211 $this->dom->emptyDoctype = true;
213 $public = is_null($token['public']) ? false : strtolower($token['public']);
214 $system = is_null($token['system']) ? false : strtolower($token['system']);
215 $publicStartsWithForQuirks = array(
216 "+//silmaril//dtd html pro v0r11 19970101//",
217 "-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
218 "-//as//dtd html 3.0 aswedit + extensions//",
219 "-//ietf//dtd html 2.0 level 1//",
220 "-//ietf//dtd html 2.0 level 2//",
221 "-//ietf//dtd html 2.0 strict level 1//",
222 "-//ietf//dtd html 2.0 strict level 2//",
223 "-//ietf//dtd html 2.0 strict//",
224 "-//ietf//dtd html 2.0//",
225 "-//ietf//dtd html 2.1e//",
226 "-//ietf//dtd html 3.0//",
227 "-//ietf//dtd html 3.2 final//",
228 "-//ietf//dtd html 3.2//",
229 "-//ietf//dtd html 3//",
230 "-//ietf//dtd html level 0//",
231 "-//ietf//dtd html level 1//",
232 "-//ietf//dtd html level 2//",
233 "-//ietf//dtd html level 3//",
234 "-//ietf//dtd html strict level 0//",
235 "-//ietf//dtd html strict level 1//",
236 "-//ietf//dtd html strict level 2//",
237 "-//ietf//dtd html strict level 3//",
238 "-//ietf//dtd html strict//",
239 "-//ietf//dtd html//",
240 "-//metrius//dtd metrius presentational//",
241 "-//microsoft//dtd internet explorer 2.0 html strict//",
242 "-//microsoft//dtd internet explorer 2.0 html//",
243 "-//microsoft//dtd internet explorer 2.0 tables//",
244 "-//microsoft//dtd internet explorer 3.0 html strict//",
245 "-//microsoft//dtd internet explorer 3.0 html//",
246 "-//microsoft//dtd internet explorer 3.0 tables//",
247 "-//netscape comm. corp.//dtd html//",
248 "-//netscape comm. corp.//dtd strict html//",
249 "-//o'reilly and associates//dtd html 2.0//",
250 "-//o'reilly and associates//dtd html extended 1.0//",
251 "-//o'reilly and associates//dtd html extended relaxed 1.0//",
252 "-//spyglass//dtd html 2.0 extended//",
253 "-//sq//dtd html 2.0 hotmetal + extensions//",
254 "-//sun microsystems corp.//dtd hotjava html//",
255 "-//sun microsystems corp.//dtd hotjava strict html//",
256 "-//w3c//dtd html 3 1995-03-24//",
257 "-//w3c//dtd html 3.2 draft//",
258 "-//w3c//dtd html 3.2 final//",
259 "-//w3c//dtd html 3.2//",
260 "-//w3c//dtd html 3.2s draft//",
261 "-//w3c//dtd html 4.0 frameset//",
262 "-//w3c//dtd html 4.0 transitional//",
263 "-//w3c//dtd html experimental 19960712//",
264 "-//w3c//dtd html experimental 970421//",
265 "-//w3c//dtd w3 html//",
266 "-//w3o//dtd w3 html 3.0//",
267 "-//webtechs//dtd mozilla html 2.0//",
268 "-//webtechs//dtd mozilla html//",
270 $publicSetToForQuirks = array(
271 "-//w3o//dtd w3 html strict 3.0//",
272 "-/w3c/dtd html 4.0 transitional/en",
275 $publicStartsWithAndSystemForQuirks = array(
276 "-//w3c//dtd html 4.01 frameset//",
277 "-//w3c//dtd html 4.01 transitional//",
279 $publicStartsWithForLimitedQuirks = array(
280 "-//w3c//dtd xhtml 1.0 frameset//",
281 "-//w3c//dtd xhtml 1.0 transitional//",
283 $publicStartsWithAndSystemForLimitedQuirks = array(
284 "-//w3c//dtd html 4.01 frameset//",
285 "-//w3c//dtd html 4.01 transitional//",
287 // first, do easy checks
289 !empty($token['force-quirks']) ||
290 strtolower($token['name']) !== 'html'
292 $this->quirks_mode = self::QUIRKS_MODE;
296 foreach ($publicStartsWithAndSystemForQuirks as $x) {
297 if (strncmp($public, $x, strlen($x)) === 0) {
298 $this->quirks_mode = self::QUIRKS_MODE;
302 if (!is_null($this->quirks_mode)) break;
303 foreach ($publicStartsWithAndSystemForLimitedQuirks as $x) {
304 if (strncmp($public, $x, strlen($x)) === 0) {
305 $this->quirks_mode = self::LIMITED_QUIRKS_MODE;
309 if (!is_null($this->quirks_mode)) break;
311 foreach ($publicSetToForQuirks as $x) {
312 if ($public === $x) {
313 $this->quirks_mode = self::QUIRKS_MODE;
317 if (!is_null($this->quirks_mode)) break;
318 foreach ($publicStartsWithForLimitedQuirks as $x) {
319 if (strncmp($public, $x, strlen($x)) === 0) {
320 $this->quirks_mode = self::LIMITED_QUIRKS_MODE;
323 if (!is_null($this->quirks_mode)) break;
324 if ($system === "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
325 $this->quirks_mode = self::QUIRKS_MODE;
328 foreach ($publicStartsWithForQuirks as $x) {
329 if (strncmp($public, $x, strlen($x)) === 0) {
330 $this->quirks_mode = self::QUIRKS_MODE;
334 if (is_null($this->quirks_mode)) {
335 $this->quirks_mode = self::NO_QUIRKS;
339 $this->mode = self::BEFORE_HTML;
342 /* Switch the insertion mode to "before html", then reprocess the
344 $this->mode = self::BEFORE_HTML;
345 $this->quirks_mode = self::QUIRKS_MODE;
346 $this->emitToken($token);
350 case self::BEFORE_HTML:
352 /* A DOCTYPE token */
353 if($token['type'] === HTML5_Tokenizer::DOCTYPE) {
354 // Parse error. Ignore the token.
355 $this->ignored = true;
357 /* A comment token */
358 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
359 /* Append a Comment node to the Document object with the data
360 attribute set to the data given in the comment token. */
361 $comment = $this->dom->createComment($token['data']);
362 $this->dom->appendChild($comment);
364 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
365 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
367 } elseif($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
368 /* Ignore the token. */
369 $this->ignored = true;
371 /* A start tag whose tag name is "html" */
372 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] == 'html') {
373 /* Create an element for the token in the HTML namespace. Append it
374 * to the Document object. Put this element in the stack of open
376 $html = $this->insertElement($token, false);
377 $this->dom->appendChild($html);
378 $this->stack[] = $html;
380 $this->mode = self::BEFORE_HEAD;
383 /* Create an html element. Append it to the Document object. Put
384 * this element in the stack of open elements. */
385 $html = $this->dom->createElementNS(self::NS_HTML, 'html');
386 $this->dom->appendChild($html);
387 $this->stack[] = $html;
389 /* Switch the insertion mode to "before head", then reprocess the
391 $this->mode = self::BEFORE_HEAD;
392 $this->emitToken($token);
396 case self::BEFORE_HEAD:
398 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
399 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
401 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
402 /* Ignore the token. */
403 $this->ignored = true;
405 /* A comment token */
406 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
407 /* Append a Comment node to the current node with the data attribute
408 set to the data given in the comment token. */
409 $this->insertComment($token['data']);
411 /* A DOCTYPE token */
412 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
413 /* Parse error. Ignore the token */
414 $this->ignored = true;
417 /* A start tag token with the tag name "html" */
418 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
419 /* Process the token using the rules for the "in body"
421 $this->processWithRulesFor($token, self::IN_BODY);
423 /* A start tag token with the tag name "head" */
424 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') {
425 /* Insert an HTML element for the token. */
426 $element = $this->insertElement($token);
428 /* Set the head element pointer to this new element node. */
429 $this->head_pointer = $element;
431 /* Change the insertion mode to "in head". */
432 $this->mode = self::IN_HEAD;
434 /* An end tag whose tag name is one of: "head", "body", "html", "br" */
436 $token['type'] === HTML5_Tokenizer::ENDTAG && (
437 $token['name'] === 'head' || $token['name'] === 'body' ||
438 $token['name'] === 'html' || $token['name'] === 'br'
440 /* Act as if a start tag token with the tag name "head" and no
441 * attributes had been seen, then reprocess the current token. */
442 $this->emitToken(array(
444 'type' => HTML5_Tokenizer::STARTTAG,
447 $this->emitToken($token);
449 /* Any other end tag */
450 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG) {
451 /* Parse error. Ignore the token. */
452 $this->ignored = true;
455 /* Act as if a start tag token with the tag name "head" and no
456 * attributes had been seen, then reprocess the current token.
457 * Note: This will result in an empty head element being
458 * generated, with the current token being reprocessed in the
459 * "after head" insertion mode. */
460 $this->emitToken(array(
462 'type' => HTML5_Tokenizer::STARTTAG,
465 $this->emitToken($token);
471 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
472 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
474 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
475 /* Insert the character into the current node. */
476 $this->insertText($token['data']);
478 /* A comment token */
479 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
480 /* Append a Comment node to the current node with the data attribute
481 set to the data given in the comment token. */
482 $this->insertComment($token['data']);
484 /* A DOCTYPE token */
485 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
486 /* Parse error. Ignore the token. */
487 $this->ignored = true;
490 /* A start tag whose tag name is "html" */
491 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
492 $token['name'] === 'html') {
493 $this->processWithRulesFor($token, self::IN_BODY);
495 /* A start tag whose tag name is one of: "base", "command", "link" */
496 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
497 ($token['name'] === 'base' || $token['name'] === 'command' ||
498 $token['name'] === 'link')) {
499 /* Insert an HTML element for the token. Immediately pop the
500 * current node off the stack of open elements. */
501 $this->insertElement($token);
502 array_pop($this->stack);
504 // YYY: Acknowledge the token's self-closing flag, if it is set.
506 /* A start tag whose tag name is "meta" */
507 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'meta') {
508 /* Insert an HTML element for the token. Immediately pop the
509 * current node off the stack of open elements. */
510 $this->insertElement($token);
511 array_pop($this->stack);
513 // XERROR: Acknowledge the token's self-closing flag, if it is set.
515 // XENCODING: If the element has a charset attribute, and its value is a
516 // supported encoding, and the confidence is currently tentative,
517 // then change the encoding to the encoding given by the value of
518 // the charset attribute.
520 // Otherwise, if the element has a content attribute, and applying
521 // the algorithm for extracting an encoding from a Content-Type to
522 // its value returns a supported encoding encoding, and the
523 // confidence is currently tentative, then change the encoding to
524 // the encoding encoding.
526 /* A start tag with the tag name "title" */
527 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'title') {
528 $this->insertRCDATAElement($token);
530 /* A start tag whose tag name is "noscript", if the scripting flag is enabled, or
531 * A start tag whose tag name is one of: "noframes", "style" */
532 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
533 ($token['name'] === 'noscript' || $token['name'] === 'noframes' || $token['name'] === 'style')) {
534 // XSCRIPT: Scripting flag not respected
535 $this->insertCDATAElement($token);
537 // XSCRIPT: Scripting flag disable not implemented
539 /* A start tag with the tag name "script" */
540 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
541 /* 1. Create an element for the token in the HTML namespace. */
542 $node = $this->insertElement($token, false);
544 /* 2. Mark the element as being "parser-inserted" */
547 /* 3. If the parser was originally created for the HTML
548 * fragment parsing algorithm, then mark the script element as
549 * "already executed". (fragment case) */
552 /* 4. Append the new element to the current node and push it onto
553 * the stack of open elements. */
554 end($this->stack)->appendChild($node);
555 $this->stack[] = $node;
556 // I guess we could squash these together
558 /* 6. Let the original insertion mode be the current insertion mode. */
559 $this->original_mode = $this->mode;
560 /* 7. Switch the insertion mode to "in CDATA/RCDATA" */
561 $this->mode = self::IN_CDATA_RCDATA;
562 /* 5. Switch the tokeniser's content model flag to the CDATA state. */
563 $this->content_model = HTML5_Tokenizer::CDATA;
565 /* An end tag with the tag name "head" */
566 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'head') {
567 /* Pop the current node (which will be the head element) off the stack of open elements. */
568 array_pop($this->stack);
570 /* Change the insertion mode to "after head". */
571 $this->mode = self::AFTER_HEAD;
573 // Slight logic inversion here to minimize duplication
574 /* A start tag with the tag name "head". */
575 /* An end tag whose tag name is not one of: "body", "html", "br" */
576 } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') ||
577 ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] !== 'html' &&
578 $token['name'] !== 'body' && $token['name'] !== 'br')) {
579 // Parse error. Ignore the token.
580 $this->ignored = true;
584 /* Act as if an end tag token with the tag name "head" had been
585 * seen, and reprocess the current token. */
586 $this->emitToken(array(
588 'type' => HTML5_Tokenizer::ENDTAG
591 /* Then, reprocess the current token. */
592 $this->emitToken($token);
596 case self::IN_HEAD_NOSCRIPT:
597 if ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
599 } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
600 $this->processWithRulesFor($token, self::IN_BODY);
601 } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'noscript') {
602 /* Pop the current node (which will be a noscript element) from the
603 * stack of open elements; the new current node will be a head
605 array_pop($this->stack);
606 $this->mode = self::IN_HEAD;
608 ($token['type'] === HTML5_Tokenizer::SPACECHARACTER) ||
609 ($token['type'] === HTML5_Tokenizer::COMMENT) ||
610 ($token['type'] === HTML5_Tokenizer::STARTTAG && (
611 $token['name'] === 'link' || $token['name'] === 'meta' ||
612 $token['name'] === 'noframes' || $token['name'] === 'style'))) {
613 $this->processWithRulesFor($token, self::IN_HEAD);
616 ($token['type'] === HTML5_Tokenizer::STARTTAG && (
617 $token['name'] === 'head' || $token['name'] === 'noscript')) ||
618 ($token['type'] === HTML5_Tokenizer::ENDTAG &&
619 $token['name'] !== 'br')) {
623 $this->emitToken(array(
624 'type' => HTML5_Tokenizer::ENDTAG,
625 'name' => 'noscript',
627 $this->emitToken($token);
631 case self::AFTER_HEAD:
632 /* Handle the token as follows: */
634 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
635 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
637 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
638 /* Append the character to the current node. */
639 $this->insertText($token['data']);
641 /* A comment token */
642 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
643 /* Append a Comment node to the current node with the data attribute
644 set to the data given in the comment token. */
645 $this->insertComment($token['data']);
647 } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
650 } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
651 $this->processWithRulesFor($token, self::IN_BODY);
653 /* A start tag token with the tag name "body" */
654 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'body') {
655 $this->insertElement($token);
657 /* Set the frameset-ok flag to "not ok". */
658 $this->flag_frameset_ok = false;
660 /* Change the insertion mode to "in body". */
661 $this->mode = self::IN_BODY;
663 /* A start tag token with the tag name "frameset" */
664 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'frameset') {
665 /* Insert a frameset element for the token. */
666 $this->insertElement($token);
668 /* Change the insertion mode to "in frameset". */
669 $this->mode = self::IN_FRAMESET;
671 /* A start tag token whose tag name is one of: "base", "link", "meta",
672 "script", "style", "title" */
673 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
674 array('base', 'link', 'meta', 'noframes', 'script', 'style', 'title'))) {
676 /* Push the node pointed to by the head element pointer onto the
677 * stack of open elements. */
678 $this->stack[] = $this->head_pointer;
679 $this->processWithRulesFor($token, self::IN_HEAD);
680 array_splice($this->stack, array_search($this->head_pointer, $this->stack, true), 1);
682 // inversion of specification
684 ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'head') ||
685 ($token['type'] === HTML5_Tokenizer::ENDTAG &&
686 $token['name'] !== 'body' && $token['name'] !== 'html' &&
687 $token['name'] !== 'br')) {
692 $this->emitToken(array(
694 'type' => HTML5_Tokenizer::STARTTAG,
697 $this->flag_frameset_ok = true;
698 $this->emitToken($token);
703 /* Handle the token as follows: */
705 switch($token['type']) {
706 /* A character token */
707 case HTML5_Tokenizer::CHARACTER:
708 case HTML5_Tokenizer::SPACECHARACTER:
709 /* Reconstruct the active formatting elements, if any. */
710 $this->reconstructActiveFormattingElements();
712 /* Append the token's character to the current node. */
713 $this->insertText($token['data']);
715 /* If the token is not one of U+0009 CHARACTER TABULATION,
716 * U+000A LINE FEED (LF), U+000C FORM FEED (FF), or U+0020
717 * SPACE, then set the frameset-ok flag to "not ok". */
718 // i.e., if any of the characters is not whitespace
719 if (strlen($token['data']) !== strspn($token['data'], HTML5_Tokenizer::WHITESPACE)) {
720 $this->flag_frameset_ok = false;
724 /* A comment token */
725 case HTML5_Tokenizer::COMMENT:
726 /* Append a Comment node to the current node with the data
727 attribute set to the data given in the comment token. */
728 $this->insertComment($token['data']);
731 case HTML5_Tokenizer::DOCTYPE:
735 case HTML5_Tokenizer::STARTTAG:
736 switch($token['name']) {
739 /* For each attribute on the token, check to see if the
740 * attribute is already present on the top element of the
741 * stack of open elements. If it is not, add the attribute
742 * and its corresponding value to that element. */
743 foreach($token['attr'] as $attr) {
744 if(!$this->stack[0]->hasAttribute($attr['name'])) {
745 $this->stack[0]->setAttribute($attr['name'], $attr['value']);
750 case 'base': case 'command': case 'link': case 'meta': case 'noframes':
751 case 'script': case 'style': case 'title':
752 /* Process the token as if the insertion mode had been "in
754 $this->processWithRulesFor($token, self::IN_HEAD);
757 /* A start tag token with the tag name "body" */
759 /* Parse error. If the second element on the stack of open
760 elements is not a body element, or, if the stack of open
761 elements has only one node on it, then ignore the token.
763 if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
764 $this->ignored = true;
767 /* Otherwise, for each attribute on the token, check to see
768 if the attribute is already present on the body element (the
769 second element) on the stack of open elements. If it is not,
770 add the attribute and its corresponding value to that
773 foreach($token['attr'] as $attr) {
774 if(!$this->stack[1]->hasAttribute($attr['name'])) {
775 $this->stack[1]->setAttribute($attr['name'], $attr['value']);
783 /* If the second element on the stack of open elements is
784 * not a body element, or, if the stack of open elements
785 * has only one node on it, then ignore the token.
787 if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
788 $this->ignored = true;
790 } elseif (!$this->flag_frameset_ok) {
791 $this->ignored = true;
794 /* 1. Remove the second element on the stack of open
795 * elements from its parent node, if it has one. */
796 if($this->stack[1]->parentNode) {
797 $this->stack[1]->parentNode->removeChild($this->stack[1]);
800 /* 2. Pop all the nodes from the bottom of the stack of
801 * open elements, from the current node up to the root
803 array_splice($this->stack, 1);
805 $this->insertElement($token);
806 $this->mode = self::IN_FRAMESET;
810 // in spec, there is a diversion here
812 case 'address': case 'article': case 'aside': case 'blockquote':
813 case 'center': case 'datagrid': case 'details': case 'dialog': case 'dir':
814 case 'div': case 'dl': case 'fieldset': case 'figure': case 'footer':
815 case 'header': case 'hgroup': case 'menu': case 'nav':
816 case 'ol': case 'p': case 'section': case 'ul':
817 /* If the stack of open elements has a p element in scope,
818 then act as if an end tag with the tag name p had been
820 if($this->elementInScope('p')) {
821 $this->emitToken(array(
823 'type' => HTML5_Tokenizer::ENDTAG
827 /* Insert an HTML element for the token. */
828 $this->insertElement($token);
831 /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
833 case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
834 /* If the stack of open elements has a p element in scope,
835 then act as if an end tag with the tag name p had been seen. */
836 if($this->elementInScope('p')) {
837 $this->emitToken(array(
839 'type' => HTML5_Tokenizer::ENDTAG
843 /* If the current node is an element whose tag name is one
844 * of "h1", "h2", "h3", "h4", "h5", or "h6", then this is a
845 * parse error; pop the current node off the stack of open
847 $peek = array_pop($this->stack);
848 if (in_array($peek->tagName, array("h1", "h2", "h3", "h4", "h5", "h6"))) {
851 $this->stack[] = $peek;
854 /* Insert an HTML element for the token. */
855 $this->insertElement($token);
858 case 'pre': case 'listing':
859 /* If the stack of open elements has a p element in scope,
860 then act as if an end tag with the tag name p had been seen. */
861 if($this->elementInScope('p')) {
862 $this->emitToken(array(
864 'type' => HTML5_Tokenizer::ENDTAG
867 $this->insertElement($token);
868 /* If the next token is a U+000A LINE FEED (LF) character
869 * token, then ignore that token and move on to the next
870 * one. (Newlines at the start of pre blocks are ignored as
871 * an authoring convenience.) */
872 $this->ignore_lf_token = 2;
873 $this->flag_frameset_ok = false;
876 /* A start tag whose tag name is "form" */
878 /* If the form element pointer is not null, ignore the
879 token with a parse error. */
880 if($this->form_pointer !== null) {
881 $this->ignored = true;
886 /* If the stack of open elements has a p element in
887 scope, then act as if an end tag with the tag name p
889 if($this->elementInScope('p')) {
890 $this->emitToken(array(
892 'type' => HTML5_Tokenizer::ENDTAG
896 /* Insert an HTML element for the token, and set the
897 form element pointer to point to the element created. */
898 $element = $this->insertElement($token);
899 $this->form_pointer = $element;
903 // condensed specification
904 case 'li': case 'dd': case 'dt':
905 /* 1. Set the frameset-ok flag to "not ok". */
906 $this->flag_frameset_ok = false;
908 $stack_length = count($this->stack) - 1;
909 for($n = $stack_length; 0 <= $n; $n--) {
910 /* 2. Initialise node to be the current node (the
911 bottommost node of the stack). */
913 $node = $this->stack[$n];
914 $cat = $this->getElementCategory($node);
917 /* 3. If node is an li element, then act as if an end
918 * tag with the tag name "li" had been seen, then jump
919 * to the last step. */
920 // for case 'dd': case 'dt':
921 /* If node is a dd or dt element, then act as if an end
922 * tag with the same tag name as node had been seen, then
923 * jump to the last step. */
924 if(($token['name'] === 'li' && $node->tagName === 'li') ||
925 ($token['name'] !== 'li' && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { // limited conditional
926 $this->emitToken(array(
927 'type' => HTML5_Tokenizer::ENDTAG,
928 'name' => $node->tagName,
933 /* 4. If node is not in the formatting category, and is
934 not in the phrasing category, and is not an address,
935 div or p element, then stop this algorithm. */
936 if($cat !== self::FORMATTING && $cat !== self::PHRASING &&
937 $node->tagName !== 'address' && $node->tagName !== 'div' &&
938 $node->tagName !== 'p') {
942 /* 5. Otherwise, set node to the previous entry in the
943 * stack of open elements and return to step 2. */
946 /* 6. This is the last step. */
948 /* If the stack of open elements has a p element in scope,
949 then act as if an end tag with the tag name p had been
951 if($this->elementInScope('p')) {
952 $this->emitToken(array(
954 'type' => HTML5_Tokenizer::ENDTAG
958 /* Finally, insert an HTML element with the same tag
959 name as the token's. */
960 $this->insertElement($token);
963 /* A start tag token whose tag name is "plaintext" */
965 /* If the stack of open elements has a p element in scope,
966 then act as if an end tag with the tag name p had been
968 if($this->elementInScope('p')) {
969 $this->emitToken(array(
971 'type' => HTML5_Tokenizer::ENDTAG
975 /* Insert an HTML element for the token. */
976 $this->insertElement($token);
978 $this->content_model = HTML5_Tokenizer::PLAINTEXT;
983 /* A start tag whose tag name is "a" */
985 /* If the list of active formatting elements contains
986 an element whose tag name is "a" between the end of the
987 list and the last marker on the list (or the start of
988 the list if there is no marker on the list), then this
989 is a parse error; act as if an end tag with the tag name
990 "a" had been seen, then remove that element from the list
991 of active formatting elements and the stack of open
992 elements if the end tag didn't already remove it (it
993 might not have if the element is not in table scope). */
994 $leng = count($this->a_formatting);
996 for($n = $leng - 1; $n >= 0; $n--) {
997 if($this->a_formatting[$n] === self::MARKER) {
1000 } elseif($this->a_formatting[$n]->tagName === 'a') {
1001 $a = $this->a_formatting[$n];
1002 $this->emitToken(array(
1004 'type' => HTML5_Tokenizer::ENDTAG
1006 if (in_array($a, $this->a_formatting)) {
1007 $a_i = array_search($a, $this->a_formatting, true);
1008 if($a_i !== false) array_splice($this->a_formatting, $a_i, 1);
1010 if (in_array($a, $this->stack)) {
1011 $a_i = array_search($a, $this->stack, true);
1012 if ($a_i !== false) array_splice($this->stack, $a_i, 1);
1018 /* Reconstruct the active formatting elements, if any. */
1019 $this->reconstructActiveFormattingElements();
1021 /* Insert an HTML element for the token. */
1022 $el = $this->insertElement($token);
1024 /* Add that element to the list of active formatting
1026 $this->a_formatting[] = $el;
1029 case 'b': case 'big': case 'code': case 'em': case 'font': case 'i':
1030 case 's': case 'small': case 'strike':
1031 case 'strong': case 'tt': case 'u':
1032 /* Reconstruct the active formatting elements, if any. */
1033 $this->reconstructActiveFormattingElements();
1035 /* Insert an HTML element for the token. */
1036 $el = $this->insertElement($token);
1038 /* Add that element to the list of active formatting
1040 $this->a_formatting[] = $el;
1044 /* Reconstruct the active formatting elements, if any. */
1045 $this->reconstructActiveFormattingElements();
1047 /* If the stack of open elements has a nobr element in
1048 * scope, then this is a parse error; act as if an end tag
1049 * with the tag name "nobr" had been seen, then once again
1050 * reconstruct the active formatting elements, if any. */
1051 if ($this->elementInScope('nobr')) {
1052 $this->emitToken(array(
1054 'type' => HTML5_Tokenizer::ENDTAG,
1056 $this->reconstructActiveFormattingElements();
1059 /* Insert an HTML element for the token. */
1060 $el = $this->insertElement($token);
1062 /* Add that element to the list of active formatting
1064 $this->a_formatting[] = $el;
1067 // another diversion
1069 /* A start tag token whose tag name is "button" */
1071 /* If the stack of open elements has a button element in scope,
1072 then this is a parse error; act as if an end tag with the tag
1073 name "button" had been seen, then reprocess the token. (We don't
1074 do that. Unnecessary.) (I hope you're right! -- ezyang) */
1075 if($this->elementInScope('button')) {
1076 $this->emitToken(array(
1078 'type' => HTML5_Tokenizer::ENDTAG
1082 /* Reconstruct the active formatting elements, if any. */
1083 $this->reconstructActiveFormattingElements();
1085 /* Insert an HTML element for the token. */
1086 $this->insertElement($token);
1088 /* Insert a marker at the end of the list of active
1089 formatting elements. */
1090 $this->a_formatting[] = self::MARKER;
1092 $this->flag_frameset_ok = false;
1095 case 'applet': case 'marquee': case 'object':
1096 /* Reconstruct the active formatting elements, if any. */
1097 $this->reconstructActiveFormattingElements();
1099 /* Insert an HTML element for the token. */
1100 $this->insertElement($token);
1102 /* Insert a marker at the end of the list of active
1103 formatting elements. */
1104 $this->a_formatting[] = self::MARKER;
1106 $this->flag_frameset_ok = false;
1111 /* A start tag whose tag name is "table" */
1113 /* If the stack of open elements has a p element in scope,
1114 then act as if an end tag with the tag name p had been seen. */
1115 if($this->quirks_mode !== self::QUIRKS_MODE &&
1116 $this->elementInScope('p')) {
1117 $this->emitToken(array(
1119 'type' => HTML5_Tokenizer::ENDTAG
1123 /* Insert an HTML element for the token. */
1124 $this->insertElement($token);
1126 $this->flag_frameset_ok = false;
1128 /* Change the insertion mode to "in table". */
1129 $this->mode = self::IN_TABLE;
1132 /* A start tag whose tag name is one of: "area", "basefont",
1133 "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
1134 case 'area': case 'basefont': case 'bgsound': case 'br':
1135 case 'embed': case 'img': case 'input': case 'keygen': case 'spacer':
1137 /* Reconstruct the active formatting elements, if any. */
1138 $this->reconstructActiveFormattingElements();
1140 /* Insert an HTML element for the token. */
1141 $this->insertElement($token);
1143 /* Immediately pop the current node off the stack of open elements. */
1144 array_pop($this->stack);
1146 // YYY: Acknowledge the token's self-closing flag, if it is set.
1148 $this->flag_frameset_ok = false;
1151 case 'param': case 'source':
1152 /* Insert an HTML element for the token. */
1153 $this->insertElement($token);
1155 /* Immediately pop the current node off the stack of open elements. */
1156 array_pop($this->stack);
1158 // YYY: Acknowledge the token's self-closing flag, if it is set.
1161 /* A start tag whose tag name is "hr" */
1163 /* If the stack of open elements has a p element in scope,
1164 then act as if an end tag with the tag name p had been seen. */
1165 if($this->elementInScope('p')) {
1166 $this->emitToken(array(
1168 'type' => HTML5_Tokenizer::ENDTAG
1172 /* Insert an HTML element for the token. */
1173 $this->insertElement($token);
1175 /* Immediately pop the current node off the stack of open elements. */
1176 array_pop($this->stack);
1178 // YYY: Acknowledge the token's self-closing flag, if it is set.
1180 $this->flag_frameset_ok = false;
1183 /* A start tag whose tag name is "image" */
1185 /* Parse error. Change the token's tag name to "img" and
1186 reprocess it. (Don't ask.) */
1187 $token['name'] = 'img';
1188 $this->emitToken($token);
1191 /* A start tag whose tag name is "isindex" */
1195 /* If the form element pointer is not null,
1196 then ignore the token. */
1197 if($this->form_pointer === null) {
1198 /* Act as if a start tag token with the tag name "form" had
1200 /* If the token has an attribute called "action", set
1201 * the action attribute on the resulting form
1202 * element to the value of the "action" attribute of
1205 $action = $this->getAttr($token, 'action');
1206 if ($action !== false) {
1207 $attr[] = array('name' => 'action', 'value' => $action);
1209 $this->emitToken(array(
1211 'type' => HTML5_Tokenizer::STARTTAG,
1215 /* Act as if a start tag token with the tag name "hr" had
1217 $this->emitToken(array(
1219 'type' => HTML5_Tokenizer::STARTTAG,
1223 /* Act as if a start tag token with the tag name "p" had
1225 $this->emitToken(array(
1227 'type' => HTML5_Tokenizer::STARTTAG,
1231 /* Act as if a start tag token with the tag name "label"
1233 $this->emitToken(array(
1235 'type' => HTML5_Tokenizer::STARTTAG,
1239 /* Act as if a stream of character tokens had been seen. */
1240 $prompt = $this->getAttr($token, 'prompt');
1241 if ($prompt === false) {
1242 $prompt = 'This is a searchable index. '.
1243 'Insert your search keywords here: ';
1245 $this->emitToken(array(
1247 'type' => HTML5_Tokenizer::CHARACTER,
1250 /* Act as if a start tag token with the tag name "input"
1251 had been seen, with all the attributes from the "isindex"
1252 token, except with the "name" attribute set to the value
1253 "isindex" (ignoring any explicit "name" attribute). */
1255 foreach ($token['attr'] as $keypair) {
1256 if ($keypair['name'] === 'name' || $keypair['name'] === 'action' ||
1257 $keypair['name'] === 'prompt') continue;
1260 $attr[] = array('name' => 'name', 'value' => 'isindex');
1262 $this->emitToken(array(
1264 'type' => HTML5_Tokenizer::STARTTAG,
1268 /* Act as if an end tag token with the tag name "label"
1270 $this->emitToken(array(
1272 'type' => HTML5_Tokenizer::ENDTAG
1275 /* Act as if an end tag token with the tag name "p" had
1277 $this->emitToken(array(
1279 'type' => HTML5_Tokenizer::ENDTAG
1282 /* Act as if a start tag token with the tag name "hr" had
1284 $this->emitToken(array(
1286 'type' => HTML5_Tokenizer::STARTTAG
1289 /* Act as if an end tag token with the tag name "form" had
1291 $this->emitToken(array(
1293 'type' => HTML5_Tokenizer::ENDTAG
1296 $this->ignored = true;
1300 /* A start tag whose tag name is "textarea" */
1302 $this->insertElement($token);
1304 /* If the next token is a U+000A LINE FEED (LF)
1305 * character token, then ignore that token and move on to
1306 * the next one. (Newlines at the start of textarea
1307 * elements are ignored as an authoring convenience.)
1308 * need flag, see also <pre> */
1309 $this->ignore_lf_token = 2;
1311 $this->original_mode = $this->mode;
1312 $this->flag_frameset_ok = false;
1313 $this->mode = self::IN_CDATA_RCDATA;
1315 /* Switch the tokeniser's content model flag to the
1317 $this->content_model = HTML5_Tokenizer::RCDATA;
1320 /* A start tag token whose tag name is "xmp" */
1322 /* Reconstruct the active formatting elements, if any. */
1323 $this->reconstructActiveFormattingElements();
1325 $this->flag_frameset_ok = false;
1327 $this->insertCDATAElement($token);
1331 $this->flag_frameset_ok = false;
1332 $this->insertCDATAElement($token);
1335 case 'noembed': case 'noscript':
1336 // XSCRIPT: should check scripting flag
1337 $this->insertCDATAElement($token);
1340 /* A start tag whose tag name is "select" */
1342 /* Reconstruct the active formatting elements, if any. */
1343 $this->reconstructActiveFormattingElements();
1345 /* Insert an HTML element for the token. */
1346 $this->insertElement($token);
1348 $this->flag_frameset_ok = false;
1350 /* If the insertion mode is one of in table", "in caption",
1351 * "in column group", "in table body", "in row", or "in
1352 * cell", then switch the insertion mode to "in select in
1353 * table". Otherwise, switch the insertion mode to "in
1356 $this->mode === self::IN_TABLE || $this->mode === self::IN_CAPTION ||
1357 $this->mode === self::IN_COLUMN_GROUP || $this->mode ==+self::IN_TABLE_BODY ||
1358 $this->mode === self::IN_ROW || $this->mode === self::IN_CELL
1360 $this->mode = self::IN_SELECT_IN_TABLE;
1362 $this->mode = self::IN_SELECT;
1366 case 'option': case 'optgroup':
1367 if ($this->elementInScope('option')) {
1368 $this->emitToken(array(
1370 'type' => HTML5_Tokenizer::ENDTAG,
1373 $this->reconstructActiveFormattingElements();
1374 $this->insertElement($token);
1377 case 'rp': case 'rt':
1378 /* If the stack of open elements has a ruby element in scope, then generate
1379 * implied end tags. If the current node is not then a ruby element, this is
1380 * a parse error; pop all the nodes from the current node up to the node
1381 * immediately before the bottommost ruby element on the stack of open elements.
1383 if ($this->elementInScope('ruby')) {
1384 $this->generateImpliedEndTags();
1391 $peek = array_pop($this->stack);
1392 } while ($peek->tagName !== 'ruby');
1393 $this->stack[] = $peek; // we popped one too many
1394 $this->insertElement($token);
1400 $this->reconstructActiveFormattingElements();
1401 $token = $this->adjustMathMLAttributes($token);
1402 $token = $this->adjustForeignAttributes($token);
1403 $this->insertForeignElement($token, self::NS_MATHML);
1404 if (isset($token['self-closing'])) {
1405 // XERROR: acknowledge the token's self-closing flag
1406 array_pop($this->stack);
1408 if ($this->mode !== self::IN_FOREIGN_CONTENT) {
1409 $this->secondary_mode = $this->mode;
1410 $this->mode = self::IN_FOREIGN_CONTENT;
1415 $this->reconstructActiveFormattingElements();
1416 $token = $this->adjustSVGAttributes($token);
1417 $token = $this->adjustForeignAttributes($token);
1418 $this->insertForeignElement($token, self::NS_SVG);
1419 if (isset($token['self-closing'])) {
1420 // XERROR: acknowledge the token's self-closing flag
1421 array_pop($this->stack);
1423 if ($this->mode !== self::IN_FOREIGN_CONTENT) {
1424 $this->secondary_mode = $this->mode;
1425 $this->mode = self::IN_FOREIGN_CONTENT;
1429 case 'caption': case 'col': case 'colgroup': case 'frame': case 'head':
1430 case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr':
1436 /* Reconstruct the active formatting elements, if any. */
1437 $this->reconstructActiveFormattingElements();
1439 $this->insertForeignElement($token, self::NS_GOOGLE);
1442 /* A start tag token not covered by the previous entries */
1444 /* Reconstruct the active formatting elements, if any. */
1445 $this->reconstructActiveFormattingElements();
1447 $this->insertElement($token);
1448 /* This element will be a phrasing element. */
1453 case HTML5_Tokenizer::ENDTAG:
1454 switch($token['name']) {
1455 /* An end tag with the tag name "body" */
1457 /* If the second element in the stack of open elements is
1458 not a body element, this is a parse error. Ignore the token.
1460 if(count($this->stack) < 2 || $this->stack[1]->tagName !== 'body') {
1461 $this->ignored = true;
1463 /* Otherwise, if there is a node in the stack of open
1464 * elements that is not either a dd element, a dt
1465 * element, an li element, an optgroup element, an
1466 * option element, a p element, an rp element, an rt
1467 * element, a tbody element, a td element, a tfoot
1468 * element, a th element, a thead element, a tr element,
1469 * the body element, or the html element, then this is a
1472 // XERROR: implement this check for parse error
1475 /* Change the insertion mode to "after body". */
1476 $this->mode = self::AFTER_BODY;
1479 /* An end tag with the tag name "html" */
1481 /* Act as if an end tag with tag name "body" had been seen,
1482 then, if that token wasn't ignored, reprocess the current
1484 $this->emitToken(array(
1486 'type' => HTML5_Tokenizer::ENDTAG
1489 if (!$this->ignored) $this->emitToken($token);
1492 case 'address': case 'article': case 'aside': case 'blockquote':
1493 case 'center': case 'datagrid': case 'details': case 'dir':
1494 case 'div': case 'dl': case 'fieldset': case 'figure': case 'footer':
1495 case 'header': case 'hgroup': case 'listing': case 'menu':
1496 case 'nav': case 'ol': case 'pre': case 'section': case 'ul':
1497 /* If the stack of open elements has an element in scope
1498 with the same tag name as that of the token, then generate
1499 implied end tags. */
1500 if($this->elementInScope($token['name'])) {
1501 $this->generateImpliedEndTags();
1503 /* Now, if the current node is not an element with
1504 the same tag name as that of the token, then this
1505 is a parse error. */
1506 // XERROR: implement parse error logic
1508 /* If the stack of open elements has an element in
1509 scope with the same tag name as that of the token,
1510 then pop elements from this stack until an element
1511 with that tag name has been popped from the stack. */
1513 $node = array_pop($this->stack);
1514 } while ($node->tagName !== $token['name']);
1520 /* An end tag whose tag name is "form" */
1522 /* Let node be the element that the form element pointer is set to. */
1523 $node = $this->form_pointer;
1524 /* Set the form element pointer to null. */
1525 $this->form_pointer = null;
1526 /* If node is null or the stack of open elements does not
1527 * have node in scope, then this is a parse error; ignore the token. */
1528 if ($node === null || !in_array($node, $this->stack)) {
1530 $this->ignored = true;
1532 /* 1. Generate implied end tags. */
1533 $this->generateImpliedEndTags();
1534 /* 2. If the current node is not node, then this is a parse error. */
1535 if (end($this->stack) !== $node) {
1538 /* 3. Remove node from the stack of open elements. */
1539 array_splice($this->stack, array_search($node, $this->stack, true), 1);
1544 /* An end tag whose tag name is "p" */
1546 /* If the stack of open elements has a p element in scope,
1547 then generate implied end tags, except for p elements. */
1548 if($this->elementInScope('p')) {
1549 /* Generate implied end tags, except for elements with
1550 * the same tag name as the token. */
1551 $this->generateImpliedEndTags(array('p'));
1553 /* If the current node is not a p element, then this is
1555 // XERROR: implement
1557 /* Pop elements from the stack of open elements until
1558 * an element with the same tag name as the token has
1559 * been popped from the stack. */
1561 $node = array_pop($this->stack);
1562 } while ($node->tagName !== 'p');
1566 $this->emitToken(array(
1568 'type' => HTML5_Tokenizer::STARTTAG,
1570 $this->emitToken($token);
1574 /* An end tag whose tag name is "dd", "dt", or "li" */
1575 case 'dd': case 'dt': case 'li':
1576 if($this->elementInScope($token['name'])) {
1577 $this->generateImpliedEndTags(array($token['name']));
1579 /* If the current node is not an element with the same
1580 tag name as the token, then this is a parse error. */
1581 // XERROR: implement parse error
1583 /* Pop elements from the stack of open elements until
1584 * an element with the same tag name as the token has
1585 * been popped from the stack. */
1587 $node = array_pop($this->stack);
1588 } while ($node->tagName !== $token['name']);
1595 /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
1597 case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
1598 $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
1600 /* If the stack of open elements has in scope an element whose
1601 tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
1602 generate implied end tags. */
1603 if($this->elementInScope($elements)) {
1604 $this->generateImpliedEndTags();
1606 /* Now, if the current node is not an element with the same
1607 tag name as that of the token, then this is a parse error. */
1608 // XERROR: implement parse error
1610 /* If the stack of open elements has in scope an element
1611 whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
1612 "h6", then pop elements from the stack until an element
1613 with one of those tag names has been popped from the stack. */
1615 $node = array_pop($this->stack);
1616 } while (!in_array($node->tagName, $elements));
1622 /* An end tag whose tag name is one of: "a", "b", "big", "em",
1623 "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
1624 case 'a': case 'b': case 'big': case 'code': case 'em': case 'font':
1625 case 'i': case 'nobr': case 's': case 'small': case 'strike':
1626 case 'strong': case 'tt': case 'u':
1627 // XERROR: generally speaking this needs parse error logic
1628 /* 1. Let the formatting element be the last element in
1629 the list of active formatting elements that:
1630 * is between the end of the list and the last scope
1631 marker in the list, if any, or the start of the list
1633 * has the same tag name as the token.
1636 for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
1637 if($this->a_formatting[$a] === self::MARKER) {
1640 } elseif($this->a_formatting[$a]->tagName === $token['name']) {
1641 $formatting_element = $this->a_formatting[$a];
1642 $in_stack = in_array($formatting_element, $this->stack, true);
1648 /* If there is no such node, or, if that node is
1649 also in the stack of open elements but the element
1650 is not in scope, then this is a parse error. Abort
1651 these steps. The token is ignored. */
1652 if(!isset($formatting_element) || ($in_stack &&
1653 !$this->elementInScope($token['name']))) {
1654 $this->ignored = true;
1657 /* Otherwise, if there is such a node, but that node
1658 is not in the stack of open elements, then this is a
1659 parse error; remove the element from the list, and
1660 abort these steps. */
1661 } elseif(isset($formatting_element) && !$in_stack) {
1662 unset($this->a_formatting[$fe_af_pos]);
1663 $this->a_formatting = array_merge($this->a_formatting);
1667 /* Otherwise, there is a formatting element and that
1668 * element is in the stack and is in scope. If the
1669 * element is not the current node, this is a parse
1670 * error. In any case, proceed with the algorithm as
1671 * written in the following steps. */
1672 // XERROR: implement me
1674 /* 2. Let the furthest block be the topmost node in the
1675 stack of open elements that is lower in the stack
1676 than the formatting element, and is not an element in
1677 the phrasing or formatting categories. There might
1679 $fe_s_pos = array_search($formatting_element, $this->stack, true);
1680 $length = count($this->stack);
1682 for($s = $fe_s_pos + 1; $s < $length; $s++) {
1683 $category = $this->getElementCategory($this->stack[$s]);
1685 if($category !== self::PHRASING && $category !== self::FORMATTING) {
1686 $furthest_block = $this->stack[$s];
1691 /* 3. If there is no furthest block, then the UA must
1692 skip the subsequent steps and instead just pop all
1693 the nodes from the bottom of the stack of open
1694 elements, from the current node up to the formatting
1695 element, and remove the formatting element from the
1696 list of active formatting elements. */
1697 if(!isset($furthest_block)) {
1698 for($n = $length - 1; $n >= $fe_s_pos; $n--) {
1699 array_pop($this->stack);
1702 unset($this->a_formatting[$fe_af_pos]);
1703 $this->a_formatting = array_merge($this->a_formatting);
1707 /* 4. Let the common ancestor be the element
1708 immediately above the formatting element in the stack
1709 of open elements. */
1710 $common_ancestor = $this->stack[$fe_s_pos - 1];
1712 /* 5. Let a bookmark note the position of the
1713 formatting element in the list of active formatting
1714 elements relative to the elements on either side
1715 of it in the list. */
1716 $bookmark = $fe_af_pos;
1718 /* 6. Let node and last node be the furthest block.
1719 Follow these steps: */
1720 $node = $furthest_block;
1721 $last_node = $furthest_block;
1724 for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
1725 /* 6.1 Let node be the element immediately
1726 prior to node in the stack of open elements. */
1727 $node = $this->stack[$n];
1729 /* 6.2 If node is not in the list of active
1730 formatting elements, then remove node from
1731 the stack of open elements and then go back
1733 if(!in_array($node, $this->a_formatting, true)) {
1734 array_splice($this->stack, $n, 1);
1741 /* 6.3 Otherwise, if node is the formatting
1742 element, then go to the next step in the overall
1744 if($node === $formatting_element) {
1747 /* 6.4 Otherwise, if last node is the furthest
1748 block, then move the aforementioned bookmark to
1749 be immediately after the node in the list of
1750 active formatting elements. */
1751 } elseif($last_node === $furthest_block) {
1752 $bookmark = array_search($node, $this->a_formatting, true) + 1;
1755 /* 6.5 Create an element for the token for which
1756 * the element node was created, replace the entry
1757 * for node in the list of active formatting
1758 * elements with an entry for the new element,
1759 * replace the entry for node in the stack of open
1760 * elements with an entry for the new element, and
1761 * let node be the new element. */
1762 // we don't know what the token is anymore
1763 $clone = $node->cloneNode();
1764 $a_pos = array_search($node, $this->a_formatting, true);
1765 $s_pos = array_search($node, $this->stack, true);
1766 $this->a_formatting[$a_pos] = $clone;
1767 $this->stack[$s_pos] = $clone;
1770 /* 6.6 Insert last node into node, first removing
1771 it from its previous parent node if any. */
1772 if($last_node->parentNode !== null) {
1773 $last_node->parentNode->removeChild($last_node);
1776 $node->appendChild($last_node);
1778 /* 6.7 Let last node be node. */
1781 /* 6.8 Return to step 1 of this inner set of steps. */
1784 /* 7. If the common ancestor node is a table, tbody,
1785 * tfoot, thead, or tr element, then, foster parent
1786 * whatever last node ended up being in the previous
1787 * step, first removing it from its previous parent
1789 if ($last_node->parentNode) { // common step
1790 $last_node->parentNode->removeChild($last_node);
1792 if (in_array($common_ancestor->tagName, array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
1793 $this->fosterParent($last_node);
1794 /* Otherwise, append whatever last node ended up being
1795 * in the previous step to the common ancestor node,
1796 * first removing it from its previous parent node if
1799 $common_ancestor->appendChild($last_node);
1802 /* 8. Create an element for the token for which the
1803 * formatting element was created. */
1804 $clone = $formatting_element->cloneNode();
1806 /* 9. Take all of the child nodes of the furthest
1807 block and append them to the element created in the
1809 while($furthest_block->hasChildNodes()) {
1810 $child = $furthest_block->firstChild;
1811 $furthest_block->removeChild($child);
1812 $clone->appendChild($child);
1815 /* 10. Append that clone to the furthest block. */
1816 $furthest_block->appendChild($clone);
1818 /* 11. Remove the formatting element from the list
1819 of active formatting elements, and insert the new element
1820 into the list of active formatting elements at the
1821 position of the aforementioned bookmark. */
1822 $fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
1823 array_splice($this->a_formatting, $fe_af_pos, 1);
1825 $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
1826 $af_part2 = array_slice($this->a_formatting, $bookmark);
1827 $this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
1829 /* 12. Remove the formatting element from the stack
1830 of open elements, and insert the new element into the stack
1831 of open elements immediately below the position of the
1832 furthest block in that stack. */
1833 $fe_s_pos = array_search($formatting_element, $this->stack, true);
1834 array_splice($this->stack, $fe_s_pos, 1);
1836 $fb_s_pos = array_search($furthest_block, $this->stack, true);
1837 $s_part1 = array_slice($this->stack, 0, $fb_s_pos + 1);
1838 $s_part2 = array_slice($this->stack, $fb_s_pos + 1);
1839 $this->stack = array_merge($s_part1, array($clone), $s_part2);
1841 /* 13. Jump back to step 1 in this series of steps. */
1842 unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
1846 case 'applet': case 'button': case 'marquee': case 'object':
1847 /* If the stack of open elements has an element in scope whose
1848 tag name matches the tag name of the token, then generate implied
1850 if($this->elementInScope($token['name'])) {
1851 $this->generateImpliedEndTags();
1853 /* Now, if the current node is not an element with the same
1854 tag name as the token, then this is a parse error. */
1855 // XERROR: implement logic
1857 /* Pop elements from the stack of open elements until
1858 * an element with the same tag name as the token has
1859 * been popped from the stack. */
1861 $node = array_pop($this->stack);
1862 } while ($node->tagName !== $token['name']);
1864 /* Clear the list of active formatting elements up to the
1866 $keys = array_keys($this->a_formatting, self::MARKER, true);
1867 $marker = end($keys);
1869 for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
1870 array_pop($this->a_formatting);
1879 $this->emitToken(array(
1881 'type' => HTML5_Tokenizer::STARTTAG,
1885 /* An end tag token not covered by the previous entries */
1887 for($n = count($this->stack) - 1; $n >= 0; $n--) {
1888 /* Initialise node to be the current node (the bottommost
1889 node of the stack). */
1890 $node = $this->stack[$n];
1892 /* If node has the same tag name as the end tag token,
1894 if($token['name'] === $node->tagName) {
1895 /* Generate implied end tags. */
1896 $this->generateImpliedEndTags();
1898 /* If the tag name of the end tag token does not
1899 match the tag name of the current node, this is a
1901 // XERROR: implement this
1903 /* Pop all the nodes from the current node up to
1904 node, including node, then stop these steps. */
1907 $pop = array_pop($this->stack);
1908 } while ($pop !== $node);
1912 $category = $this->getElementCategory($node);
1914 if($category !== self::FORMATTING && $category !== self::PHRASING) {
1915 /* Otherwise, if node is in neither the formatting
1916 category nor the phrasing category, then this is a
1917 parse error. Stop this algorithm. The end tag token
1919 $this->ignored = true;
1924 /* Set node to the previous entry in the stack of open elements. Loop. */
1932 case self::IN_CDATA_RCDATA:
1934 $token['type'] === HTML5_Tokenizer::CHARACTER ||
1935 $token['type'] === HTML5_Tokenizer::SPACECHARACTER
1937 $this->insertText($token['data']);
1938 } elseif ($token['type'] === HTML5_Tokenizer::EOF) {
1940 /* If the current node is a script element, mark the script
1941 * element as "already executed". */
1942 // probably not necessary
1943 array_pop($this->stack);
1944 $this->mode = $this->original_mode;
1945 $this->emitToken($token);
1946 } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'script') {
1947 array_pop($this->stack);
1948 $this->mode = $this->original_mode;
1949 // we're ignoring all of the execution stuff
1950 } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG) {
1951 array_pop($this->stack);
1952 $this->mode = $this->original_mode;
1956 case self::IN_TABLE:
1957 $clear = array('html', 'table');
1959 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
1960 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
1962 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER &&
1963 /* If the current table is tainted, then act as described in
1964 * the "anything else" entry below. */
1965 // Note: hsivonen has a test that fails due to this line
1966 // because he wants to convince Hixie not to do taint
1967 !$this->currentTableIsTainted()) {
1968 /* Append the character to the current node. */
1969 $this->insertText($token['data']);
1971 /* A comment token */
1972 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
1973 /* Append a Comment node to the current node with the data
1974 attribute set to the data given in the comment token. */
1975 $this->insertComment($token['data']);
1977 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
1980 /* A start tag whose tag name is "caption" */
1981 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
1982 $token['name'] === 'caption') {
1983 /* Clear the stack back to a table context. */
1984 $this->clearStackToTableContext($clear);
1986 /* Insert a marker at the end of the list of active
1987 formatting elements. */
1988 $this->a_formatting[] = self::MARKER;
1990 /* Insert an HTML element for the token, then switch the
1991 insertion mode to "in caption". */
1992 $this->insertElement($token);
1993 $this->mode = self::IN_CAPTION;
1995 /* A start tag whose tag name is "colgroup" */
1996 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
1997 $token['name'] === 'colgroup') {
1998 /* Clear the stack back to a table context. */
1999 $this->clearStackToTableContext($clear);
2001 /* Insert an HTML element for the token, then switch the
2002 insertion mode to "in column group". */
2003 $this->insertElement($token);
2004 $this->mode = self::IN_COLUMN_GROUP;
2006 /* A start tag whose tag name is "col" */
2007 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2008 $token['name'] === 'col') {
2009 $this->emitToken(array(
2010 'name' => 'colgroup',
2011 'type' => HTML5_Tokenizer::STARTTAG,
2015 $this->emitToken($token);
2017 /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
2018 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
2019 array('tbody', 'tfoot', 'thead'))) {
2020 /* Clear the stack back to a table context. */
2021 $this->clearStackToTableContext($clear);
2023 /* Insert an HTML element for the token, then switch the insertion
2024 mode to "in table body". */
2025 $this->insertElement($token);
2026 $this->mode = self::IN_TABLE_BODY;
2028 /* A start tag whose tag name is one of: "td", "th", "tr" */
2029 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2030 in_array($token['name'], array('td', 'th', 'tr'))) {
2031 /* Act as if a start tag token with the tag name "tbody" had been
2032 seen, then reprocess the current token. */
2033 $this->emitToken(array(
2035 'type' => HTML5_Tokenizer::STARTTAG,
2039 $this->emitToken($token);
2041 /* A start tag whose tag name is "table" */
2042 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2043 $token['name'] === 'table') {
2044 /* Parse error. Act as if an end tag token with the tag name "table"
2045 had been seen, then, if that token wasn't ignored, reprocess the
2047 $this->emitToken(array(
2049 'type' => HTML5_Tokenizer::ENDTAG
2052 if (!$this->ignored) $this->emitToken($token);
2054 /* An end tag whose tag name is "table" */
2055 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2056 $token['name'] === 'table') {
2057 /* If the stack of open elements does not have an element in table
2058 scope with the same tag name as the token, this is a parse error.
2059 Ignore the token. (fragment case) */
2060 if(!$this->elementInScope($token['name'], true)) {
2061 $this->ignored = true;
2066 $node = array_pop($this->stack);
2067 } while ($node->tagName !== 'table');
2069 /* Reset the insertion mode appropriately. */
2070 $this->resetInsertionMode();
2073 /* An end tag whose tag name is one of: "body", "caption", "col",
2074 "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
2075 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
2076 array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
2077 'tfoot', 'th', 'thead', 'tr'))) {
2078 // Parse error. Ignore the token.
2080 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2081 ($token['name'] === 'style' || $token['name'] === 'script')) {
2082 $this->processWithRulesFor($token, self::IN_HEAD);
2084 } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'input' &&
2085 // assignment is intentional
2086 /* If the token does not have an attribute with the name "type", or
2087 * if it does, but that attribute's value is not an ASCII
2088 * case-insensitive match for the string "hidden", then: act as
2089 * described in the "anything else" entry below. */
2090 ($type = $this->getAttr($token, 'type')) && strtolower($type) === 'hidden') {
2091 // I.e., if its an input with the type attribute == 'hidden'
2094 $this->insertElement($token);
2095 array_pop($this->stack);
2096 } elseif ($token['type'] === HTML5_Tokenizer::EOF) {
2097 /* If the current node is not the root html element, then this is a parse error. */
2098 if (end($this->stack)->tagName !== 'html') {
2099 // Note: It can only be the current node in the fragment case.
2105 /* Parse error. Process the token as if the insertion mode was "in
2106 body", with the following exception: */
2108 $old = $this->foster_parent;
2109 $this->foster_parent = true;
2110 $this->processWithRulesFor($token, self::IN_BODY);
2111 $this->foster_parent = $old;
2115 case self::IN_CAPTION:
2116 /* An end tag whose tag name is "caption" */
2117 if($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'caption') {
2118 /* If the stack of open elements does not have an element in table
2119 scope with the same tag name as the token, this is a parse error.
2120 Ignore the token. (fragment case) */
2121 if(!$this->elementInScope($token['name'], true)) {
2122 $this->ignored = true;
2127 /* Generate implied end tags. */
2128 $this->generateImpliedEndTags();
2130 /* Now, if the current node is not a caption element, then this
2131 is a parse error. */
2132 // XERROR: implement
2134 /* Pop elements from this stack until a caption element has
2135 been popped from the stack. */
2137 $node = array_pop($this->stack);
2138 } while ($node->tagName !== 'caption');
2140 /* Clear the list of active formatting elements up to the last
2142 $this->clearTheActiveFormattingElementsUpToTheLastMarker();
2144 /* Switch the insertion mode to "in table". */
2145 $this->mode = self::IN_TABLE;
2148 /* A start tag whose tag name is one of: "caption", "col", "colgroup",
2149 "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
2151 } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
2152 array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
2153 'thead', 'tr'))) || ($token['type'] === HTML5_Tokenizer::ENDTAG &&
2154 $token['name'] === 'table')) {
2155 /* Parse error. Act as if an end tag with the tag name "caption"
2156 had been seen, then, if that token wasn't ignored, reprocess the
2158 $this->emitToken(array(
2159 'name' => 'caption',
2160 'type' => HTML5_Tokenizer::ENDTAG
2163 if (!$this->ignored) $this->emitToken($token);
2165 /* An end tag whose tag name is one of: "body", "col", "colgroup",
2166 "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
2167 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
2168 array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
2170 // Parse error. Ignore the token.
2171 $this->ignored = true;
2175 /* Process the token as if the insertion mode was "in body". */
2176 $this->processWithRulesFor($token, self::IN_BODY);
2180 case self::IN_COLUMN_GROUP:
2181 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
2182 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
2184 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2185 /* Append the character to the current node. */
2186 $this->insertText($token['data']);
2188 /* A comment token */
2189 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
2190 /* Append a Comment node to the current node with the data
2191 attribute set to the data given in the comment token. */
2192 $this->insertToken($token['data']);
2194 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2197 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2198 $this->processWithRulesFor($token, self::IN_BODY);
2200 /* A start tag whose tag name is "col" */
2201 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'col') {
2202 /* Insert a col element for the token. Immediately pop the current
2203 node off the stack of open elements. */
2204 $this->insertElement($token);
2205 array_pop($this->stack);
2206 // XERROR: Acknowledge the token's self-closing flag, if it is set.
2208 /* An end tag whose tag name is "colgroup" */
2209 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2210 $token['name'] === 'colgroup') {
2211 /* If the current node is the root html element, then this is a
2212 parse error, ignore the token. (fragment case) */
2213 if(end($this->stack)->tagName === 'html') {
2214 $this->ignored = true;
2216 /* Otherwise, pop the current node (which will be a colgroup
2217 element) from the stack of open elements. Switch the insertion
2218 mode to "in table". */
2220 array_pop($this->stack);
2221 $this->mode = self::IN_TABLE;
2224 /* An end tag whose tag name is "col" */
2225 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'col') {
2226 /* Parse error. Ignore the token. */
2227 $this->ignored = true;
2229 /* An end-of-file token */
2230 /* If the current node is the root html element */
2231 } elseif($token['type'] === HTML5_Tokenizer::EOF && end($this->stack)->tagName === 'html') {
2236 /* Act as if an end tag with the tag name "colgroup" had been seen,
2237 and then, if that token wasn't ignored, reprocess the current token. */
2238 $this->emitToken(array(
2239 'name' => 'colgroup',
2240 'type' => HTML5_Tokenizer::ENDTAG
2243 if (!$this->ignored) $this->emitToken($token);
2247 case self::IN_TABLE_BODY:
2248 $clear = array('tbody', 'tfoot', 'thead', 'html');
2250 /* A start tag whose tag name is "tr" */
2251 if($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'tr') {
2252 /* Clear the stack back to a table body context. */
2253 $this->clearStackToTableContext($clear);
2255 /* Insert a tr element for the token, then switch the insertion
2256 mode to "in row". */
2257 $this->insertElement($token);
2258 $this->mode = self::IN_ROW;
2260 /* A start tag whose tag name is one of: "th", "td" */
2261 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2262 ($token['name'] === 'th' || $token['name'] === 'td')) {
2263 /* Parse error. Act as if a start tag with the tag name "tr" had
2264 been seen, then reprocess the current token. */
2265 $this->emitToken(array(
2267 'type' => HTML5_Tokenizer::STARTTAG,
2271 $this->emitToken($token);
2273 /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
2274 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2275 in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
2276 /* If the stack of open elements does not have an element in table
2277 scope with the same tag name as the token, this is a parse error.
2278 Ignore the token. */
2279 if(!$this->elementInScope($token['name'], true)) {
2281 $this->ignored = true;
2285 /* Clear the stack back to a table body context. */
2286 $this->clearStackToTableContext($clear);
2288 /* Pop the current node from the stack of open elements. Switch
2289 the insertion mode to "in table". */
2290 array_pop($this->stack);
2291 $this->mode = self::IN_TABLE;
2294 /* A start tag whose tag name is one of: "caption", "col", "colgroup",
2295 "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
2296 } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
2297 array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead'))) ||
2298 ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
2299 /* If the stack of open elements does not have a tbody, thead, or
2300 tfoot element in table scope, this is a parse error. Ignore the
2301 token. (fragment case) */
2302 if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) {
2304 $this->ignored = true;
2308 /* Clear the stack back to a table body context. */
2309 $this->clearStackToTableContext($clear);
2311 /* Act as if an end tag with the same tag name as the current
2312 node ("tbody", "tfoot", or "thead") had been seen, then
2313 reprocess the current token. */
2314 $this->emitToken(array(
2315 'name' => end($this->stack)->tagName,
2316 'type' => HTML5_Tokenizer::ENDTAG
2319 $this->emitToken($token);
2322 /* An end tag whose tag name is one of: "body", "caption", "col",
2323 "colgroup", "html", "td", "th", "tr" */
2324 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
2325 array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
2326 /* Parse error. Ignore the token. */
2327 $this->ignored = true;
2331 /* Process the token as if the insertion mode was "in table". */
2332 $this->processWithRulesFor($token, self::IN_TABLE);
2337 $clear = array('tr', 'html');
2339 /* A start tag whose tag name is one of: "th", "td" */
2340 if($token['type'] === HTML5_Tokenizer::STARTTAG &&
2341 ($token['name'] === 'th' || $token['name'] === 'td')) {
2342 /* Clear the stack back to a table row context. */
2343 $this->clearStackToTableContext($clear);
2345 /* Insert an HTML element for the token, then switch the insertion
2346 mode to "in cell". */
2347 $this->insertElement($token);
2348 $this->mode = self::IN_CELL;
2350 /* Insert a marker at the end of the list of active formatting
2352 $this->a_formatting[] = self::MARKER;
2354 /* An end tag whose tag name is "tr" */
2355 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'tr') {
2356 /* If the stack of open elements does not have an element in table
2357 scope with the same tag name as the token, this is a parse error.
2358 Ignore the token. (fragment case) */
2359 if(!$this->elementInScope($token['name'], true)) {
2361 $this->ignored = true;
2365 /* Clear the stack back to a table row context. */
2366 $this->clearStackToTableContext($clear);
2368 /* Pop the current node (which will be a tr element) from the
2369 stack of open elements. Switch the insertion mode to "in table
2371 array_pop($this->stack);
2372 $this->mode = self::IN_TABLE_BODY;
2375 /* A start tag whose tag name is one of: "caption", "col", "colgroup",
2376 "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
2377 } elseif(($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
2378 array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) ||
2379 ($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'table')) {
2380 /* Act as if an end tag with the tag name "tr" had been seen, then,
2381 if that token wasn't ignored, reprocess the current token. */
2382 $this->emitToken(array(
2384 'type' => HTML5_Tokenizer::ENDTAG
2386 if (!$this->ignored) $this->emitToken($token);
2388 /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
2389 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2390 in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
2391 /* If the stack of open elements does not have an element in table
2392 scope with the same tag name as the token, this is a parse error.
2393 Ignore the token. */
2394 if(!$this->elementInScope($token['name'], true)) {
2395 $this->ignored = true;
2399 /* Otherwise, act as if an end tag with the tag name "tr" had
2400 been seen, then reprocess the current token. */
2401 $this->emitToken(array(
2403 'type' => HTML5_Tokenizer::ENDTAG
2406 $this->emitToken($token);
2409 /* An end tag whose tag name is one of: "body", "caption", "col",
2410 "colgroup", "html", "td", "th" */
2411 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
2412 array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th'))) {
2413 /* Parse error. Ignore the token. */
2414 $this->ignored = true;
2418 /* Process the token as if the insertion mode was "in table". */
2419 $this->processWithRulesFor($token, self::IN_TABLE);
2424 /* An end tag whose tag name is one of: "td", "th" */
2425 if($token['type'] === HTML5_Tokenizer::ENDTAG &&
2426 ($token['name'] === 'td' || $token['name'] === 'th')) {
2427 /* If the stack of open elements does not have an element in table
2428 scope with the same tag name as that of the token, then this is a
2429 parse error and the token must be ignored. */
2430 if(!$this->elementInScope($token['name'], true)) {
2431 $this->ignored = true;
2435 /* Generate implied end tags, except for elements with the same
2436 tag name as the token. */
2437 $this->generateImpliedEndTags(array($token['name']));
2439 /* Now, if the current node is not an element with the same tag
2440 name as the token, then this is a parse error. */
2441 // XERROR: Implement parse error code
2443 /* Pop elements from this stack until an element with the same
2444 tag name as the token has been popped from the stack. */
2446 $node = array_pop($this->stack);
2447 } while ($node->tagName !== $token['name']);
2449 /* Clear the list of active formatting elements up to the last
2451 $this->clearTheActiveFormattingElementsUpToTheLastMarker();
2453 /* Switch the insertion mode to "in row". (The current node
2454 will be a tr element at this point.) */
2455 $this->mode = self::IN_ROW;
2458 /* A start tag whose tag name is one of: "caption", "col", "colgroup",
2459 "tbody", "td", "tfoot", "th", "thead", "tr" */
2460 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && in_array($token['name'],
2461 array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
2463 /* If the stack of open elements does not have a td or th element
2464 in table scope, then this is a parse error; ignore the token.
2466 if(!$this->elementInScope(array('td', 'th'), true)) {
2468 $this->ignored = true;
2470 /* Otherwise, close the cell (see below) and reprocess the current
2474 $this->emitToken($token);
2477 /* An end tag whose tag name is one of: "body", "caption", "col",
2478 "colgroup", "html" */
2479 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
2480 array('body', 'caption', 'col', 'colgroup', 'html'))) {
2481 /* Parse error. Ignore the token. */
2482 $this->ignored = true;
2484 /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
2486 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && in_array($token['name'],
2487 array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
2488 /* If the stack of open elements does not have a td or th element
2489 in table scope, then this is a parse error; ignore the token.
2491 if(!$this->elementInScope(array('td', 'th'), true)) {
2493 $this->ignored = true;
2495 /* Otherwise, close the cell (see below) and reprocess the current
2499 $this->emitToken($token);
2504 /* Process the token as if the insertion mode was "in body". */
2505 $this->processWithRulesFor($token, self::IN_BODY);
2509 case self::IN_SELECT:
2510 /* Handle the token as follows: */
2512 /* A character token */
2514 $token['type'] === HTML5_Tokenizer::CHARACTER ||
2515 $token['type'] === HTML5_Tokenizer::SPACECHARACTER
2517 /* Append the token's character to the current node. */
2518 $this->insertText($token['data']);
2520 /* A comment token */
2521 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
2522 /* Append a Comment node to the current node with the data
2523 attribute set to the data given in the comment token. */
2524 $this->insertComment($token['data']);
2526 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2529 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2530 $this->processWithRulesFor($token, self::INBODY);
2532 /* A start tag token whose tag name is "option" */
2533 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2534 $token['name'] === 'option') {
2535 /* If the current node is an option element, act as if an end tag
2536 with the tag name "option" had been seen. */
2537 if(end($this->stack)->tagName === 'option') {
2538 $this->emitToken(array(
2540 'type' => HTML5_Tokenizer::ENDTAG
2544 /* Insert an HTML element for the token. */
2545 $this->insertElement($token);
2547 /* A start tag token whose tag name is "optgroup" */
2548 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2549 $token['name'] === 'optgroup') {
2550 /* If the current node is an option element, act as if an end tag
2551 with the tag name "option" had been seen. */
2552 if(end($this->stack)->tagName === 'option') {
2553 $this->emitToken(array(
2555 'type' => HTML5_Tokenizer::ENDTAG
2559 /* If the current node is an optgroup element, act as if an end tag
2560 with the tag name "optgroup" had been seen. */
2561 if(end($this->stack)->tagName === 'optgroup') {
2562 $this->emitToken(array(
2563 'name' => 'optgroup',
2564 'type' => HTML5_Tokenizer::ENDTAG
2568 /* Insert an HTML element for the token. */
2569 $this->insertElement($token);
2571 /* An end tag token whose tag name is "optgroup" */
2572 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2573 $token['name'] === 'optgroup') {
2574 /* First, if the current node is an option element, and the node
2575 immediately before it in the stack of open elements is an optgroup
2576 element, then act as if an end tag with the tag name "option" had
2578 $elements_in_stack = count($this->stack);
2580 if($this->stack[$elements_in_stack - 1]->tagName === 'option' &&
2581 $this->stack[$elements_in_stack - 2]->tagName === 'optgroup') {
2582 $this->emitToken(array(
2584 'type' => HTML5_Tokenizer::ENDTAG
2588 /* If the current node is an optgroup element, then pop that node
2589 from the stack of open elements. Otherwise, this is a parse error,
2590 ignore the token. */
2591 if(end($this->stack)->tagName === 'optgroup') {
2592 array_pop($this->stack);
2595 $this->ignored = true;
2598 /* An end tag token whose tag name is "option" */
2599 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2600 $token['name'] === 'option') {
2601 /* If the current node is an option element, then pop that node
2602 from the stack of open elements. Otherwise, this is a parse error,
2603 ignore the token. */
2604 if(end($this->stack)->tagName === 'option') {
2605 array_pop($this->stack);
2608 $this->ignored = true;
2611 /* An end tag whose tag name is "select" */
2612 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2613 $token['name'] === 'select') {
2614 /* If the stack of open elements does not have an element in table
2615 scope with the same tag name as the token, this is a parse error.
2616 Ignore the token. (fragment case) */
2617 if(!$this->elementInScope($token['name'], true)) {
2618 $this->ignored = true;
2623 /* Pop elements from the stack of open elements until a select
2624 element has been popped from the stack. */
2626 $node = array_pop($this->stack);
2627 } while ($node->tagName !== 'select');
2629 /* Reset the insertion mode appropriately. */
2630 $this->resetInsertionMode();
2633 /* A start tag whose tag name is "select" */
2634 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'select') {
2635 /* Parse error. Act as if the token had been an end tag with the
2636 tag name "select" instead. */
2637 $this->emitToken(array(
2639 'type' => HTML5_Tokenizer::ENDTAG
2642 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2643 ($token['name'] === 'input' || $token['name'] === 'textarea')) {
2645 $this->emitToken(array(
2647 'type' => HTML5_Tokenizer::ENDTAG
2649 $this->emitToken($token);
2651 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
2652 $this->processWithRulesFor($token, self::IN_HEAD);
2654 } elseif($token['type'] === HTML5_Tokenizer::EOF) {
2655 // XERROR: If the current node is not the root html element, then this is a parse error.
2660 /* Parse error. Ignore the token. */
2661 $this->ignored = true;
2665 case self::IN_SELECT_IN_TABLE:
2667 if($token['type'] === HTML5_Tokenizer::STARTTAG &&
2668 in_array($token['name'], array('caption', 'table', 'tbody',
2669 'tfoot', 'thead', 'tr', 'td', 'th'))) {
2671 $this->emitToken(array(
2673 'type' => HTML5_Tokenizer::ENDTAG,
2675 $this->emitToken($token);
2677 /* An end tag whose tag name is one of: "caption", "table", "tbody",
2678 "tfoot", "thead", "tr", "td", "th" */
2679 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2680 in_array($token['name'], array('caption', 'table', 'tbody', 'tfoot', 'thead', 'tr', 'td', 'th'))) {
2684 /* If the stack of open elements has an element in table scope with
2685 the same tag name as that of the token, then act as if an end tag
2686 with the tag name "select" had been seen, and reprocess the token.
2687 Otherwise, ignore the token. */
2688 if($this->elementInScope($token['name'], true)) {
2689 $this->emitToken(array(
2691 'type' => HTML5_Tokenizer::ENDTAG
2694 $this->emitToken($token);
2696 $this->ignored = true;
2699 $this->processWithRulesFor($token, self::IN_SELECT);
2703 case self::IN_FOREIGN_CONTENT:
2704 if ($token['type'] === HTML5_Tokenizer::CHARACTER ||
2705 $token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2706 $this->insertText($token['data']);
2707 } elseif ($token['type'] === HTML5_Tokenizer::COMMENT) {
2708 $this->insertComment($token['data']);
2709 } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2710 // XERROR: parse error
2711 } elseif ($token['type'] === HTML5_Tokenizer::ENDTAG &&
2712 $token['name'] === 'script' && end($this->stack)->tagName === 'script' &&
2713 end($this->stack)->namespaceURI === self::NS_SVG) {
2714 array_pop($this->stack);
2715 // a bunch of script running mumbo jumbo
2717 ($token['type'] === HTML5_Tokenizer::STARTTAG &&
2719 $token['name'] !== 'mglyph' &&
2720 $token['name'] !== 'malignmark' &&
2721 end($this->stack)->namespaceURI === self::NS_MATHML &&
2722 in_array(end($this->stack)->tagName, array('mi', 'mo', 'mn', 'ms', 'mtext'))
2725 $token['name'] === 'svg' &&
2726 end($this->stack)->namespaceURI === self::NS_MATHML &&
2727 end($this->stack)->tagName === 'annotation-xml'
2730 end($this->stack)->namespaceURI === self::NS_SVG &&
2731 in_array(end($this->stack)->tagName, array('foreignObject', 'desc', 'title'))
2735 end($this->stack)->namespaceURI === self::NS_HTML
2737 ) || $token['type'] === HTML5_Tokenizer::ENDTAG
2739 $this->processWithRulesFor($token, $this->secondary_mode);
2740 /* If, after doing so, the insertion mode is still "in foreign
2741 * content", but there is no element in scope that has a namespace
2742 * other than the HTML namespace, switch the insertion mode to the
2743 * secondary insertion mode. */
2744 if ($this->mode === self::IN_FOREIGN_CONTENT) {
2746 // this basically duplicates elementInScope()
2747 for ($i = count($this->stack) - 1; $i >= 0; $i--) {
2748 $node = $this->stack[$i];
2749 if ($node->namespaceURI !== self::NS_HTML) {
2752 } elseif (in_array($node->tagName, array('table', 'html',
2753 'applet', 'caption', 'td', 'th', 'button', 'marquee',
2754 'object')) || ($node->tagName === 'foreignObject' &&
2755 $node->namespaceURI === self::NS_SVG)) {
2760 $this->mode = $this->secondary_mode;
2763 } elseif ($token['type'] === HTML5_Tokenizer::EOF || (
2764 $token['type'] === HTML5_Tokenizer::STARTTAG &&
2765 (in_array($token['name'], array('b', "big", "blockquote", "body", "br",
2766 "center", "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2",
2767 "h3", "h4", "h5", "h6", "head", "hr", "i", "img", "li", "listing",
2768 "menu", "meta", "nobr", "ol", "p", "pre", "ruby", "s", "small",
2769 "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul",
2770 "var")) || ($token['name'] === 'font' && ($this->getAttr($token, 'color') ||
2771 $this->getAttr($token, 'face') || $this->getAttr($token, 'size')))))) {
2772 // XERROR: parse error
2774 $node = array_pop($this->stack);
2775 } while ($node->namespaceURI !== self::NS_HTML);
2776 $this->stack[] = $node;
2777 $this->mode = $this->secondary_mode;
2778 $this->emitToken($token);
2779 } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG) {
2780 static $svg_lookup = array(
2781 'altglyph' => 'altGlyph',
2782 'altglyphdef' => 'altGlyphDef',
2783 'altglyphitem' => 'altGlyphItem',
2784 'animatecolor' => 'animateColor',
2785 'animatemotion' => 'animateMotion',
2786 'animatetransform' => 'animateTransform',
2787 'clippath' => 'clipPath',
2788 'feblend' => 'feBlend',
2789 'fecolormatrix' => 'feColorMatrix',
2790 'fecomponenttransfer' => 'feComponentTransfer',
2791 'fecomposite' => 'feComposite',
2792 'feconvolvematrix' => 'feConvolveMatrix',
2793 'fediffuselighting' => 'feDiffuseLighting',
2794 'fedisplacementmap' => 'feDisplacementMap',
2795 'fedistantlight' => 'feDistantLight',
2796 'feflood' => 'feFlood',
2797 'fefunca' => 'feFuncA',
2798 'fefuncb' => 'feFuncB',
2799 'fefuncg' => 'feFuncG',
2800 'fefuncr' => 'feFuncR',
2801 'fegaussianblur' => 'feGaussianBlur',
2802 'feimage' => 'feImage',
2803 'femerge' => 'feMerge',
2804 'femergenode' => 'feMergeNode',
2805 'femorphology' => 'feMorphology',
2806 'feoffset' => 'feOffset',
2807 'fepointlight' => 'fePointLight',
2808 'fespecularlighting' => 'feSpecularLighting',
2809 'fespotlight' => 'feSpotLight',
2810 'fetile' => 'feTile',
2811 'feturbulence' => 'feTurbulence',
2812 'foreignobject' => 'foreignObject',
2813 'glyphref' => 'glyphRef',
2814 'lineargradient' => 'linearGradient',
2815 'radialgradient' => 'radialGradient',
2816 'textpath' => 'textPath',
2818 $current = end($this->stack);
2819 if ($current->namespaceURI === self::NS_MATHML) {
2820 $token = $this->adjustMathMLAttributes($token);
2822 if ($current->namespaceURI === self::NS_SVG &&
2823 isset($svg_lookup[$token['name']])) {
2824 $token['name'] = $svg_lookup[$token['name']];
2826 if ($current->namespaceURI === self::NS_SVG) {
2827 $token = $this->adjustSVGAttributes($token);
2829 $token = $this->adjustForeignAttributes($token);
2830 $this->insertForeignElement($token, $current->namespaceURI);
2831 if (isset($token['self-closing'])) {
2832 array_pop($this->stack);
2833 // XERROR: acknowledge self-closing flag
2838 case self::AFTER_BODY:
2839 /* Handle the token as follows: */
2841 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
2842 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
2844 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2845 /* Process the token as it would be processed if the insertion mode
2847 $this->processWithRulesFor($token, self::IN_BODY);
2849 /* A comment token */
2850 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
2851 /* Append a Comment node to the first element in the stack of open
2852 elements (the html element), with the data attribute set to the
2853 data given in the comment token. */
2854 $comment = $this->dom->createComment($token['data']);
2855 $this->stack[0]->appendChild($comment);
2857 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2860 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2861 $this->processWithRulesFor($token, self::IN_BODY);
2863 /* An end tag with the tag name "html" */
2864 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG && $token['name'] === 'html') {
2865 /* If the parser was originally created as part of the HTML
2866 * fragment parsing algorithm, this is a parse error; ignore
2867 * the token. (fragment case) */
2868 $this->ignored = true;
2869 // XERROR: implement this
2871 $this->mode = self::AFTER_AFTER_BODY;
2873 } elseif($token['type'] === HTML5_Tokenizer::EOF) {
2878 /* Parse error. Set the insertion mode to "in body" and reprocess
2880 $this->mode = self::IN_BODY;
2881 $this->emitToken($token);
2885 case self::IN_FRAMESET:
2886 /* Handle the token as follows: */
2888 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
2889 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
2890 U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
2891 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2892 /* Append the character to the current node. */
2893 $this->insertText($token['data']);
2895 /* A comment token */
2896 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
2897 /* Append a Comment node to the current node with the data
2898 attribute set to the data given in the comment token. */
2899 $this->insertComment($token['data']);
2901 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2904 /* A start tag with the tag name "frameset" */
2905 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2906 $token['name'] === 'frameset') {
2907 $this->insertElement($token);
2909 /* An end tag with the tag name "frameset" */
2910 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2911 $token['name'] === 'frameset') {
2912 /* If the current node is the root html element, then this is a
2913 parse error; ignore the token. (fragment case) */
2914 if(end($this->stack)->tagName === 'html') {
2915 $this->ignored = true;
2919 /* Otherwise, pop the current node from the stack of open
2921 array_pop($this->stack);
2923 /* If the parser was not originally created as part of the HTML
2924 * fragment parsing algorithm (fragment case), and the current
2925 * node is no longer a frameset element, then switch the
2926 * insertion mode to "after frameset". */
2927 $this->mode = self::AFTER_FRAMESET;
2930 /* A start tag with the tag name "frame" */
2931 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2932 $token['name'] === 'frame') {
2933 /* Insert an HTML element for the token. */
2934 $this->insertElement($token);
2936 /* Immediately pop the current node off the stack of open elements. */
2937 array_pop($this->stack);
2939 // XERROR: Acknowledge the token's self-closing flag, if it is set.
2941 /* A start tag with the tag name "noframes" */
2942 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2943 $token['name'] === 'noframes') {
2944 /* Process the token using the rules for the "in head" insertion mode. */
2945 $this->processwithRulesFor($token, self::IN_HEAD);
2947 } elseif($token['type'] === HTML5_Tokenizer::EOF) {
2948 // XERROR: If the current node is not the root html element, then this is a parse error.
2952 /* Parse error. Ignore the token. */
2953 $this->ignored = true;
2957 case self::AFTER_FRAMESET:
2958 /* Handle the token as follows: */
2960 /* A character token that is one of one of U+0009 CHARACTER TABULATION,
2961 U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
2962 U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
2963 if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2964 /* Append the character to the current node. */
2965 $this->insertText($token['data']);
2967 /* A comment token */
2968 } elseif($token['type'] === HTML5_Tokenizer::COMMENT) {
2969 /* Append a Comment node to the current node with the data
2970 attribute set to the data given in the comment token. */
2971 $this->insertComment($token['data']);
2973 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2976 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2977 $this->processWithRulesFor($token, self::IN_BODY);
2979 /* An end tag with the tag name "html" */
2980 } elseif($token['type'] === HTML5_Tokenizer::ENDTAG &&
2981 $token['name'] === 'html') {
2982 $this->mode = self::AFTER_AFTER_FRAMESET;
2984 /* A start tag with the tag name "noframes" */
2985 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2986 $token['name'] === 'noframes') {
2987 $this->processWithRulesFor($token, self::IN_HEAD);
2989 } elseif($token['type'] === HTML5_Tokenizer::EOF) {
2994 /* Parse error. Ignore the token. */
2995 $this->ignored = true;
2999 case self::AFTER_AFTER_BODY:
3000 /* A comment token */
3001 if($token['type'] === HTML5_Tokenizer::COMMENT) {
3002 /* Append a Comment node to the Document object with the data
3003 attribute set to the data given in the comment token. */
3004 $comment = $this->dom->createComment($token['data']);
3005 $this->dom->appendChild($comment);
3007 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
3008 $token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
3009 ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
3010 $this->processWithRulesFor($token, self::IN_BODY);
3012 /* An end-of-file token */
3013 } elseif($token['type'] === HTML5_Tokenizer::EOF) {
3017 $this->mode = self::IN_BODY;
3018 $this->emitToken($token);
3022 case self::AFTER_AFTER_FRAMESET:
3023 /* A comment token */
3024 if($token['type'] === HTML5_Tokenizer::COMMENT) {
3025 /* Append a Comment node to the Document object with the data
3026 attribute set to the data given in the comment token. */
3027 $comment = $this->dom->createComment($token['data']);
3028 $this->dom->appendChild($comment);
3030 } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE ||
3031 $token['type'] === HTML5_Tokenizer::SPACECHARACTER ||
3032 ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html')) {
3033 $this->processWithRulesFor($token, self::IN_BODY);
3035 /* An end-of-file token */
3036 } elseif($token['type'] === HTML5_Tokenizer::EOF) {
3038 } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'nofrmaes') {
3039 $this->processWithRulesFor($token, self::IN_HEAD);
3045 // end funky indenting
3048 private function insertElement($token, $append = true) {
3049 $el = $this->dom->createElementNS(self::NS_HTML, $token['name']);
3051 if (!empty($token['attr'])) {
3052 foreach($token['attr'] as $attr) {
3054 // mike@macgirvin.com 2011-11-17, check attribute name for
3055 // validity (ignoring extenders and combiners) as illegal chars in names
3056 // causes everything to abort
3058 $valid = preg_match('/^[a-zA-Z\_\:]([\-a-zA-Z0-9\_\:\.]+$)/',$attr['name'],$matches);
3059 if($attr['name'] && (!$el->hasAttribute($attr['name'])) && ($valid)) {
3060 $el->setAttribute($attr['name'], $attr['value']);
3065 $this->appendToRealParent($el);
3066 $this->stack[] = $el;
3072 private function insertText($data) {
3073 if ($data === '') return;
3074 if ($this->ignore_lf_token) {
3075 if ($data[0] === "\n") {
3076 $data = substr($data, 1);
3077 if ($data === false) return;
3080 $text = $this->dom->createTextNode($data);
3081 $this->appendToRealParent($text);
3084 private function insertComment($data) {
3085 $comment = $this->dom->createComment($data);
3086 $this->appendToRealParent($comment);
3089 private function appendToRealParent($node) {
3090 // this is only for the foster_parent case
3091 /* If the current node is a table, tbody, tfoot, thead, or tr
3092 element, then, whenever a node would be inserted into the current
3093 node, it must instead be inserted into the foster parent element. */
3094 if(!$this->foster_parent || !in_array(end($this->stack)->tagName,
3095 array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
3096 end($this->stack)->appendChild($node);
3098 $this->fosterParent($node);
3102 private function elementInScope($el, $table = false) {
3104 foreach($el as $element) {
3105 if($this->elementInScope($element, $table)) {
3113 $leng = count($this->stack);
3115 for($n = 0; $n < $leng; $n++) {
3116 /* 1. Initialise node to be the current node (the bottommost node of
3118 $node = $this->stack[$leng - 1 - $n];
3120 if($node->tagName === $el) {
3121 /* 2. If node is the target node, terminate in a match state. */
3124 // these are the common states for "in scope" and "in table scope"
3125 } elseif($node->tagName === 'table' || $node->tagName === 'html') {
3128 // these are only valid for "in scope"
3130 (in_array($node->tagName, array('applet', 'caption', 'td',
3131 'th', 'button', 'marquee', 'object')) ||
3132 $node->tagName === 'foreignObject' && $node->namespaceURI === self::NS_SVG)) {
3136 /* Otherwise, set node to the previous entry in the stack of open
3137 elements and return to step 2. (This will never fail, since the loop
3138 will always terminate in the previous step if the top of the stack
3143 private function reconstructActiveFormattingElements() {
3144 /* 1. If there are no entries in the list of active formatting elements,
3145 then there is nothing to reconstruct; stop this algorithm. */
3146 $formatting_elements = count($this->a_formatting);
3148 if($formatting_elements === 0) {
3152 /* 3. Let entry be the last (most recently added) element in the list
3153 of active formatting elements. */
3154 $entry = end($this->a_formatting);
3156 /* 2. If the last (most recently added) entry in the list of active
3157 formatting elements is a marker, or if it is an element that is in the
3158 stack of open elements, then there is nothing to reconstruct; stop this
3160 if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
3164 for($a = $formatting_elements - 1; $a >= 0; true) {
3165 /* 4. If there are no entries before entry in the list of active
3166 formatting elements, then jump to step 8. */
3168 $step_seven = false;
3172 /* 5. Let entry be the entry one earlier than entry in the list of
3173 active formatting elements. */
3175 $entry = $this->a_formatting[$a];
3177 /* 6. If entry is neither a marker nor an element that is also in
3178 thetack of open elements, go to step 4. */
3179 if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
3185 /* 7. Let entry be the element one later than entry in the list of
3186 active formatting elements. */
3187 if(isset($step_seven) && $step_seven === true) {
3189 $entry = $this->a_formatting[$a];
3192 /* 8. Perform a shallow clone of the element entry to obtain clone. */
3193 $clone = $entry->cloneNode();
3195 /* 9. Append clone to the current node and push it onto the stack
3196 of open elements so that it is the new current node. */
3197 $this->appendToRealParent($clone);
3198 $this->stack[] = $clone;
3200 /* 10. Replace the entry for entry in the list with an entry for
3202 $this->a_formatting[$a] = $clone;
3204 /* 11. If the entry for clone in the list of active formatting
3205 elements is not the last entry in the list, return to step 7. */
3206 if(end($this->a_formatting) !== $clone) {
3214 private function clearTheActiveFormattingElementsUpToTheLastMarker() {
3215 /* When the steps below require the UA to clear the list of active
3216 formatting elements up to the last marker, the UA must perform the
3220 /* 1. Let entry be the last (most recently added) entry in the list
3221 of active formatting elements. */
3222 $entry = end($this->a_formatting);
3224 /* 2. Remove entry from the list of active formatting elements. */
3225 array_pop($this->a_formatting);
3227 /* 3. If entry was a marker, then stop the algorithm at this point.
3228 The list has been cleared up to the last marker. */
3229 if($entry === self::MARKER) {
3235 private function generateImpliedEndTags($exclude = array()) {
3236 /* When the steps below require the UA to generate implied end tags,
3237 then, if the current node is a dd element, a dt element, an li element,
3238 a p element, a td element, a th element, or a tr element, the UA must
3239 act as if an end tag with the respective tag name had been seen and
3240 then generate implied end tags again. */
3241 $node = end($this->stack);
3242 $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
3244 while(in_array(end($this->stack)->tagName, $elements)) {
3245 array_pop($this->stack);
3249 private function getElementCategory($node) {
3250 if (!is_object($node)) debug_print_backtrace();
3251 $name = $node->tagName;
3252 if(in_array($name, $this->special))
3253 return self::SPECIAL;
3255 elseif(in_array($name, $this->scoping))
3256 return self::SCOPING;
3258 elseif(in_array($name, $this->formatting))
3259 return self::FORMATTING;
3262 return self::PHRASING;
3265 private function clearStackToTableContext($elements) {
3266 /* When the steps above require the UA to clear the stack back to a
3267 table context, it means that the UA must, while the current node is not
3268 a table element or an html element, pop elements from the stack of open
3271 $name = end($this->stack)->tagName;
3273 if(in_array($name, $elements)) {
3276 array_pop($this->stack);
3281 private function resetInsertionMode($context = null) {
3282 /* 1. Let last be false. */
3284 $leng = count($this->stack);
3286 for($n = $leng - 1; $n >= 0; $n--) {
3287 /* 2. Let node be the last node in the stack of open elements. */
3288 $node = $this->stack[$n];
3290 /* 3. If node is the first node in the stack of open elements, then
3291 * set last to true and set node to the context element. (fragment
3293 if($this->stack[0]->isSameNode($node)) {
3298 /* 4. If node is a select element, then switch the insertion mode to
3299 "in select" and abort these steps. (fragment case) */
3300 if($node->tagName === 'select') {
3301 $this->mode = self::IN_SELECT;
3304 /* 5. If node is a td or th element, then switch the insertion mode
3305 to "in cell" and abort these steps. */
3306 } elseif($node->tagName === 'td' || $node->nodeName === 'th') {
3307 $this->mode = self::IN_CELL;
3310 /* 6. If node is a tr element, then switch the insertion mode to
3311 "in row" and abort these steps. */
3312 } elseif($node->tagName === 'tr') {
3313 $this->mode = self::IN_ROW;
3316 /* 7. If node is a tbody, thead, or tfoot element, then switch the
3317 insertion mode to "in table body" and abort these steps. */
3318 } elseif(in_array($node->tagName, array('tbody', 'thead', 'tfoot'))) {
3319 $this->mode = self::IN_TABLE_BODY;
3322 /* 8. If node is a caption element, then switch the insertion mode
3323 to "in caption" and abort these steps. */
3324 } elseif($node->tagName === 'caption') {
3325 $this->mode = self::IN_CAPTION;
3328 /* 9. If node is a colgroup element, then switch the insertion mode
3329 to "in column group" and abort these steps. (innerHTML case) */
3330 } elseif($node->tagName === 'colgroup') {
3331 $this->mode = self::IN_COLUMN_GROUP;
3334 /* 10. If node is a table element, then switch the insertion mode
3335 to "in table" and abort these steps. */
3336 } elseif($node->tagName === 'table') {
3337 $this->mode = self::IN_TABLE;
3340 /* 11. If node is an element from the MathML namespace or the SVG
3341 * namespace, then switch the insertion mode to "in foreign
3342 * content", let the secondary insertion mode be "in body", and
3343 * abort these steps. */
3344 } elseif($node->namespaceURI === self::NS_SVG ||
3345 $node->namespaceURI === self::NS_MATHML) {
3346 $this->mode = self::IN_FOREIGN_CONTENT;
3347 $this->secondary_mode = self::IN_BODY;
3350 /* 12. If node is a head element, then switch the insertion mode
3351 to "in body" ("in body"! not "in head"!) and abort these steps.
3353 } elseif($node->tagName === 'head') {
3354 $this->mode = self::IN_BODY;
3357 /* 13. If node is a body element, then switch the insertion mode to
3358 "in body" and abort these steps. */
3359 } elseif($node->tagName === 'body') {
3360 $this->mode = self::IN_BODY;
3363 /* 14. If node is a frameset element, then switch the insertion
3364 mode to "in frameset" and abort these steps. (fragment case) */
3365 } elseif($node->tagName === 'frameset') {
3366 $this->mode = self::IN_FRAMESET;
3369 /* 15. If node is an html element, then: if the head element
3370 pointer is null, switch the insertion mode to "before head",
3371 otherwise, switch the insertion mode to "after head". In either
3372 case, abort these steps. (fragment case) */
3373 } elseif($node->tagName === 'html') {
3374 $this->mode = ($this->head_pointer === null)
3380 /* 16. If last is true, then set the insertion mode to "in body"
3381 and abort these steps. (fragment case) */
3383 $this->mode = self::IN_BODY;
3389 private function closeCell() {
3390 /* If the stack of open elements has a td or th element in table scope,
3391 then act as if an end tag token with that tag name had been seen. */
3392 foreach(array('td', 'th') as $cell) {
3393 if($this->elementInScope($cell, true)) {
3394 $this->emitToken(array(
3396 'type' => HTML5_Tokenizer::ENDTAG
3404 private function processWithRulesFor($token, $mode) {
3405 /* "using the rules for the m insertion mode", where m is one of these
3406 * modes, the user agent must use the rules described under the m
3407 * insertion mode's section, but must leave the insertion mode
3408 * unchanged unless the rules in m themselves switch the insertion mode
3409 * to a new value. */
3410 return $this->emitToken($token, $mode);
3413 private function insertCDATAElement($token) {
3414 $this->insertElement($token);
3415 $this->original_mode = $this->mode;
3416 $this->mode = self::IN_CDATA_RCDATA;
3417 $this->content_model = HTML5_Tokenizer::CDATA;
3420 private function insertRCDATAElement($token) {
3421 $this->insertElement($token);
3422 $this->original_mode = $this->mode;
3423 $this->mode = self::IN_CDATA_RCDATA;
3424 $this->content_model = HTML5_Tokenizer::RCDATA;
3427 private function getAttr($token, $key) {
3428 if (!isset($token['attr'])) return false;
3430 foreach ($token['attr'] as $keypair) {
3431 if ($keypair['name'] === $key) $ret = $keypair['value'];
3436 private function getCurrentTable() {
3437 /* The current table is the last table element in the stack of open
3438 * elements, if there is one. If there is no table element in the stack
3439 * of open elements (fragment case), then the current table is the
3440 * first element in the stack of open elements (the html element). */
3441 for ($i = count($this->stack) - 1; $i >= 0; $i--) {
3442 if ($this->stack[$i]->tagName === 'table') {
3443 return $this->stack[$i];
3446 return $this->stack[0];
3449 private function getFosterParent() {
3450 /* The foster parent element is the parent element of the last
3451 table element in the stack of open elements, if there is a
3452 table element and it has such a parent element. If there is no
3453 table element in the stack of open elements (innerHTML case),
3454 then the foster parent element is the first element in the
3455 stack of open elements (the html element). Otherwise, if there
3456 is a table element in the stack of open elements, but the last
3457 table element in the stack of open elements has no parent, or
3458 its parent node is not an element, then the foster parent
3459 element is the element before the last table element in the
3460 stack of open elements. */
3461 for($n = count($this->stack) - 1; $n >= 0; $n--) {
3462 if($this->stack[$n]->tagName === 'table') {
3463 $table = $this->stack[$n];
3468 if(isset($table) && $table->parentNode !== null) {
3469 return $table->parentNode;
3471 } elseif(!isset($table)) {
3472 return $this->stack[0];
3474 } elseif(isset($table) && ($table->parentNode === null ||
3475 $table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
3476 return $this->stack[$n - 1];
3480 public function fosterParent($node) {
3481 $foster_parent = $this->getFosterParent();
3482 $table = $this->getCurrentTable(); // almost equivalent to last table element, except it can be html
3483 /* When a node node is to be foster parented, the node node must be
3484 * inserted into the foster parent element, and the current table must
3485 * be marked as tainted. (Once the current table has been tainted,
3486 * whitespace characters are inserted into the foster parent element
3487 * instead of the current node.) */
3488 $table->tainted = true;
3489 /* If the foster parent element is the parent element of the last table
3490 * element in the stack of open elements, then node must be inserted
3491 * immediately before the last table element in the stack of open
3492 * elements in the foster parent element; otherwise, node must be
3493 * appended to the foster parent element. */
3494 if ($table->tagName === 'table' && $table->parentNode->isSameNode($foster_parent)) {
3495 $foster_parent->insertBefore($node, $table);
3497 $foster_parent->appendChild($node);
3502 * For debugging, prints the stack
3504 private function printStack() {
3506 foreach ($this->stack as $i => $element) {
3507 $names[] = $element->tagName;
3509 echo " -> stack [" . implode(', ', $names) . "]\n";
3513 * For debugging, prints active formatting elements
3515 private function printActiveFormattingElements() {
3516 if (!$this->a_formatting) return;
3518 foreach ($this->a_formatting as $node) {
3519 if ($node === self::MARKER) $names[] = 'MARKER';
3520 else $names[] = $node->tagName;
3522 echo " -> active formatting [" . implode(', ', $names) . "]\n";
3525 public function currentTableIsTainted() {
3526 return !empty($this->getCurrentTable()->tainted);
3530 * Sets up the tree constructor for building a fragment.
3532 public function setupContext($context = null) {
3533 $this->fragment = true;
3535 $context = $this->dom->createElementNS(self::NS_HTML, $context);
3536 /* 4.1. Set the HTML parser's tokenization stage's content model
3537 * flag according to the context element, as follows: */
3538 switch ($context->tagName) {
3539 case 'title': case 'textarea':
3540 $this->content_model = HTML5_Tokenizer::RCDATA;
3542 case 'style': case 'script': case 'xmp': case 'iframe':
3543 case 'noembed': case 'noframes':
3544 $this->content_model = HTML5_Tokenizer::CDATA;
3547 // XSCRIPT: assuming scripting is enabled
3548 $this->content_model = HTML5_Tokenizer::CDATA;
3551 $this->content_model = HTML5_Tokenizer::PLAINTEXT;
3554 /* 4.2. Let root be a new html element with no attributes. */
3555 $root = $this->dom->createElementNS(self::NS_HTML, 'html');
3556 $this->root = $root;
3557 /* 4.3 Append the element root to the Document node created above. */
3558 $this->dom->appendChild($root);
3559 /* 4.4 Set up the parser's stack of open elements so that it
3560 * contains just the single element root. */
3561 $this->stack = array($root);
3562 /* 4.5 Reset the parser's insertion mode appropriately. */
3563 $this->resetInsertionMode($context);
3564 /* 4.6 Set the parser's form element pointer to the nearest node
3565 * to the context element that is a form element (going straight up
3566 * the ancestor chain, and including the element itself, if it is a
3567 * form element), or, if there is no such form element, to null. */
3570 if ($node->tagName === 'form') {
3571 $this->form_pointer = $node;
3574 } while ($node = $node->parentNode);
3578 public function adjustMathMLAttributes($token) {
3579 foreach ($token['attr'] as &$kp) {
3580 if ($kp['name'] === 'definitionurl') {
3581 $kp['name'] = 'definitionURL';
3587 public function adjustSVGAttributes($token) {
3588 static $lookup = array(
3589 'attributename' => 'attributeName',
3590 'attributetype' => 'attributeType',
3591 'basefrequency' => 'baseFrequency',
3592 'baseprofile' => 'baseProfile',
3593 'calcmode' => 'calcMode',
3594 'clippathunits' => 'clipPathUnits',
3595 'contentscripttype' => 'contentScriptType',
3596 'contentstyletype' => 'contentStyleType',
3597 'diffuseconstant' => 'diffuseConstant',
3598 'edgemode' => 'edgeMode',
3599 'externalresourcesrequired' => 'externalResourcesRequired',
3600 'filterres' => 'filterRes',
3601 'filterunits' => 'filterUnits',
3602 'glyphref' => 'glyphRef',
3603 'gradienttransform' => 'gradientTransform',
3604 'gradientunits' => 'gradientUnits',
3605 'kernelmatrix' => 'kernelMatrix',
3606 'kernelunitlength' => 'kernelUnitLength',
3607 'keypoints' => 'keyPoints',
3608 'keysplines' => 'keySplines',
3609 'keytimes' => 'keyTimes',
3610 'lengthadjust' => 'lengthAdjust',
3611 'limitingconeangle' => 'limitingConeAngle',
3612 'markerheight' => 'markerHeight',
3613 'markerunits' => 'markerUnits',
3614 'markerwidth' => 'markerWidth',
3615 'maskcontentunits' => 'maskContentUnits',
3616 'maskunits' => 'maskUnits',
3617 'numoctaves' => 'numOctaves',
3618 'pathlength' => 'pathLength',
3619 'patterncontentunits' => 'patternContentUnits',
3620 'patterntransform' => 'patternTransform',
3621 'patternunits' => 'patternUnits',
3622 'pointsatx' => 'pointsAtX',
3623 'pointsaty' => 'pointsAtY',
3624 'pointsatz' => 'pointsAtZ',
3625 'preservealpha' => 'preserveAlpha',
3626 'preserveaspectratio' => 'preserveAspectRatio',
3627 'primitiveunits' => 'primitiveUnits',
3630 'repeatcount' => 'repeatCount',
3631 'repeatdur' => 'repeatDur',
3632 'requiredextensions' => 'requiredExtensions',
3633 'requiredfeatures' => 'requiredFeatures',
3634 'specularconstant' => 'specularConstant',
3635 'specularexponent' => 'specularExponent',
3636 'spreadmethod' => 'spreadMethod',
3637 'startoffset' => 'startOffset',
3638 'stddeviation' => 'stdDeviation',
3639 'stitchtiles' => 'stitchTiles',
3640 'surfacescale' => 'surfaceScale',
3641 'systemlanguage' => 'systemLanguage',
3642 'tablevalues' => 'tableValues',
3643 'targetx' => 'targetX',
3644 'targety' => 'targetY',
3645 'textlength' => 'textLength',
3646 'viewbox' => 'viewBox',
3647 'viewtarget' => 'viewTarget',
3648 'xchannelselector' => 'xChannelSelector',
3649 'ychannelselector' => 'yChannelSelector',
3650 'zoomandpan' => 'zoomAndPan',
3652 foreach ($token['attr'] as &$kp) {
3653 if (isset($lookup[$kp['name']])) {
3654 $kp['name'] = $lookup[$kp['name']];
3660 public function adjustForeignAttributes($token) {
3661 static $lookup = array(
3662 'xlink:actuate' => array('xlink', 'actuate', self::NS_XLINK),
3663 'xlink:arcrole' => array('xlink', 'arcrole', self::NS_XLINK),
3664 'xlink:href' => array('xlink', 'href', self::NS_XLINK),
3665 'xlink:role' => array('xlink', 'role', self::NS_XLINK),
3666 'xlink:show' => array('xlink', 'show', self::NS_XLINK),
3667 'xlink:title' => array('xlink', 'title', self::NS_XLINK),
3668 'xlink:type' => array('xlink', 'type', self::NS_XLINK),
3669 'xml:base' => array('xml', 'base', self::NS_XML),
3670 'xml:lang' => array('xml', 'lang', self::NS_XML),
3671 'xml:space' => array('xml', 'space', self::NS_XML),
3672 'xmlns' => array(null, 'xmlns', self::NS_XMLNS),
3673 'xmlns:xlink' => array('xmlns', 'xlink', self::NS_XMLNS),
3675 foreach ($token['attr'] as &$kp) {
3676 if (isset($lookup[$kp['name']])) {
3677 $kp['name'] = $lookup[$kp['name']];
3683 public function insertForeignElement($token, $namespaceURI) {
3684 $el = $this->dom->createElementNS($namespaceURI, $token['name']);
3685 if (!empty($token['attr'])) {
3686 foreach ($token['attr'] as $kp) {
3687 $attr = $kp['name'];
3688 if (is_array($attr)) {
3692 $ns = self::NS_HTML;
3694 if (!$el->hasAttributeNS($ns, $attr)) {
3695 // XSKETCHY: work around godawful libxml bug
3696 if ($ns === self::NS_XLINK) {
3697 $el->setAttribute('xlink:'.$attr, $kp['value']);
3698 } elseif ($ns === self::NS_HTML) {
3699 // Another godawful libxml bug
3700 $el->setAttribute($attr, $kp['value']);
3702 $el->setAttributeNS($ns, $attr, $kp['value']);
3707 $this->appendToRealParent($el);
3708 $this->stack[] = $el;
3709 // XERROR: see below
3710 /* If the newly created element has an xmlns attribute in the XMLNS
3711 * namespace whose value is not exactly the same as the element's
3712 * namespace, that is a parse error. Similarly, if the newly created
3713 * element has an xmlns:xlink attribute in the XMLNS namespace whose
3714 * value is not the XLink Namespace, that is a parse error. */
3717 public function save() {
3718 $this->dom->normalize();
3719 if (!$this->fragment) {
3723 return $this->root->childNodes;
3725 return $this->dom->childNodes;