]> git.mxchange.org Git - friendica.git/blob - library/HTML5/TreeBuilder.php
257e73c3e06a04b79b3c67ada8c4d663f5add15e
[friendica.git] / library / HTML5 / TreeBuilder.php
1 <?php
2
3 /*
4
5 Copyright 2007 Jeroen van der Meer <http://jero.net/>
6 Copyright 2009 Edward Z. Yang <edwardzyang@thewritingpot.com>
7
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:
15
16 The above copyright notice and this permission notice shall be included
17 in all copies or substantial portions of the Software.
18
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.
26
27 */
28
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)
34
35 class HTML5_TreeBuilder {
36     public $stack = array();
37     public $content_model;
38
39     private $mode;
40     private $original_mode;
41     private $secondary_mode;
42     private $dom;
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();
47
48     private $head_pointer = null;
49     private $form_pointer = null;
50
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
58     // appropriately)
59     private $ignore_lf_token = 0;
60     private $fragment = false;
61     private $root;
62
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');
72
73     // Tree construction modes
74     const INITIAL           = 0;
75     const BEFORE_HTML       = 1;
76     const BEFORE_HEAD       = 2;
77     const IN_HEAD           = 3;
78     const IN_HEAD_NOSCRIPT  = 4;
79     const AFTER_HEAD        = 5;
80     const IN_BODY           = 6;
81     const IN_CDATA_RCDATA   = 7;
82     const IN_TABLE          = 8;
83     const IN_CAPTION        = 9;
84     const IN_COLUMN_GROUP   = 10;
85     const IN_TABLE_BODY     = 11;
86     const IN_ROW            = 12;
87     const IN_CELL           = 13;
88     const IN_SELECT         = 14;
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;
96
97     /**
98      * Converts a magic number to a readable name. Use for debugging.
99      */
100     private function strConst($number) {
101         static $lookup;
102         if (!$lookup) {
103             $r = new ReflectionClass('HTML5_TreeBuilder');
104             $lookup = array_flip($r->getConstants());
105         }
106         return $lookup[$number];
107     }
108
109     // The different types of elements.
110     const SPECIAL    = 100;
111     const SCOPING    = 101;
112     const FORMATTING = 102;
113     const PHRASING   = 103;
114
115     // Quirks modes in $quirks_mode
116     const NO_QUIRKS             = 200;
117     const QUIRKS_MODE           = 201;
118     const LIMITED_QUIRKS_MODE   = 202;
119
120     // Marker to be placed in $a_formatting
121     const MARKER     = 300;
122
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';
131
132     public function __construct() {
133         $this->mode = self::INITIAL;
134         $this->dom = new DOMDocument;
135
136         $this->dom->encoding = 'UTF-8';
137         $this->dom->preserveWhiteSpace = true;
138         $this->dom->substituteEntities = true;
139         $this->dom->strictErrorChecking = false;
140     }
141
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;
147
148         /*
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).")";
153         echo "\n  ";
154         token_dump($token);
155         $this->printStack();
156         $this->printActiveFormattingElements();
157         if ($this->foster_parent) echo "  -> this is a foster parent mode\n";
158         */
159
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
163         switch ($mode) {
164
165     case self::INITIAL:
166
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) {
173             if (
174                 $token['name'] !== 'html' || !empty($token['public']) ||
175                 !empty($token['system']) || $token !== 'about:legacy-compat'
176             ) {
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
184             }
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
201             // syllables.
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);
208             } else {
209                 // It looks like libxml's not actually *able* to express this case.
210                 // So... don't.
211                 $this->dom->emptyDoctype = true;
212             }
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//",
269             );
270             $publicSetToForQuirks = array(
271              "-//w3o//dtd w3 html strict 3.0//",
272              "-/w3c/dtd html 4.0 transitional/en",
273              "html",
274             );
275             $publicStartsWithAndSystemForQuirks = array(
276              "-//w3c//dtd html 4.01 frameset//",
277              "-//w3c//dtd html 4.01 transitional//",
278             );
279             $publicStartsWithForLimitedQuirks = array(
280              "-//w3c//dtd xhtml 1.0 frameset//",
281              "-//w3c//dtd xhtml 1.0 transitional//",
282             );
283             $publicStartsWithAndSystemForLimitedQuirks = array(
284              "-//w3c//dtd html 4.01 frameset//",
285              "-//w3c//dtd html 4.01 transitional//",
286             );
287             // first, do easy checks
288             if (
289                 !empty($token['force-quirks']) ||
290                 strtolower($token['name']) !== 'html'
291             ) {
292                 $this->quirks_mode = self::QUIRKS_MODE;
293             } else {
294                 do {
295                     if ($system) {
296                         foreach ($publicStartsWithAndSystemForQuirks as $x) {
297                             if (strncmp($public, $x, strlen($x)) === 0) {
298                                 $this->quirks_mode = self::QUIRKS_MODE;
299                                 break;
300                             }
301                         }
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;
306                                 break;
307                             }
308                         }
309                         if (!is_null($this->quirks_mode)) break;
310                     }
311                     foreach ($publicSetToForQuirks as $x) {
312                         if ($public === $x) {
313                             $this->quirks_mode = self::QUIRKS_MODE;
314                             break;
315                         }
316                     }
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;
321                         }
322                     }
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;
326                         break;
327                     }
328                     foreach ($publicStartsWithForQuirks as $x) {
329                         if (strncmp($public, $x, strlen($x)) === 0) {
330                             $this->quirks_mode = self::QUIRKS_MODE;
331                             break;
332                         }
333                     }
334                     if (is_null($this->quirks_mode)) {
335                         $this->quirks_mode = self::NO_QUIRKS;
336                     }
337                 } while (false);
338             }
339             $this->mode = self::BEFORE_HTML;
340         } else {
341             // parse error
342             /* Switch the insertion mode to "before html", then reprocess the
343              * current token. */
344             $this->mode = self::BEFORE_HTML;
345             $this->quirks_mode = self::QUIRKS_MODE;
346             $this->emitToken($token);
347         }
348         break;
349
350     case self::BEFORE_HTML:
351
352         /* A DOCTYPE token */
353         if($token['type'] === HTML5_Tokenizer::DOCTYPE) {
354             // Parse error. Ignore the token.
355             $this->ignored = true;
356
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);
363
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),
366         or U+0020 SPACE */
367         } elseif($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
368             /* Ignore the token. */
369             $this->ignored = true;
370
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 
375              * elements. */
376             $html = $this->insertElement($token, false);
377             $this->dom->appendChild($html);
378             $this->stack[] = $html;
379
380             $this->mode = self::BEFORE_HEAD;
381
382         } else {
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;
388
389             /* Switch the insertion mode to "before head", then reprocess the
390              * current token. */
391             $this->mode = self::BEFORE_HEAD;
392             $this->emitToken($token);
393         }
394         break;
395
396     case self::BEFORE_HEAD:
397
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),
400         or U+0020 SPACE */
401         if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
402             /* Ignore the token. */
403             $this->ignored = true;
404
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']);
410
411         /* A DOCTYPE token */
412         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
413             /* Parse error. Ignore the token */
414             $this->ignored = true;
415             // parse error
416
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"
420              * insertion mode. */
421             $this->processWithRulesFor($token, self::IN_BODY);
422
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);
427
428             /* Set the head element pointer to this new element node. */
429             $this->head_pointer = $element;
430
431             /* Change the insertion mode to "in head". */
432             $this->mode = self::IN_HEAD;
433
434         /* An end tag whose tag name is one of: "head", "body", "html", "br" */
435         } elseif(
436             $token['type'] === HTML5_Tokenizer::ENDTAG && (
437                 $token['name'] === 'head' || $token['name'] === 'body' ||
438                 $token['name'] === 'html' || $token['name'] === 'br'
439         )) {
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(
443                 'name' => 'head',
444                 'type' => HTML5_Tokenizer::STARTTAG,
445                 'attr' => array()
446             ));
447             $this->emitToken($token);
448
449         /* Any other end tag */
450         } elseif($token['type'] === HTML5_Tokenizer::ENDTAG) {
451             /* Parse error. Ignore the token. */
452             $this->ignored = true;
453
454         } else {
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(
461                 'name' => 'head',
462                 'type' => HTML5_Tokenizer::STARTTAG,
463                 'attr' => array()
464             ));
465             $this->emitToken($token);
466         }
467         break;
468
469     case self::IN_HEAD:
470
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),
473         or U+0020 SPACE. */
474         if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
475             /* Insert the character into the current node. */
476             $this->insertText($token['data']);
477
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']);
483
484         /* A DOCTYPE token */
485         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
486             /* Parse error. Ignore the token. */
487             $this->ignored = true;
488             // parse error
489
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);
494
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);
503
504             // YYY: Acknowledge the token's self-closing flag, if it is set.
505
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);
512
513             // XERROR: Acknowledge the token's self-closing flag, if it is set.
514
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.
519             //
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.
525
526         /* A start tag with the tag name "title" */
527         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'title') {
528             $this->insertRCDATAElement($token);
529
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);
536
537         // XSCRIPT: Scripting flag disable not implemented
538
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);
543
544             /* 2. Mark the element as being "parser-inserted" */
545             // Uhhh... XSCRIPT
546
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) */
550             // ditto... XSCRIPT
551
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
557
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;
564
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);
569
570             /* Change the insertion mode to "after head". */
571             $this->mode = self::AFTER_HEAD;
572
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;
581
582         /* Anything else */
583         } else {
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(
587                 'name' => 'head',
588                 'type' => HTML5_Tokenizer::ENDTAG
589             ));
590
591             /* Then, reprocess the current token. */
592             $this->emitToken($token);
593         }
594         break;
595
596     case self::IN_HEAD_NOSCRIPT:
597         if ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
598             // parse error
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
604              * element. */
605             array_pop($this->stack);
606             $this->mode = self::IN_HEAD;
607         } elseif (
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);
614         // inverted logic
615         } elseif (
616             ($token['type'] === HTML5_Tokenizer::STARTTAG && (
617                 $token['name'] === 'head' || $token['name'] === 'noscript')) ||
618             ($token['type'] === HTML5_Tokenizer::ENDTAG &&
619                 $token['name'] !== 'br')) {
620             // parse error
621         } else {
622             // parse error
623             $this->emitToken(array(
624                 'type' => HTML5_Tokenizer::ENDTAG,
625                 'name' => 'noscript',
626             ));
627             $this->emitToken($token);
628         }
629         break;
630
631     case self::AFTER_HEAD:
632         /* Handle the token as follows: */
633
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),
636         or U+0020 SPACE */
637         if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
638             /* Append the character to the current node. */
639             $this->insertText($token['data']);
640
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']);
646
647         } elseif ($token['type'] === HTML5_Tokenizer::DOCTYPE) {
648             // parse error
649
650         } elseif ($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
651             $this->processWithRulesFor($token, self::IN_BODY);
652
653         /* A start tag token with the tag name "body" */
654         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'body') {
655             $this->insertElement($token);
656
657             /* Set the frameset-ok flag to "not ok". */
658             $this->flag_frameset_ok = false;
659
660             /* Change the insertion mode to "in body". */
661             $this->mode = self::IN_BODY;
662
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);
667
668             /* Change the insertion mode to "in frameset". */
669             $this->mode = self::IN_FRAMESET;
670
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'))) {
675             // parse error
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);
681
682         // inversion of specification
683         } elseif(
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')) {
688             // parse error
689
690         /* Anything else */
691         } else {
692             $this->emitToken(array(
693                 'name' => 'body',
694                 'type' => HTML5_Tokenizer::STARTTAG,
695                 'attr' => array()
696             ));
697             $this->flag_frameset_ok = true;
698             $this->emitToken($token);
699         }
700         break;
701
702     case self::IN_BODY:
703         /* Handle the token as follows: */
704
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();
711
712                 /* Append the token's character to the current node. */
713                 $this->insertText($token['data']);
714
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;
721                 }
722             break;
723
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']);
729             break;
730
731             case HTML5_Tokenizer::DOCTYPE:
732                 // parse error
733             break;
734
735             case HTML5_Tokenizer::STARTTAG:
736             switch($token['name']) {
737                 case 'html':
738                     // parse error
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']);
746                         }
747                     }
748                 break;
749
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
753                     head". */
754                     $this->processWithRulesFor($token, self::IN_HEAD);
755                 break;
756
757                 /* A start tag token with the tag name "body" */
758                 case '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.
762                     (fragment case) */
763                     if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
764                         $this->ignored = true;
765                         // Ignore
766
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
771                     element. */
772                     } else {
773                         foreach($token['attr'] as $attr) {
774                             if(!$this->stack[1]->hasAttribute($attr['name'])) {
775                                 $this->stack[1]->setAttribute($attr['name'], $attr['value']);
776                             }
777                         }
778                     }
779                 break;
780
781                 case 'frameset':
782                     // parse error
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.
786                      * (fragment case) */
787                     if(count($this->stack) === 1 || $this->stack[1]->tagName !== 'body') {
788                         $this->ignored = true;
789                         // Ignore
790                     } elseif (!$this->flag_frameset_ok) {
791                         $this->ignored = true;
792                         // Ignore
793                     } else {
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]);
798                         }
799
800                         /* 2. Pop all the nodes from the bottom of the stack of 
801                          * open elements, from the current node up to the root 
802                          * html element. */
803                         array_splice($this->stack, 1);
804
805                         $this->insertElement($token);
806                         $this->mode = self::IN_FRAMESET;
807                     }
808                 break;
809
810                 // in spec, there is a diversion here
811
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
819                     seen. */
820                     if($this->elementInScope('p')) {
821                         $this->emitToken(array(
822                             'name' => 'p',
823                             'type' => HTML5_Tokenizer::ENDTAG
824                         ));
825                     }
826
827                     /* Insert an HTML element for the token. */
828                     $this->insertElement($token);
829                 break;
830
831                 /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
832                 "h5", "h6" */
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(
838                             'name' => 'p',
839                             'type' => HTML5_Tokenizer::ENDTAG
840                         ));
841                     }
842
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
846                      * elements. */
847                     $peek = array_pop($this->stack);
848                     if (in_array($peek->tagName, array("h1", "h2", "h3", "h4", "h5", "h6"))) {
849                         // parse error
850                     } else {
851                         $this->stack[] = $peek;
852                     }
853
854                     /* Insert an HTML element for the token. */
855                     $this->insertElement($token);
856                 break;
857
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(
863                             'name' => 'p',
864                             'type' => HTML5_Tokenizer::ENDTAG
865                         ));
866                     }
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;
874                 break;
875
876                 /* A start tag whose tag name is "form" */
877                 case '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;
882                         // Ignore.
883
884                     /* Otherwise: */
885                     } else {
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
888                         had been seen. */
889                         if($this->elementInScope('p')) {
890                             $this->emitToken(array(
891                                 'name' => 'p',
892                                 'type' => HTML5_Tokenizer::ENDTAG
893                             ));
894                         }
895
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;
900                     }
901                 break;
902
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;
907
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). */
912                         $stop = false;
913                         $node = $this->stack[$n];
914                         $cat  = $this->getElementCategory($node);
915
916                         // for case 'li':
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,
929                             ));
930                             break;
931                         }
932
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') {
939                             break;
940                         }
941
942                         /* 5. Otherwise, set node to the previous entry in the
943                          * stack of open elements and return to step 2. */
944                     }
945
946                     /* 6. This is the last step. */
947
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
950                     seen. */
951                     if($this->elementInScope('p')) {
952                         $this->emitToken(array(
953                             'name' => 'p',
954                             'type' => HTML5_Tokenizer::ENDTAG
955                         ));
956                     }
957
958                     /* Finally, insert an HTML element with the same tag
959                     name as the    token's. */
960                     $this->insertElement($token);
961                 break;
962
963                 /* A start tag token whose tag name is "plaintext" */
964                 case '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
967                     seen. */
968                     if($this->elementInScope('p')) {
969                         $this->emitToken(array(
970                             'name' => 'p',
971                             'type' => HTML5_Tokenizer::ENDTAG
972                         ));
973                     }
974
975                     /* Insert an HTML element for the token. */
976                     $this->insertElement($token);
977
978                     $this->content_model = HTML5_Tokenizer::PLAINTEXT;
979                 break;
980
981                 // more diversions
982
983                 /* A start tag whose tag name is "a" */
984                 case '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);
995
996                     for($n = $leng - 1; $n >= 0; $n--) {
997                         if($this->a_formatting[$n] === self::MARKER) {
998                             break;
999
1000                         } elseif($this->a_formatting[$n]->tagName === 'a') {
1001                             $a = $this->a_formatting[$n];
1002                             $this->emitToken(array(
1003                                 'name' => 'a',
1004                                 'type' => HTML5_Tokenizer::ENDTAG
1005                             ));
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);
1009                             }
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);
1013                             }
1014                             break;
1015                         }
1016                     }
1017
1018                     /* Reconstruct the active formatting elements, if any. */
1019                     $this->reconstructActiveFormattingElements();
1020
1021                     /* Insert an HTML element for the token. */
1022                     $el = $this->insertElement($token);
1023
1024                     /* Add that element to the list of active formatting
1025                     elements. */
1026                     $this->a_formatting[] = $el;
1027                 break;
1028
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();
1034
1035                     /* Insert an HTML element for the token. */
1036                     $el = $this->insertElement($token);
1037
1038                     /* Add that element to the list of active formatting
1039                     elements. */
1040                     $this->a_formatting[] = $el;
1041                 break;
1042
1043                 case 'nobr':
1044                     /* Reconstruct the active formatting elements, if any. */
1045                     $this->reconstructActiveFormattingElements();
1046
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(
1053                             'name' => 'nobr',
1054                             'type' => HTML5_Tokenizer::ENDTAG,
1055                         ));
1056                         $this->reconstructActiveFormattingElements();
1057                     }
1058
1059                     /* Insert an HTML element for the token. */
1060                     $el = $this->insertElement($token);
1061
1062                     /* Add that element to the list of active formatting
1063                     elements. */
1064                     $this->a_formatting[] = $el;
1065                 break;
1066
1067                 // another diversion
1068
1069                 /* A start tag token whose tag name is "button" */
1070                 case '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(
1077                             'name' => 'button',
1078                             'type' => HTML5_Tokenizer::ENDTAG
1079                         ));
1080                     }
1081
1082                     /* Reconstruct the active formatting elements, if any. */
1083                     $this->reconstructActiveFormattingElements();
1084
1085                     /* Insert an HTML element for the token. */
1086                     $this->insertElement($token);
1087
1088                     /* Insert a marker at the end of the list of active
1089                     formatting elements. */
1090                     $this->a_formatting[] = self::MARKER;
1091
1092                     $this->flag_frameset_ok = false;
1093                 break;
1094
1095                 case 'applet': case 'marquee': case 'object':
1096                     /* Reconstruct the active formatting elements, if any. */
1097                     $this->reconstructActiveFormattingElements();
1098
1099                     /* Insert an HTML element for the token. */
1100                     $this->insertElement($token);
1101
1102                     /* Insert a marker at the end of the list of active
1103                     formatting elements. */
1104                     $this->a_formatting[] = self::MARKER;
1105
1106                     $this->flag_frameset_ok = false;
1107                 break;
1108
1109                 // spec diversion
1110
1111                 /* A start tag whose tag name is "table" */
1112                 case '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(
1118                             'name' => 'p',
1119                             'type' => HTML5_Tokenizer::ENDTAG
1120                         ));
1121                     }
1122
1123                     /* Insert an HTML element for the token. */
1124                     $this->insertElement($token);
1125
1126                     $this->flag_frameset_ok = false;
1127
1128                     /* Change the insertion mode to "in table". */
1129                     $this->mode = self::IN_TABLE;
1130                 break;
1131
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':
1136                 case 'wbr':
1137                     /* Reconstruct the active formatting elements, if any. */
1138                     $this->reconstructActiveFormattingElements();
1139
1140                     /* Insert an HTML element for the token. */
1141                     $this->insertElement($token);
1142
1143                     /* Immediately pop the current node off the stack of open elements. */
1144                     array_pop($this->stack);
1145
1146                     // YYY: Acknowledge the token's self-closing flag, if it is set.
1147
1148                     $this->flag_frameset_ok = false;
1149                 break;
1150
1151                 case 'param': case 'source':
1152                     /* Insert an HTML element for the token. */
1153                     $this->insertElement($token);
1154
1155                     /* Immediately pop the current node off the stack of open elements. */
1156                     array_pop($this->stack);
1157
1158                     // YYY: Acknowledge the token's self-closing flag, if it is set.
1159                 break;
1160
1161                 /* A start tag whose tag name is "hr" */
1162                 case '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(
1167                             'name' => 'p',
1168                             'type' => HTML5_Tokenizer::ENDTAG
1169                         ));
1170                     }
1171
1172                     /* Insert an HTML element for the token. */
1173                     $this->insertElement($token);
1174
1175                     /* Immediately pop the current node off the stack of open elements. */
1176                     array_pop($this->stack);
1177
1178                     // YYY: Acknowledge the token's self-closing flag, if it is set.
1179
1180                     $this->flag_frameset_ok = false;
1181                 break;
1182
1183                 /* A start tag whose tag name is "image" */
1184                 case '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);
1189                 break;
1190
1191                 /* A start tag whose tag name is "isindex" */
1192                 case 'isindex':
1193                     /* Parse error. */
1194
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
1199                         been seen. */
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
1203                          * the token. */
1204                         $attr = array();
1205                         $action = $this->getAttr($token, 'action');
1206                         if ($action !== false) {
1207                             $attr[] = array('name' => 'action', 'value' => $action);
1208                         }
1209                         $this->emitToken(array(
1210                             'name' => 'form',
1211                             'type' => HTML5_Tokenizer::STARTTAG,
1212                             'attr' => $attr
1213                         ));
1214
1215                         /* Act as if a start tag token with the tag name "hr" had
1216                         been seen. */
1217                         $this->emitToken(array(
1218                             'name' => 'hr',
1219                             'type' => HTML5_Tokenizer::STARTTAG,
1220                             'attr' => array()
1221                         ));
1222
1223                         /* Act as if a start tag token with the tag name "p" had
1224                         been seen. */
1225                         $this->emitToken(array(
1226                             'name' => 'p',
1227                             'type' => HTML5_Tokenizer::STARTTAG,
1228                             'attr' => array()
1229                         ));
1230
1231                         /* Act as if a start tag token with the tag name "label"
1232                         had been seen. */
1233                         $this->emitToken(array(
1234                             'name' => 'label',
1235                             'type' => HTML5_Tokenizer::STARTTAG,
1236                             'attr' => array()
1237                         ));
1238
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: ';
1244                         }
1245                         $this->emitToken(array(
1246                             'data' => $prompt,
1247                             'type' => HTML5_Tokenizer::CHARACTER,
1248                         ));
1249
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). */
1254                         $attr = array();
1255                         foreach ($token['attr'] as $keypair) {
1256                             if ($keypair['name'] === 'name' || $keypair['name'] === 'action' ||
1257                                 $keypair['name'] === 'prompt') continue;
1258                             $attr[] = $keypair;
1259                         }
1260                         $attr[] = array('name' => 'name', 'value' => 'isindex');
1261
1262                         $this->emitToken(array(
1263                             'name' => 'input',
1264                             'type' => HTML5_Tokenizer::STARTTAG,
1265                             'attr' => $attr
1266                         ));
1267
1268                         /* Act as if an end tag token with the tag name "label"
1269                         had been seen. */
1270                         $this->emitToken(array(
1271                             'name' => 'label',
1272                             'type' => HTML5_Tokenizer::ENDTAG
1273                         ));
1274
1275                         /* Act as if an end tag token with the tag name "p" had
1276                         been seen. */
1277                         $this->emitToken(array(
1278                             'name' => 'p',
1279                             'type' => HTML5_Tokenizer::ENDTAG
1280                         ));
1281
1282                         /* Act as if a start tag token with the tag name "hr" had
1283                         been seen. */
1284                         $this->emitToken(array(
1285                             'name' => 'hr',
1286                             'type' => HTML5_Tokenizer::STARTTAG
1287                         ));
1288
1289                         /* Act as if an end tag token with the tag name "form" had
1290                         been seen. */
1291                         $this->emitToken(array(
1292                             'name' => 'form',
1293                             'type' => HTML5_Tokenizer::ENDTAG
1294                         ));
1295                     } else {
1296                         $this->ignored = true;
1297                     }
1298                 break;
1299
1300                 /* A start tag whose tag name is "textarea" */
1301                 case 'textarea':
1302                     $this->insertElement($token);
1303
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;
1310
1311                     $this->original_mode = $this->mode;
1312                     $this->flag_frameset_ok = false;
1313                     $this->mode = self::IN_CDATA_RCDATA;
1314
1315                     /* Switch the tokeniser's content model flag to the
1316                     RCDATA state. */
1317                     $this->content_model = HTML5_Tokenizer::RCDATA;
1318                 break;
1319
1320                 /* A start tag token whose tag name is "xmp" */
1321                 case 'xmp':
1322                     /* Reconstruct the active formatting elements, if any. */
1323                     $this->reconstructActiveFormattingElements();
1324
1325                     $this->flag_frameset_ok = false;
1326
1327                     $this->insertCDATAElement($token);
1328                 break;
1329
1330                 case 'iframe':
1331                     $this->flag_frameset_ok = false;
1332                     $this->insertCDATAElement($token);
1333                 break;
1334
1335                 case 'noembed': case 'noscript':
1336                     // XSCRIPT: should check scripting flag
1337                     $this->insertCDATAElement($token);
1338                 break;
1339
1340                 /* A start tag whose tag name is "select" */
1341                 case 'select':
1342                     /* Reconstruct the active formatting elements, if any. */
1343                     $this->reconstructActiveFormattingElements();
1344
1345                     /* Insert an HTML element for the token. */
1346                     $this->insertElement($token);
1347
1348                     $this->flag_frameset_ok = false;
1349
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
1354                      * select". */
1355                     if (
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
1359                     ) {
1360                         $this->mode = self::IN_SELECT_IN_TABLE;
1361                     } else {
1362                         $this->mode = self::IN_SELECT;
1363                     }
1364                 break;
1365
1366                 case 'option': case 'optgroup':
1367                     if ($this->elementInScope('option')) {
1368                         $this->emitToken(array(
1369                             'name' => 'option',
1370                             'type' => HTML5_Tokenizer::ENDTAG,
1371                         ));
1372                     }
1373                     $this->reconstructActiveFormattingElements();
1374                     $this->insertElement($token);
1375                 break;
1376
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.
1382                      */
1383                     if ($this->elementInScope('ruby')) {
1384                         $this->generateImpliedEndTags();
1385                     }
1386                     $peek = false;
1387                     do {
1388                         if ($peek) {
1389                             // parse error
1390                         }
1391                         $peek = array_pop($this->stack);
1392                     } while ($peek->tagName !== 'ruby');
1393                     $this->stack[] = $peek; // we popped one too many
1394                     $this->insertElement($token);
1395                 break;
1396
1397                 // spec diversion
1398
1399                 case 'math':
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);
1407                     }
1408                     if ($this->mode !== self::IN_FOREIGN_CONTENT) {
1409                         $this->secondary_mode = $this->mode;
1410                         $this->mode = self::IN_FOREIGN_CONTENT;
1411                     }
1412                 break;
1413
1414                 case 'svg':
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);
1422                     }
1423                     if ($this->mode !== self::IN_FOREIGN_CONTENT) {
1424                         $this->secondary_mode = $this->mode;
1425                         $this->mode = self::IN_FOREIGN_CONTENT;
1426                     }
1427                 break;
1428
1429                 case 'caption': case 'col': case 'colgroup': case 'frame': case 'head':
1430                 case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': case 'tr':
1431                     // parse error
1432                 break;
1433
1434                 /* Google */
1435                 case 'g:plusone':
1436                                         /* Reconstruct the active formatting elements, if any. */
1437                     $this->reconstructActiveFormattingElements();
1438
1439                     $this->insertForeignElement($token, self::NS_GOOGLE);
1440                 break;
1441
1442                 /* A start tag token not covered by the previous entries */
1443                 default:
1444                     /* Reconstruct the active formatting elements, if any. */
1445                     $this->reconstructActiveFormattingElements();
1446
1447                     $this->insertElement($token);
1448                     /* This element will be a phrasing  element. */
1449                 break;
1450             }
1451             break;
1452
1453             case HTML5_Tokenizer::ENDTAG:
1454             switch($token['name']) {
1455                 /* An end tag with the tag name "body" */
1456                 case '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.
1459                     (innerHTML case) */
1460                     if(count($this->stack) < 2 || $this->stack[1]->tagName !== 'body') {
1461                         $this->ignored = true;
1462
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
1470                      * parse error. */
1471                     } else {
1472                         // XERROR: implement this check for parse error
1473                     }
1474
1475                     /* Change the insertion mode to "after body". */
1476                     $this->mode = self::AFTER_BODY;
1477                 break;
1478
1479                 /* An end tag with the tag name "html" */
1480                 case '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
1483                     token. */
1484                     $this->emitToken(array(
1485                         'name' => 'body',
1486                         'type' => HTML5_Tokenizer::ENDTAG
1487                     ));
1488
1489                     if (!$this->ignored) $this->emitToken($token);
1490                 break;
1491
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();
1502
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
1507
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. */
1512                         do {
1513                             $node = array_pop($this->stack);
1514                         } while ($node->tagName !== $token['name']);
1515                     } else {
1516                         // parse error
1517                     }
1518                 break;
1519
1520                 /* An end tag whose tag name is "form" */
1521                 case '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)) {
1529                         // parse error
1530                         $this->ignored = true;
1531                     } else {
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) {
1536                             // parse error
1537                         }
1538                         /* 3. Remove node from the stack of open elements. */
1539                         array_splice($this->stack, array_search($node, $this->stack, true), 1);
1540                     }
1541
1542                 break;
1543
1544                 /* An end tag whose tag name is "p" */
1545                 case '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'));
1552
1553                         /* If the current node is not a p element, then this is
1554                         a parse error. */
1555                         // XERROR: implement
1556
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. */
1560                         do {
1561                             $node = array_pop($this->stack);
1562                         } while ($node->tagName !== 'p');
1563
1564                     } else {
1565                         // parse error
1566                         $this->emitToken(array(
1567                             'name' => 'p',
1568                             'type' => HTML5_Tokenizer::STARTTAG,
1569                         ));
1570                         $this->emitToken($token);
1571                     }
1572                 break;
1573
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']));
1578
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
1582
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. */
1586                         do {
1587                             $node = array_pop($this->stack);
1588                         } while ($node->tagName !== $token['name']);
1589
1590                     } else {
1591                         // parse error
1592                     }
1593                 break;
1594
1595                 /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
1596                 "h5", "h6" */
1597                 case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
1598                     $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
1599
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();
1605
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
1609
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. */
1614                         do {
1615                             $node = array_pop($this->stack);
1616                         } while (!in_array($node->tagName, $elements));
1617                     } else {
1618                         // parse error
1619                     }
1620                 break;
1621
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
1632                         otherwise, and
1633                         * has the same tag name as the token.
1634                     */
1635                     while(true) {
1636                         for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
1637                             if($this->a_formatting[$a] === self::MARKER) {
1638                                 break;
1639
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);
1643                                 $fe_af_pos = $a;
1644                                 break;
1645                             }
1646                         }
1647
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;
1655                             break;
1656
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);
1664                             break;
1665                         }
1666
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
1673
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
1678                         not be one. */
1679                         $fe_s_pos = array_search($formatting_element, $this->stack, true);
1680                         $length = count($this->stack);
1681
1682                         for($s = $fe_s_pos + 1; $s < $length; $s++) {
1683                             $category = $this->getElementCategory($this->stack[$s]);
1684
1685                             if($category !== self::PHRASING && $category !== self::FORMATTING) {
1686                                 $furthest_block = $this->stack[$s];
1687                                 break;
1688                             }
1689                         }
1690
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);
1700                             }
1701
1702                             unset($this->a_formatting[$fe_af_pos]);
1703                             $this->a_formatting = array_merge($this->a_formatting);
1704                             break;
1705                         }
1706
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];
1711
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;
1717
1718                         /* 6. Let node and last node  be the furthest block.
1719                         Follow these steps: */
1720                         $node = $furthest_block;
1721                         $last_node = $furthest_block;
1722
1723                         while(true) {
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];
1728
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
1732                                 to step 1. */
1733                                 if(!in_array($node, $this->a_formatting, true)) {
1734                                     array_splice($this->stack, $n, 1);
1735
1736                                 } else {
1737                                     break;
1738                                 }
1739                             }
1740
1741                             /* 6.3 Otherwise, if node is the formatting
1742                             element, then go to the next step in the overall
1743                             algorithm. */
1744                             if($node === $formatting_element) {
1745                                 break;
1746
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;
1753                             }
1754
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;
1768                             $node = $clone;
1769
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);
1774                             }
1775
1776                             $node->appendChild($last_node);
1777
1778                             /* 6.7 Let last node be node. */
1779                             $last_node = $node;
1780
1781                             /* 6.8 Return to step 1 of this inner set of steps. */
1782                         }
1783
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
1788                          * node if any. */
1789                         if ($last_node->parentNode) { // common step
1790                             $last_node->parentNode->removeChild($last_node);
1791                         }
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
1797                          * any. */
1798                         } else {
1799                             $common_ancestor->appendChild($last_node);
1800                         }
1801
1802                         /* 8. Create an element for the token for which the
1803                          * formatting element was created. */
1804                         $clone = $formatting_element->cloneNode();
1805
1806                         /* 9. Take all of the child nodes of the furthest
1807                         block and append them to the element created in the
1808                         last step. */
1809                         while($furthest_block->hasChildNodes()) {
1810                             $child = $furthest_block->firstChild;
1811                             $furthest_block->removeChild($child);
1812                             $clone->appendChild($child);
1813                         }
1814
1815                         /* 10. Append that clone to the furthest block. */
1816                         $furthest_block->appendChild($clone);
1817
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);
1824
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);
1828
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);
1835
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);
1840
1841                         /* 13. Jump back to step 1 in this series of steps. */
1842                         unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
1843                     }
1844                 break;
1845
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
1849                     tags. */
1850                     if($this->elementInScope($token['name'])) {
1851                         $this->generateImpliedEndTags();
1852
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
1856
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. */
1860                         do {
1861                             $node = array_pop($this->stack);
1862                         } while ($node->tagName !== $token['name']);
1863
1864                         /* Clear the list of active formatting elements up to the
1865                          * last marker. */
1866                         $keys = array_keys($this->a_formatting, self::MARKER, true);
1867                         $marker = end($keys);
1868
1869                         for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
1870                             array_pop($this->a_formatting);
1871                         }
1872                     } else {
1873                         // parse error
1874                     }
1875                 break;
1876
1877                 case 'br':
1878                     // Parse error
1879                     $this->emitToken(array(
1880                         'name' => 'br',
1881                         'type' => HTML5_Tokenizer::STARTTAG,
1882                     ));
1883                 break;
1884
1885                 /* An end tag token not covered by the previous entries */
1886                 default:
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];
1891
1892                         /* If node has the same tag name as the end tag token,
1893                         then: */
1894                         if($token['name'] === $node->tagName) {
1895                             /* Generate implied end tags. */
1896                             $this->generateImpliedEndTags();
1897
1898                             /* If the tag name of the end tag token does not
1899                             match the tag name of the current node, this is a
1900                             parse error. */
1901                             // XERROR: implement this
1902
1903                             /* Pop all the nodes from the current node up to
1904                             node, including node, then stop these steps. */
1905                             // XSKETCHY
1906                             do {
1907                                 $pop = array_pop($this->stack);
1908                             } while ($pop !== $node);
1909                             break;
1910
1911                         } else {
1912                             $category = $this->getElementCategory($node);
1913
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
1918                                 is ignored. */
1919                                 $this->ignored = true;
1920                                 break;
1921                                 // parse error
1922                             }
1923                         }
1924                         /* Set node to the previous entry in the stack of open elements. Loop. */
1925                     }
1926                 break;
1927             }
1928             break;
1929         }
1930         break;
1931
1932     case self::IN_CDATA_RCDATA:
1933         if (
1934             $token['type'] === HTML5_Tokenizer::CHARACTER ||
1935             $token['type'] === HTML5_Tokenizer::SPACECHARACTER
1936         ) {
1937             $this->insertText($token['data']);
1938         } elseif ($token['type'] === HTML5_Tokenizer::EOF) {
1939             // parse error
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;
1953         }
1954     break;
1955
1956     case self::IN_TABLE:
1957         $clear = array('html', 'table');
1958
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),
1961         or U+0020 SPACE */
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']);
1970
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']);
1976
1977         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
1978             // parse error
1979
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);
1985
1986             /* Insert a marker at the end of the list of active
1987             formatting elements. */
1988             $this->a_formatting[] = self::MARKER;
1989
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;
1994
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);
2000
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;
2005
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,
2012                 'attr' => array()
2013             ));
2014
2015             $this->emitToken($token);
2016
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);
2022
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;
2027
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(
2034                 'name' => 'tbody',
2035                 'type' => HTML5_Tokenizer::STARTTAG,
2036                 'attr' => array()
2037             ));
2038
2039             $this->emitToken($token);
2040
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
2046             current token. */
2047             $this->emitToken(array(
2048                 'name' => 'table',
2049                 'type' => HTML5_Tokenizer::ENDTAG
2050             ));
2051
2052             if (!$this->ignored) $this->emitToken($token);
2053
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;
2062
2063             /* Otherwise: */
2064             } else {
2065                 do {
2066                     $node = array_pop($this->stack);
2067                 } while ($node->tagName !== 'table');
2068
2069                 /* Reset the insertion mode appropriately. */
2070                 $this->resetInsertionMode();
2071             }
2072
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.
2079
2080         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2081         ($token['name'] === 'style' || $token['name'] === 'script')) {
2082             $this->processWithRulesFor($token, self::IN_HEAD);
2083
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'
2092             /* Otherwise */
2093             // parse error
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.
2100                 // parse error
2101             }
2102             /* Stop parsing. */
2103         /* Anything else */
2104         } else {
2105             /* Parse error. Process the token as if the insertion mode was "in
2106             body", with the following exception: */
2107
2108             $old = $this->foster_parent;
2109             $this->foster_parent = true;
2110             $this->processWithRulesFor($token, self::IN_BODY);
2111             $this->foster_parent = $old;
2112         }
2113     break;
2114
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;
2123                 // Ignore
2124
2125             /* Otherwise: */
2126             } else {
2127                 /* Generate implied end tags. */
2128                 $this->generateImpliedEndTags();
2129
2130                 /* Now, if the current node is not a caption element, then this
2131                 is a parse error. */
2132                 // XERROR: implement
2133
2134                 /* Pop elements from this stack until a caption element has
2135                 been popped from the stack. */
2136                 do {
2137                     $node = array_pop($this->stack);
2138                 } while ($node->tagName !== 'caption');
2139
2140                 /* Clear the list of active formatting elements up to the last
2141                 marker. */
2142                 $this->clearTheActiveFormattingElementsUpToTheLastMarker();
2143
2144                 /* Switch the insertion mode to "in table". */
2145                 $this->mode = self::IN_TABLE;
2146             }
2147
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
2150         name is "table" */
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
2157             current token. */
2158             $this->emitToken(array(
2159                 'name' => 'caption',
2160                 'type' => HTML5_Tokenizer::ENDTAG
2161             ));
2162
2163             if (!$this->ignored) $this->emitToken($token);
2164
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',
2169         'thead', 'tr'))) {
2170             // Parse error. Ignore the token.
2171             $this->ignored = true;
2172
2173         /* Anything else */
2174         } else {
2175             /* Process the token as if the insertion mode was "in body". */
2176             $this->processWithRulesFor($token, self::IN_BODY);
2177         }
2178     break;
2179
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),
2183         or U+0020 SPACE */
2184         if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2185             /* Append the character to the current node. */
2186             $this->insertText($token['data']);
2187
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']);
2193
2194         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2195             // parse error
2196
2197         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2198             $this->processWithRulesFor($token, self::IN_BODY);
2199
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.
2207
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;
2215
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". */
2219             } else {
2220                 array_pop($this->stack);
2221                 $this->mode = self::IN_TABLE;
2222             }
2223
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;
2228
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') {
2232             /* Stop parsing */
2233
2234         /* Anything else */
2235         } else {
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
2241             ));
2242
2243             if (!$this->ignored) $this->emitToken($token);
2244         }
2245     break;
2246
2247     case self::IN_TABLE_BODY:
2248         $clear = array('tbody', 'tfoot', 'thead', 'html');
2249
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);
2254
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;
2259
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(
2266                 'name' => 'tr',
2267                 'type' => HTML5_Tokenizer::STARTTAG,
2268                 'attr' => array()
2269             ));
2270
2271             $this->emitToken($token);
2272
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)) {
2280                 // Parse error
2281                 $this->ignored = true;
2282
2283             /* Otherwise: */
2284             } else {
2285                 /* Clear the stack back to a table body context. */
2286                 $this->clearStackToTableContext($clear);
2287
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;
2292             }
2293
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)) {
2303                 // parse error
2304                 $this->ignored = true;
2305
2306             /* Otherwise: */
2307             } else {
2308                 /* Clear the stack back to a table body context. */
2309                 $this->clearStackToTableContext($clear);
2310
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
2317                 ));
2318
2319                 $this->emitToken($token);
2320             }
2321
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;
2328
2329         /* Anything else */
2330         } else {
2331             /* Process the token as if the insertion mode was "in table". */
2332             $this->processWithRulesFor($token, self::IN_TABLE);
2333         }
2334     break;
2335
2336     case self::IN_ROW:
2337         $clear = array('tr', 'html');
2338
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);
2344
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;
2349
2350             /* Insert a marker at the end of the list of active formatting
2351             elements. */
2352             $this->a_formatting[] = self::MARKER;
2353
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)) {
2360                 // Ignore.
2361                 $this->ignored = true;
2362
2363             /* Otherwise: */
2364             } else {
2365                 /* Clear the stack back to a table row context. */
2366                 $this->clearStackToTableContext($clear);
2367
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
2370                 body". */
2371                 array_pop($this->stack);
2372                 $this->mode = self::IN_TABLE_BODY;
2373             }
2374
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(
2383                 'name' => 'tr',
2384                 'type' => HTML5_Tokenizer::ENDTAG
2385             ));
2386             if (!$this->ignored) $this->emitToken($token);
2387
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;
2396
2397             /* Otherwise: */
2398             } else {
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(
2402                     'name' => 'tr',
2403                     'type' => HTML5_Tokenizer::ENDTAG
2404                 ));
2405
2406                 $this->emitToken($token);
2407             }
2408
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;
2415
2416         /* Anything else */
2417         } else {
2418             /* Process the token as if the insertion mode was "in table". */
2419             $this->processWithRulesFor($token, self::IN_TABLE);
2420         }
2421     break;
2422
2423     case self::IN_CELL:
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;
2432
2433             /* Otherwise: */
2434             } else {
2435                 /* Generate implied end tags, except for elements with the same
2436                 tag name as the token. */
2437                 $this->generateImpliedEndTags(array($token['name']));
2438
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
2442
2443                 /* Pop elements from this stack until an element with the same
2444                 tag name as the token has been popped from the stack. */
2445                 do {
2446                     $node = array_pop($this->stack);
2447                 } while ($node->tagName !== $token['name']);
2448
2449                 /* Clear the list of active formatting elements up to the last
2450                 marker. */
2451                 $this->clearTheActiveFormattingElementsUpToTheLastMarker();
2452
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;
2456             }
2457
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',
2462         'thead', 'tr'))) {
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.
2465             (fragment case) */
2466             if(!$this->elementInScope(array('td', 'th'), true)) {
2467                 // parse error
2468                 $this->ignored = true;
2469
2470             /* Otherwise, close the cell (see below) and reprocess the current
2471             token. */
2472             } else {
2473                 $this->closeCell();
2474                 $this->emitToken($token);
2475             }
2476
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;
2483
2484         /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
2485         "thead", "tr" */
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.
2490             (innerHTML case) */
2491             if(!$this->elementInScope(array('td', 'th'), true)) {
2492                 // Parse error
2493                 $this->ignored = true;
2494
2495             /* Otherwise, close the cell (see below) and reprocess the current
2496             token. */
2497             } else {
2498                 $this->closeCell();
2499                 $this->emitToken($token);
2500             }
2501
2502         /* Anything else */
2503         } else {
2504             /* Process the token as if the insertion mode was "in body". */
2505             $this->processWithRulesFor($token, self::IN_BODY);
2506         }
2507     break;
2508
2509     case self::IN_SELECT:
2510         /* Handle the token as follows: */
2511
2512         /* A character token */
2513         if(
2514             $token['type'] === HTML5_Tokenizer::CHARACTER ||
2515             $token['type'] === HTML5_Tokenizer::SPACECHARACTER
2516         ) {
2517             /* Append the token's character to the current node. */
2518             $this->insertText($token['data']);
2519
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']);
2525
2526         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2527             // parse error
2528
2529         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2530             $this->processWithRulesFor($token, self::INBODY);
2531
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(
2539                     'name' => 'option',
2540                     'type' => HTML5_Tokenizer::ENDTAG
2541                 ));
2542             }
2543
2544             /* Insert an HTML element for the token. */
2545             $this->insertElement($token);
2546
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(
2554                     'name' => 'option',
2555                     'type' => HTML5_Tokenizer::ENDTAG
2556                 ));
2557             }
2558
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
2565                 ));
2566             }
2567
2568             /* Insert an HTML element for the token. */
2569             $this->insertElement($token);
2570
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
2577             been seen. */
2578             $elements_in_stack = count($this->stack);
2579
2580             if($this->stack[$elements_in_stack - 1]->tagName === 'option' &&
2581             $this->stack[$elements_in_stack - 2]->tagName === 'optgroup') {
2582                 $this->emitToken(array(
2583                     'name' => 'option',
2584                     'type' => HTML5_Tokenizer::ENDTAG
2585                 ));
2586             }
2587
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);
2593             } else {
2594                 // parse error
2595                 $this->ignored = true;
2596             }
2597
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);
2606             } else {
2607                 // parse error
2608                 $this->ignored = true;
2609             }
2610
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;
2619                 // parse error
2620
2621             /* Otherwise: */
2622             } else {
2623                 /* Pop elements from the stack of open elements until a select
2624                 element has been popped from the stack. */
2625                 do {
2626                     $node = array_pop($this->stack);
2627                 } while ($node->tagName !== 'select');
2628
2629                 /* Reset the insertion mode appropriately. */
2630                 $this->resetInsertionMode();
2631             }
2632
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(
2638                 'name' => 'select',
2639                 'type' => HTML5_Tokenizer::ENDTAG
2640             ));
2641
2642         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2643         ($token['name'] === 'input' || $token['name'] === 'textarea')) {
2644             // parse error
2645             $this->emitToken(array(
2646                 'name' => 'select',
2647                 'type' => HTML5_Tokenizer::ENDTAG
2648             ));
2649             $this->emitToken($token);
2650
2651         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'script') {
2652             $this->processWithRulesFor($token, self::IN_HEAD);
2653
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.
2656             /* Stop parsing */
2657
2658         /* Anything else */
2659         } else {
2660             /* Parse error. Ignore the token. */
2661             $this->ignored = true;
2662         }
2663     break;
2664
2665     case self::IN_SELECT_IN_TABLE:
2666
2667         if($token['type'] === HTML5_Tokenizer::STARTTAG &&
2668         in_array($token['name'], array('caption', 'table', 'tbody',
2669         'tfoot', 'thead', 'tr', 'td', 'th'))) {
2670             // parse error
2671             $this->emitToken(array(
2672                 'name' => 'select',
2673                 'type' => HTML5_Tokenizer::ENDTAG,
2674             ));
2675             $this->emitToken($token);
2676
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')))  {
2681             /* Parse error. */
2682             // parse error
2683
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(
2690                     'name' => 'select',
2691                     'type' => HTML5_Tokenizer::ENDTAG
2692                 ));
2693
2694                 $this->emitToken($token);
2695             } else {
2696                 $this->ignored = true;
2697             }
2698         } else {
2699             $this->processWithRulesFor($token, self::IN_SELECT);
2700         }
2701     break;
2702
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
2716         } elseif (
2717             ($token['type'] === HTML5_Tokenizer::STARTTAG &&
2718                 ((
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'))
2723                 ) ||
2724                 (
2725                     $token['name'] === 'svg' &&
2726                     end($this->stack)->namespaceURI === self::NS_MATHML &&
2727                     end($this->stack)->tagName === 'annotation-xml'
2728                 ) ||
2729                 (
2730                     end($this->stack)->namespaceURI === self::NS_SVG &&
2731                     in_array(end($this->stack)->tagName, array('foreignObject', 'desc', 'title'))
2732                 ) ||
2733                 (
2734                     // XSKETCHY
2735                     end($this->stack)->namespaceURI === self::NS_HTML
2736                 ))
2737             ) || $token['type'] === HTML5_Tokenizer::ENDTAG
2738         ) {
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) {
2745                 $found = false;
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) {
2750                         $found = true;
2751                         break;
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)) {
2756                         break;
2757                     }
2758                 }
2759                 if (!$found) {
2760                     $this->mode = $this->secondary_mode;
2761                 }
2762             }
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
2773             do {
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',
2817             );
2818             $current = end($this->stack);
2819             if ($current->namespaceURI === self::NS_MATHML) {
2820                 $token = $this->adjustMathMLAttributes($token);
2821             }
2822             if ($current->namespaceURI === self::NS_SVG &&
2823             isset($svg_lookup[$token['name']])) {
2824                 $token['name'] = $svg_lookup[$token['name']];
2825             }
2826             if ($current->namespaceURI === self::NS_SVG) {
2827                 $token = $this->adjustSVGAttributes($token);
2828             }
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
2834             }
2835         }
2836     break;
2837
2838     case self::AFTER_BODY:
2839         /* Handle the token as follows: */
2840
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),
2843         or U+0020 SPACE */
2844         if($token['type'] === HTML5_Tokenizer::SPACECHARACTER) {
2845             /* Process the token as it would be processed if the insertion mode
2846             was "in body". */
2847             $this->processWithRulesFor($token, self::IN_BODY);
2848
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);
2856
2857         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2858             // parse error
2859
2860         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2861             $this->processWithRulesFor($token, self::IN_BODY);
2862
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
2870
2871             $this->mode = self::AFTER_AFTER_BODY;
2872
2873         } elseif($token['type'] === HTML5_Tokenizer::EOF) {
2874             /* Stop parsing */
2875
2876         /* Anything else */
2877         } else {
2878             /* Parse error. Set the insertion mode to "in body" and reprocess
2879             the token. */
2880             $this->mode = self::IN_BODY;
2881             $this->emitToken($token);
2882         }
2883     break;
2884
2885     case self::IN_FRAMESET:
2886         /* Handle the token as follows: */
2887
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']);
2894
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']);
2900
2901         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2902             // parse error
2903
2904         /* A start tag with the tag name "frameset" */
2905         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG &&
2906         $token['name'] === 'frameset') {
2907             $this->insertElement($token);
2908
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;
2916                 // Parse error
2917
2918             } else {
2919                 /* Otherwise, pop the current node from the stack of open
2920                 elements. */
2921                 array_pop($this->stack);
2922
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;
2928             }
2929
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);
2935
2936             /* Immediately pop the current node off the stack of open elements. */
2937             array_pop($this->stack);
2938
2939             // XERROR: Acknowledge the token's self-closing flag, if it is set.
2940
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);
2946
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.
2949             /* Stop parsing */
2950         /* Anything else */
2951         } else {
2952             /* Parse error. Ignore the token. */
2953             $this->ignored = true;
2954         }
2955     break;
2956
2957     case self::AFTER_FRAMESET:
2958         /* Handle the token as follows: */
2959
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']);
2966
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']);
2972
2973         } elseif($token['type'] === HTML5_Tokenizer::DOCTYPE) {
2974             // parse error
2975
2976         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'html') {
2977             $this->processWithRulesFor($token, self::IN_BODY);
2978
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;
2983
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);
2988
2989         } elseif($token['type'] === HTML5_Tokenizer::EOF) {
2990             /* Stop parsing */
2991
2992         /* Anything else */
2993         } else {
2994             /* Parse error. Ignore the token. */
2995             $this->ignored = true;
2996         }
2997     break;
2998
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);
3006
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);
3011
3012         /* An end-of-file token */
3013         } elseif($token['type'] === HTML5_Tokenizer::EOF) {
3014             /* OMG DONE!! */
3015         } else {
3016             // parse error
3017             $this->mode = self::IN_BODY;
3018             $this->emitToken($token);
3019         }
3020     break;
3021
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);
3029
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);
3034
3035         /* An end-of-file token */
3036         } elseif($token['type'] === HTML5_Tokenizer::EOF) {
3037             /* OMG DONE!! */
3038         } elseif($token['type'] === HTML5_Tokenizer::STARTTAG && $token['name'] === 'nofrmaes') {
3039             $this->processWithRulesFor($token, self::IN_HEAD);
3040         } else {
3041             // parse error
3042         }
3043     break;
3044     }
3045         // end funky indenting
3046         }
3047
3048     private function insertElement($token, $append = true) {
3049         $el = $this->dom->createElementNS(self::NS_HTML, $token['name']);
3050                 
3051         if (!empty($token['attr'])) {
3052             foreach($token['attr'] as $attr) {
3053
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
3057
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']);
3061                 }
3062             }
3063         }
3064         if ($append) {
3065             $this->appendToRealParent($el);
3066             $this->stack[] = $el;
3067         }
3068
3069         return $el;
3070     }
3071
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;
3078             }
3079         }
3080         $text = $this->dom->createTextNode($data);
3081         $this->appendToRealParent($text);
3082     }
3083
3084     private function insertComment($data) {
3085         $comment = $this->dom->createComment($data);
3086         $this->appendToRealParent($comment);
3087     }
3088
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);
3097         } else {
3098             $this->fosterParent($node);
3099         }
3100     }
3101
3102     private function elementInScope($el, $table = false) {
3103         if(is_array($el)) {
3104             foreach($el as $element) {
3105                 if($this->elementInScope($element, $table)) {
3106                     return true;
3107                 }
3108             }
3109
3110             return false;
3111         }
3112
3113         $leng = count($this->stack);
3114
3115         for($n = 0; $n < $leng; $n++) {
3116             /* 1. Initialise node to be the current node (the bottommost node of
3117             the stack). */
3118             $node = $this->stack[$leng - 1 - $n];
3119
3120             if($node->tagName === $el) {
3121                 /* 2. If node is the target node, terminate in a match state. */
3122                 return true;
3123
3124             // these are the common states for "in scope" and "in table scope"
3125             } elseif($node->tagName === 'table' || $node->tagName === 'html') {
3126                 return false;
3127
3128             // these are only valid for "in scope"
3129             } elseif(!$table &&
3130             (in_array($node->tagName, array('applet', 'caption', 'td',
3131                 'th', 'button', 'marquee', 'object')) ||
3132                 $node->tagName === 'foreignObject' && $node->namespaceURI === self::NS_SVG)) {
3133                 return false;
3134             }
3135
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
3139             is reached.) */
3140         }
3141     }
3142
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);
3147
3148         if($formatting_elements === 0) {
3149             return false;
3150         }
3151
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);
3155
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
3159         algorithm. */
3160         if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
3161             return false;
3162         }
3163
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. */
3167             if($a === 0) {
3168                 $step_seven = false;
3169                 break;
3170             }
3171
3172             /* 5. Let entry be the entry one earlier than entry in the list of
3173             active formatting elements. */
3174             $a--;
3175             $entry = $this->a_formatting[$a];
3176
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)) {
3180                 break;
3181             }
3182         }
3183
3184         while(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) {
3188                 $a++;
3189                 $entry = $this->a_formatting[$a];
3190             }
3191
3192             /* 8. Perform a shallow clone of the element entry to obtain clone. */
3193             $clone = $entry->cloneNode();
3194
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;
3199
3200             /* 10. Replace the entry for entry in the list with an entry for
3201             clone. */
3202             $this->a_formatting[$a] = $clone;
3203
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) {
3207                 $step_seven = true;
3208             } else {
3209                 break;
3210             }
3211         }
3212     }
3213
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
3217         following steps: */
3218
3219         while(true) {
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);
3223
3224             /* 2. Remove entry from the list of active formatting elements. */
3225             array_pop($this->a_formatting);
3226
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) {
3230                 break;
3231             }
3232         }
3233     }
3234
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);
3243
3244         while(in_array(end($this->stack)->tagName, $elements)) {
3245             array_pop($this->stack);
3246         }
3247     }
3248
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;
3254
3255         elseif(in_array($name, $this->scoping))
3256             return self::SCOPING;
3257
3258         elseif(in_array($name, $this->formatting))
3259             return self::FORMATTING;
3260
3261         else
3262             return self::PHRASING;
3263     }
3264
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
3269         elements. */
3270         while(true) {
3271             $name = end($this->stack)->tagName;
3272
3273             if(in_array($name, $elements)) {
3274                 break;
3275             } else {
3276                 array_pop($this->stack);
3277             }
3278         }
3279     }
3280
3281     private function resetInsertionMode($context = null) {
3282         /* 1. Let last be false. */
3283         $last = false;
3284         $leng = count($this->stack);
3285
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];
3289
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 
3292              * case) */
3293             if($this->stack[0]->isSameNode($node)) {
3294                 $last = true;
3295                 $node = $context;
3296             }
3297
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;
3302                 break;
3303
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;
3308                 break;
3309
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;
3314                 break;
3315
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;
3320                 break;
3321
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;
3326                 break;
3327
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;
3332                 break;
3333
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;
3338                 break;
3339
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;
3348                 break;
3349
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.
3352             (fragment case) */
3353             } elseif($node->tagName === 'head') {
3354                 $this->mode = self::IN_BODY;
3355                 break;
3356
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;
3361                 break;
3362
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;
3367                 break;
3368
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)
3375                     ? self::BEFORE_HEAD
3376                     : self::AFTER_HEAD;
3377
3378                 break;
3379
3380             /* 16. If last is true, then set the insertion mode to "in body"
3381             and    abort these steps. (fragment case) */
3382             } elseif($last) {
3383                 $this->mode = self::IN_BODY;
3384                 break;
3385             }
3386         }
3387     }
3388
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(
3395                     'name' => $cell,
3396                     'type' => HTML5_Tokenizer::ENDTAG
3397                 ));
3398
3399                 break;
3400             }
3401         }
3402     }
3403
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);
3411     }
3412
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;
3418     }
3419
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;
3425     }
3426
3427     private function getAttr($token, $key) {
3428         if (!isset($token['attr'])) return false;
3429         $ret = false;
3430         foreach ($token['attr'] as $keypair) {
3431             if ($keypair['name'] === $key) $ret = $keypair['value'];
3432         }
3433         return $ret;
3434     }
3435
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];
3444             }
3445         }
3446         return $this->stack[0];
3447     }
3448
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];
3464                 break;
3465             }
3466         }
3467
3468         if(isset($table) && $table->parentNode !== null) {
3469             return $table->parentNode;
3470
3471         } elseif(!isset($table)) {
3472             return $this->stack[0];
3473
3474         } elseif(isset($table) && ($table->parentNode === null ||
3475         $table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
3476             return $this->stack[$n - 1];
3477         }
3478     }
3479
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);
3496         } else {
3497             $foster_parent->appendChild($node);
3498         }
3499     }
3500
3501     /**
3502      * For debugging, prints the stack
3503      */
3504     private function printStack() {
3505         $names = array();
3506         foreach ($this->stack as $i => $element) {
3507             $names[] = $element->tagName;
3508         }
3509         echo "  -> stack [" . implode(', ', $names) . "]\n";
3510     }
3511
3512     /**
3513      * For debugging, prints active formatting elements
3514      */
3515     private function printActiveFormattingElements() {
3516         if (!$this->a_formatting) return;
3517         $names = array();
3518         foreach ($this->a_formatting as $node) {
3519             if ($node === self::MARKER) $names[] = 'MARKER';
3520             else $names[] = $node->tagName;
3521         }
3522         echo "  -> active formatting [" . implode(', ', $names) . "]\n";
3523     }
3524
3525     public function currentTableIsTainted() {
3526         return !empty($this->getCurrentTable()->tainted);
3527     }
3528
3529     /**
3530      * Sets up the tree constructor for building a fragment.
3531      */
3532     public function setupContext($context = null) {
3533         $this->fragment = true;
3534         if ($context) {
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;
3541                 break;
3542             case 'style': case 'script': case 'xmp': case 'iframe':
3543             case 'noembed': case 'noframes':
3544                 $this->content_model = HTML5_Tokenizer::CDATA;
3545                 break;
3546             case 'noscript':
3547                 // XSCRIPT: assuming scripting is enabled
3548                 $this->content_model = HTML5_Tokenizer::CDATA;
3549                 break;
3550             case 'plaintext':
3551                 $this->content_model = HTML5_Tokenizer::PLAINTEXT;
3552                 break;
3553             }
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. */
3568             $node = $context;
3569             do {
3570                 if ($node->tagName === 'form') {
3571                     $this->form_pointer = $node;
3572                     break;
3573                 }
3574             } while ($node = $node->parentNode);
3575         }
3576     }
3577
3578     public function adjustMathMLAttributes($token) {
3579         foreach ($token['attr'] as &$kp) {
3580             if ($kp['name'] === 'definitionurl') {
3581                 $kp['name'] = 'definitionURL';
3582             }
3583         }
3584         return $token;
3585     }
3586
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',
3628             'refx' => 'refX',
3629             'refy' => 'refY',
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',
3651         );
3652         foreach ($token['attr'] as &$kp) {
3653             if (isset($lookup[$kp['name']])) {
3654                 $kp['name'] = $lookup[$kp['name']];
3655             }
3656         }
3657         return $token;
3658     }
3659
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),
3674         );
3675         foreach ($token['attr'] as &$kp) {
3676             if (isset($lookup[$kp['name']])) {
3677                 $kp['name'] = $lookup[$kp['name']];
3678             }
3679         }
3680         return $token;
3681     }
3682
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)) {
3689                     $ns = $attr[2];
3690                     $attr = $attr[1];
3691                 } else {
3692                     $ns = self::NS_HTML;
3693                 }
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']);
3701                     } else {
3702                         $el->setAttributeNS($ns, $attr, $kp['value']);
3703                     }
3704                 }
3705             }
3706         }
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. */
3715     }
3716
3717     public function save() {
3718         $this->dom->normalize();
3719         if (!$this->fragment) {
3720             return $this->dom;
3721         } else {
3722             if ($this->root) {
3723                 return $this->root->childNodes;
3724             } else {
3725                 return $this->dom->childNodes;
3726             }
3727         }
3728     }
3729 }
3730