- This can greatly improve readability for editors who are hand-editing
- the HTML, but is by no means necessary as HTML Purifier has already
- fixed all major errors the HTML may have had. Tidy is a non-default
- extension, and this directive will silently fail if Tidy is not
- available.
-
-
- If you are looking to make the overall look of your page's source
- better, I recommend running Tidy on the entire page rather than just
- user-content (after all, the indentation relative to the containing
- blocks will be incorrect).
-
-
- Newline string to format final output with. If left null, HTML Purifier
- will auto-detect the default newline type of the system and use that;
- you can manually override it here. Remember, \r\n is Windows, \r
- is Mac, and \n is Unix. This directive was available since 2.0.1.
-
-');
-
/**
* Generates HTML from tokens.
* @todo Refactor interface so that configuration/context is determined
- * upon instantiation, no need for messy generateFromTokens() calls
+ * upon instantiation, no need for messy generateFromTokens() calls
+ * @todo Make some of the more internal functions protected, and have
+ * unit tests work around that
*/
class HTMLPurifier_Generator
{
-
+
/**
- * Bool cache of %HTML.XHTML
- * @private
+ * Whether or not generator should produce XML output
*/
- var $_xhtml = true;
-
+ private $_xhtml = true;
+
/**
- * Bool cache of %Output.CommentScriptContents
- * @private
+ * :HACK: Whether or not generator should comment the insides of )#si',
- array('HTMLPurifier_Lexer_DirectLex', 'scriptCallback'), $html);
+ array($this, 'scriptCallback'), $html);
}
-
+
$html = $this->normalize($html, $config, $context);
-
+
$cursor = 0; // our location in the text
$inside_tag = false; // whether or not we're parsing the inside of a tag
$array = array(); // result array
-
- $maintain_line_numbers = $config->get('Core', 'MaintainLineNumbers');
-
+
+ // This is also treated to mean maintain *column* numbers too
+ $maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
+
if ($maintain_line_numbers === null) {
// automatically determine line numbering by checking
// if error collection is on
- $maintain_line_numbers = $config->get('Core', 'CollectErrors');
+ $maintain_line_numbers = $config->get('Core.CollectErrors');
+ }
+
+ if ($maintain_line_numbers) {
+ $current_line = 1;
+ $current_col = 0;
+ $length = strlen($html);
+ } else {
+ $current_line = false;
+ $current_col = false;
+ $length = false;
}
-
- if ($maintain_line_numbers) $current_line = 1;
- else $current_line = false;
$context->register('CurrentLine', $current_line);
+ $context->register('CurrentCol', $current_col);
$nl = "\n";
// how often to manually recalculate. This will ALWAYS be right,
// but it's pretty wasteful. Set to 0 to turn off
- $synchronize_interval = $config->get('Core', 'DirectLexLineNumberSyncInterval');
-
+ $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
+
$e = false;
- if ($config->get('Core', 'CollectErrors')) {
+ if ($config->get('Core.CollectErrors')) {
$e =& $context->get('ErrorCollector');
}
-
- // infinite loop protection
- // has to be pretty big, since html docs can be big
- // we're allow two hundred thousand tags... more than enough?
- // NOTE: this is also used for synchronization, so watch out
+
+ // for testing synchronization
$loops = 0;
-
- while(true) {
-
- // infinite loop protection
- if (++$loops > 200000) return array();
-
- // recalculate lines
- if (
- $maintain_line_numbers && // line number tracking is on
- $synchronize_interval && // synchronization is on
- $cursor > 0 && // cursor is further than zero
- $loops % $synchronize_interval === 0 // time to synchronize!
- ) {
- $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
+
+ while(++$loops) {
+
+ // $cursor is either at the start of a token, or inside of
+ // a tag (i.e. there was a < immediately before it), as indicated
+ // by $inside_tag
+
+ if ($maintain_line_numbers) {
+
+ // $rcursor, however, is always at the start of a token.
+ $rcursor = $cursor - (int) $inside_tag;
+
+ // Column number is cheap, so we calculate it every round.
+ // We're interested at the *end* of the newline string, so
+ // we need to add strlen($nl) == 1 to $nl_pos before subtracting it
+ // from our "rcursor" position.
+ $nl_pos = strrpos($html, $nl, $rcursor - $length);
+ $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
+
+ // recalculate lines
+ if (
+ $synchronize_interval && // synchronization is on
+ $cursor > 0 && // cursor is further than zero
+ $loops % $synchronize_interval === 0 // time to synchronize!
+ ) {
+ $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
+ }
+
}
-
+
$position_next_lt = strpos($html, '<', $cursor);
$position_next_gt = strpos($html, '>', $cursor);
-
+
// triggers on "
- , $tokens);
- return $tokens;
- }
-
-}
-
-/*
-
-Copyright 2007 Jeroen van der Meer
-
-Permission is hereby granted, free of charge, to any person obtaining a
-copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be included
-in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
-OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-*/
-
-class HTML5 {
- private $data;
- private $char;
- private $EOF;
- private $state;
- private $tree;
- private $token;
- private $content_model;
- private $escape = false;
- private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute',
- 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;',
- 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;',
- 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;',
- 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;',
- 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;',
- 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;',
- 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;',
- 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;',
- 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN',
- 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;',
- 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;',
- 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig',
- 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;',
- 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;',
- 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil',
- 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;',
- 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;',
- 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;',
- 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth',
- 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12',
- 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt',
- 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc',
- 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;',
- 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;',
- 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;',
- 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro',
- 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;',
- 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;',
- 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;',
- 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash',
- 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;',
- 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;',
- 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;',
- 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;',
- 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;',
- 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;',
- 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;',
- 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;',
- 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc',
- 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;',
- 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;');
-
- const PCDATA = 0;
- const RCDATA = 1;
- const CDATA = 2;
- const PLAINTEXT = 3;
-
- const DOCTYPE = 0;
- const STARTTAG = 1;
- const ENDTAG = 2;
- const COMMENT = 3;
- const CHARACTR = 4;
- const EOF = 5;
-
- public function __construct($data) {
- $data = str_replace("\r\n", "\n", $data);
- $date = str_replace("\r", null, $data);
-
- $this->data = $data;
- $this->char = -1;
- $this->EOF = strlen($data);
- $this->tree = new HTML5TreeConstructer;
- $this->content_model = self::PCDATA;
-
- $this->state = 'data';
-
- while($this->state !== null) {
- $this->{$this->state.'State'}();
- }
- }
-
- public function save() {
- return $this->tree->save();
- }
-
- private function char() {
- return ($this->char < $this->EOF)
- ? $this->data[$this->char]
- : false;
- }
-
- private function character($s, $l = 0) {
- if($s + $l < $this->EOF) {
- if($l === 0) {
- return $this->data[$s];
- } else {
- return substr($this->data, $s, $l);
- }
- }
- }
-
- private function characters($char_class, $start) {
- return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start));
- }
-
- private function dataState() {
- // Consume the next input character
- $this->char++;
- $char = $this->char();
-
- if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) {
- /* U+0026 AMPERSAND (&)
- When the content model flag is set to one of the PCDATA or RCDATA
- states: switch to the entity data state. Otherwise: treat it as per
- the "anything else" entry below. */
- $this->state = 'entityData';
-
- } elseif($char === '-') {
- /* If the content model flag is set to either the RCDATA state or
- the CDATA state, and the escape flag is false, and there are at
- least three characters before this one in the input stream, and the
- last four characters in the input stream, including this one, are
- U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
- and U+002D HYPHEN-MINUS (""),
- set the escape flag to false. */
- if(($this->content_model === self::RCDATA ||
- $this->content_model === self::CDATA) && $this->escape === true &&
- $this->character($this->char, 3) === '-->') {
- $this->escape = false;
- }
-
- /* In any case, emit the input character as a character token.
- Stay in the data state. */
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => $char
- ));
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Emit an end-of-file token. */
- $this->EOF();
-
- } elseif($this->content_model === self::PLAINTEXT) {
- /* When the content model flag is set to the PLAINTEXT state
- THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of
- the text and emit it as a character token. */
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => substr($this->data, $this->char)
- ));
-
- $this->EOF();
-
- } else {
- /* Anything else
- THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that
- otherwise would also be treated as a character token and emit it
- as a single character token. Stay in the data state. */
- $len = strcspn($this->data, '<&', $this->char);
- $char = substr($this->data, $this->char, $len);
- $this->char += $len - 1;
-
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => $char
- ));
-
- $this->state = 'data';
- }
- }
-
- private function entityDataState() {
- // Attempt to consume an entity.
- $entity = $this->entity();
-
- // If nothing is returned, emit a U+0026 AMPERSAND character token.
- // Otherwise, emit the character token that was returned.
- $char = (!$entity) ? '&' : $entity;
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => $char
- ));
-
- // Finally, switch to the data state.
- $this->state = 'data';
- }
-
- private function tagOpenState() {
- switch($this->content_model) {
- case self::RCDATA:
- case self::CDATA:
- /* If the next input character is a U+002F SOLIDUS (/) character,
- consume it and switch to the close tag open state. If the next
- input character is not a U+002F SOLIDUS (/) character, emit a
- U+003C LESS-THAN SIGN character token and switch to the data
- state to process the next input character. */
- if($this->character($this->char + 1) === '/') {
- $this->char++;
- $this->state = 'closeTagOpen';
-
- } else {
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => '<'
- ));
-
- $this->state = 'data';
- }
- break;
-
- case self::PCDATA:
- // If the content model flag is set to the PCDATA state
- // Consume the next input character:
- $this->char++;
- $char = $this->char();
-
- if($char === '!') {
- /* U+0021 EXCLAMATION MARK (!)
- Switch to the markup declaration open state. */
- $this->state = 'markupDeclarationOpen';
-
- } elseif($char === '/') {
- /* U+002F SOLIDUS (/)
- Switch to the close tag open state. */
- $this->state = 'closeTagOpen';
-
- } elseif(preg_match('/^[A-Za-z]$/', $char)) {
- /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
- Create a new start tag token, set its tag name to the lowercase
- version of the input character (add 0x0020 to the character's code
- point), then switch to the tag name state. (Don't emit the token
- yet; further details will be filled in before it is emitted.) */
- $this->token = array(
- 'name' => strtolower($char),
- 'type' => self::STARTTAG,
- 'attr' => array()
- );
-
- $this->state = 'tagName';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Parse error. Emit a U+003C LESS-THAN SIGN character token and a
- U+003E GREATER-THAN SIGN character token. Switch to the data state. */
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => '<>'
- ));
-
- $this->state = 'data';
-
- } elseif($char === '?') {
- /* U+003F QUESTION MARK (?)
- Parse error. Switch to the bogus comment state. */
- $this->state = 'bogusComment';
-
- } else {
- /* Anything else
- Parse error. Emit a U+003C LESS-THAN SIGN character token and
- reconsume the current input character in the data state. */
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => '<'
- ));
-
- $this->char--;
- $this->state = 'data';
- }
- break;
- }
- }
-
- private function closeTagOpenState() {
- $next_node = strtolower($this->characters('A-Za-z', $this->char + 1));
- $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName;
-
- if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) &&
- (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/',
- $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) {
- /* If the content model flag is set to the RCDATA or CDATA states then
- examine the next few characters. If they do not match the tag name of
- the last start tag token emitted (case insensitively), or if they do but
- they are not immediately followed by one of the following characters:
- * U+0009 CHARACTER TABULATION
- * U+000A LINE FEED (LF)
- * U+000B LINE TABULATION
- * U+000C FORM FEED (FF)
- * U+0020 SPACE
- * U+003E GREATER-THAN SIGN (>)
- * U+002F SOLIDUS (/)
- * EOF
- ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character
- token, a U+002F SOLIDUS character token, and switch to the data state
- to process the next input character. */
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => ''
- ));
-
- $this->state = 'data';
-
- } else {
- /* Otherwise, if the content model flag is set to the PCDATA state,
- or if the next few characters do match that tag name, consume the
- next input character: */
- $this->char++;
- $char = $this->char();
-
- if(preg_match('/^[A-Za-z]$/', $char)) {
- /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
- Create a new end tag token, set its tag name to the lowercase version
- of the input character (add 0x0020 to the character's code point), then
- switch to the tag name state. (Don't emit the token yet; further details
- will be filled in before it is emitted.) */
- $this->token = array(
- 'name' => strtolower($char),
- 'type' => self::ENDTAG
- );
-
- $this->state = 'tagName';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Parse error. Switch to the data state. */
- $this->state = 'data';
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
- SOLIDUS character token. Reconsume the EOF character in the data state. */
- $this->emitToken(array(
- 'type' => self::CHARACTR,
- 'data' => ''
- ));
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- /* Parse error. Switch to the bogus comment state. */
- $this->state = 'bogusComment';
- }
- }
- }
-
- private function tagNameState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- /* U+0009 CHARACTER TABULATION
- U+000A LINE FEED (LF)
- U+000B LINE TABULATION
- U+000C FORM FEED (FF)
- U+0020 SPACE
- Switch to the before attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Emit the current tag token. Switch to the data state. */
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit the current tag token. Reconsume the EOF
- character in the data state. */
- $this->emitToken($this->token);
-
- $this->char--;
- $this->state = 'data';
-
- } elseif($char === '/') {
- /* U+002F SOLIDUS (/)
- Parse error unless this is a permitted slash. Switch to the before
- attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } else {
- /* Anything else
- Append the current input character to the current tag token's tag name.
- Stay in the tag name state. */
- $this->token['name'] .= strtolower($char);
- $this->state = 'tagName';
- }
- }
-
- private function beforeAttributeNameState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- /* U+0009 CHARACTER TABULATION
- U+000A LINE FEED (LF)
- U+000B LINE TABULATION
- U+000C FORM FEED (FF)
- U+0020 SPACE
- Stay in the before attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Emit the current tag token. Switch to the data state. */
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($char === '/') {
- /* U+002F SOLIDUS (/)
- Parse error unless this is a permitted slash. Stay in the before
- attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit the current tag token. Reconsume the EOF
- character in the data state. */
- $this->emitToken($this->token);
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- /* Anything else
- Start a new attribute in the current tag token. Set that attribute's
- name to the current input character, and its value to the empty string.
- Switch to the attribute name state. */
- $this->token['attr'][] = array(
- 'name' => strtolower($char),
- 'value' => null
- );
-
- $this->state = 'attributeName';
- }
- }
-
- private function attributeNameState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- /* U+0009 CHARACTER TABULATION
- U+000A LINE FEED (LF)
- U+000B LINE TABULATION
- U+000C FORM FEED (FF)
- U+0020 SPACE
- Stay in the before attribute name state. */
- $this->state = 'afterAttributeName';
-
- } elseif($char === '=') {
- /* U+003D EQUALS SIGN (=)
- Switch to the before attribute value state. */
- $this->state = 'beforeAttributeValue';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Emit the current tag token. Switch to the data state. */
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($char === '/' && $this->character($this->char + 1) !== '>') {
- /* U+002F SOLIDUS (/)
- Parse error unless this is a permitted slash. Switch to the before
- attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit the current tag token. Reconsume the EOF
- character in the data state. */
- $this->emitToken($this->token);
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- /* Anything else
- Append the current input character to the current attribute's name.
- Stay in the attribute name state. */
- $last = count($this->token['attr']) - 1;
- $this->token['attr'][$last]['name'] .= strtolower($char);
-
- $this->state = 'attributeName';
- }
- }
-
- private function afterAttributeNameState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- /* U+0009 CHARACTER TABULATION
- U+000A LINE FEED (LF)
- U+000B LINE TABULATION
- U+000C FORM FEED (FF)
- U+0020 SPACE
- Stay in the after attribute name state. */
- $this->state = 'afterAttributeName';
-
- } elseif($char === '=') {
- /* U+003D EQUALS SIGN (=)
- Switch to the before attribute value state. */
- $this->state = 'beforeAttributeValue';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Emit the current tag token. Switch to the data state. */
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($char === '/' && $this->character($this->char + 1) !== '>') {
- /* U+002F SOLIDUS (/)
- Parse error unless this is a permitted slash. Switch to the
- before attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit the current tag token. Reconsume the EOF
- character in the data state. */
- $this->emitToken($this->token);
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- /* Anything else
- Start a new attribute in the current tag token. Set that attribute's
- name to the current input character, and its value to the empty string.
- Switch to the attribute name state. */
- $this->token['attr'][] = array(
- 'name' => strtolower($char),
- 'value' => null
- );
-
- $this->state = 'attributeName';
- }
- }
-
- private function beforeAttributeValueState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- /* U+0009 CHARACTER TABULATION
- U+000A LINE FEED (LF)
- U+000B LINE TABULATION
- U+000C FORM FEED (FF)
- U+0020 SPACE
- Stay in the before attribute value state. */
- $this->state = 'beforeAttributeValue';
-
- } elseif($char === '"') {
- /* U+0022 QUOTATION MARK (")
- Switch to the attribute value (double-quoted) state. */
- $this->state = 'attributeValueDoubleQuoted';
-
- } elseif($char === '&') {
- /* U+0026 AMPERSAND (&)
- Switch to the attribute value (unquoted) state and reconsume
- this input character. */
- $this->char--;
- $this->state = 'attributeValueUnquoted';
-
- } elseif($char === '\'') {
- /* U+0027 APOSTROPHE (')
- Switch to the attribute value (single-quoted) state. */
- $this->state = 'attributeValueSingleQuoted';
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Emit the current tag token. Switch to the data state. */
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } else {
- /* Anything else
- Append the current input character to the current attribute's value.
- Switch to the attribute value (unquoted) state. */
- $last = count($this->token['attr']) - 1;
- $this->token['attr'][$last]['value'] .= $char;
-
- $this->state = 'attributeValueUnquoted';
- }
- }
-
- private function attributeValueDoubleQuotedState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if($char === '"') {
- /* U+0022 QUOTATION MARK (")
- Switch to the before attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($char === '&') {
- /* U+0026 AMPERSAND (&)
- Switch to the entity in attribute value state. */
- $this->entityInAttributeValueState('double');
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit the current tag token. Reconsume the character
- in the data state. */
- $this->emitToken($this->token);
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- /* Anything else
- Append the current input character to the current attribute's value.
- Stay in the attribute value (double-quoted) state. */
- $last = count($this->token['attr']) - 1;
- $this->token['attr'][$last]['value'] .= $char;
-
- $this->state = 'attributeValueDoubleQuoted';
- }
- }
-
- private function attributeValueSingleQuotedState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if($char === '\'') {
- /* U+0022 QUOTATION MARK (')
- Switch to the before attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($char === '&') {
- /* U+0026 AMPERSAND (&)
- Switch to the entity in attribute value state. */
- $this->entityInAttributeValueState('single');
-
- } elseif($this->char === $this->EOF) {
- /* EOF
- Parse error. Emit the current tag token. Reconsume the character
- in the data state. */
- $this->emitToken($this->token);
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- /* Anything else
- Append the current input character to the current attribute's value.
- Stay in the attribute value (single-quoted) state. */
- $last = count($this->token['attr']) - 1;
- $this->token['attr'][$last]['value'] .= $char;
-
- $this->state = 'attributeValueSingleQuoted';
- }
- }
-
- private function attributeValueUnquotedState() {
- // Consume the next input character:
- $this->char++;
- $char = $this->character($this->char);
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- /* U+0009 CHARACTER TABULATION
- U+000A LINE FEED (LF)
- U+000B LINE TABULATION
- U+000C FORM FEED (FF)
- U+0020 SPACE
- Switch to the before attribute name state. */
- $this->state = 'beforeAttributeName';
-
- } elseif($char === '&') {
- /* U+0026 AMPERSAND (&)
- Switch to the entity in attribute value state. */
- $this->entityInAttributeValueState();
-
- } elseif($char === '>') {
- /* U+003E GREATER-THAN SIGN (>)
- Emit the current tag token. Switch to the data state. */
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } else {
- /* Anything else
- Append the current input character to the current attribute's value.
- Stay in the attribute value (unquoted) state. */
- $last = count($this->token['attr']) - 1;
- $this->token['attr'][$last]['value'] .= $char;
-
- $this->state = 'attributeValueUnquoted';
- }
- }
-
- private function entityInAttributeValueState() {
- // Attempt to consume an entity.
- $entity = $this->entity();
-
- // If nothing is returned, append a U+0026 AMPERSAND character to the
- // current attribute's value. Otherwise, emit the character token that
- // was returned.
- $char = (!$entity)
- ? '&'
- : $entity;
-
- $last = count($this->token['attr']) - 1;
- $this->token['attr'][$last]['value'] .= $char;
- }
-
- private function bogusCommentState() {
- /* Consume every character up to the first U+003E GREATER-THAN SIGN
- character (>) or the end of the file (EOF), whichever comes first. Emit
- a comment token whose data is the concatenation of all the characters
- starting from and including the character that caused the state machine
- to switch into the bogus comment state, up to and including the last
- consumed character before the U+003E character, if any, or up to the
- end of the file otherwise. (If the comment was started by the end of
- the file (EOF), the token is empty.) */
- $data = $this->characters('^>', $this->char);
- $this->emitToken(array(
- 'data' => $data,
- 'type' => self::COMMENT
- ));
-
- $this->char += strlen($data);
-
- /* Switch to the data state. */
- $this->state = 'data';
-
- /* If the end of the file was reached, reconsume the EOF character. */
- if($this->char === $this->EOF) {
- $this->char = $this->EOF - 1;
- }
- }
-
- private function markupDeclarationOpenState() {
- /* If the next two characters are both U+002D HYPHEN-MINUS (-)
- characters, consume those two characters, create a comment token whose
- data is the empty string, and switch to the comment state. */
- if($this->character($this->char + 1, 2) === '--') {
- $this->char += 2;
- $this->state = 'comment';
- $this->token = array(
- 'data' => null,
- 'type' => self::COMMENT
- );
-
- /* Otherwise if the next seven chacacters are a case-insensitive match
- for the word "DOCTYPE", then consume those characters and switch to the
- DOCTYPE state. */
- } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') {
- $this->char += 7;
- $this->state = 'doctype';
-
- /* Otherwise, is is a parse error. Switch to the bogus comment state.
- The next character that is consumed, if any, is the first character
- that will be in the comment. */
- } else {
- $this->char++;
- $this->state = 'bogusComment';
- }
- }
-
- private function commentState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- /* U+002D HYPHEN-MINUS (-) */
- if($char === '-') {
- /* Switch to the comment dash state */
- $this->state = 'commentDash';
-
- /* EOF */
- } elseif($this->char === $this->EOF) {
- /* Parse error. Emit the comment token. Reconsume the EOF character
- in the data state. */
- $this->emitToken($this->token);
- $this->char--;
- $this->state = 'data';
-
- /* Anything else */
- } else {
- /* Append the input character to the comment token's data. Stay in
- the comment state. */
- $this->token['data'] .= $char;
- }
- }
-
- private function commentDashState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- /* U+002D HYPHEN-MINUS (-) */
- if($char === '-') {
- /* Switch to the comment end state */
- $this->state = 'commentEnd';
-
- /* EOF */
- } elseif($this->char === $this->EOF) {
- /* Parse error. Emit the comment token. Reconsume the EOF character
- in the data state. */
- $this->emitToken($this->token);
- $this->char--;
- $this->state = 'data';
-
- /* Anything else */
- } else {
- /* Append a U+002D HYPHEN-MINUS (-) character and the input
- character to the comment token's data. Switch to the comment state. */
- $this->token['data'] .= '-'.$char;
- $this->state = 'comment';
- }
- }
-
- private function commentEndState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- if($char === '>') {
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($char === '-') {
- $this->token['data'] .= '-';
-
- } elseif($this->char === $this->EOF) {
- $this->emitToken($this->token);
- $this->char--;
- $this->state = 'data';
-
- } else {
- $this->token['data'] .= '--'.$char;
- $this->state = 'comment';
- }
- }
-
- private function doctypeState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- $this->state = 'beforeDoctypeName';
-
- } else {
- $this->char--;
- $this->state = 'beforeDoctypeName';
- }
- }
-
- private function beforeDoctypeNameState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- // Stay in the before DOCTYPE name state.
-
- } elseif(preg_match('/^[a-z]$/', $char)) {
- $this->token = array(
- 'name' => strtoupper($char),
- 'type' => self::DOCTYPE,
- 'error' => true
- );
-
- $this->state = 'doctypeName';
-
- } elseif($char === '>') {
- $this->emitToken(array(
- 'name' => null,
- 'type' => self::DOCTYPE,
- 'error' => true
- ));
-
- $this->state = 'data';
-
- } elseif($this->char === $this->EOF) {
- $this->emitToken(array(
- 'name' => null,
- 'type' => self::DOCTYPE,
- 'error' => true
- ));
-
- $this->char--;
- $this->state = 'data';
-
- } else {
- $this->token = array(
- 'name' => $char,
- 'type' => self::DOCTYPE,
- 'error' => true
- );
-
- $this->state = 'doctypeName';
- }
- }
-
- private function doctypeNameState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- $this->state = 'AfterDoctypeName';
-
- } elseif($char === '>') {
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif(preg_match('/^[a-z]$/', $char)) {
- $this->token['name'] .= strtoupper($char);
-
- } elseif($this->char === $this->EOF) {
- $this->emitToken($this->token);
- $this->char--;
- $this->state = 'data';
-
- } else {
- $this->token['name'] .= $char;
- }
-
- $this->token['error'] = ($this->token['name'] === 'HTML')
- ? false
- : true;
- }
-
- private function afterDoctypeNameState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
- // Stay in the DOCTYPE name state.
-
- } elseif($char === '>') {
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($this->char === $this->EOF) {
- $this->emitToken($this->token);
- $this->char--;
- $this->state = 'data';
-
- } else {
- $this->token['error'] = true;
- $this->state = 'bogusDoctype';
- }
- }
-
- private function bogusDoctypeState() {
- /* Consume the next input character: */
- $this->char++;
- $char = $this->char();
-
- if($char === '>') {
- $this->emitToken($this->token);
- $this->state = 'data';
-
- } elseif($this->char === $this->EOF) {
- $this->emitToken($this->token);
- $this->char--;
- $this->state = 'data';
-
- } else {
- // Stay in the bogus DOCTYPE state.
- }
- }
-
- private function entity() {
- $start = $this->char;
-
- // This section defines how to consume an entity. This definition is
- // used when parsing entities in text and in attributes.
-
- // The behaviour depends on the identity of the next character (the
- // one immediately after the U+0026 AMPERSAND character):
-
- switch($this->character($this->char + 1)) {
- // U+0023 NUMBER SIGN (#)
- case '#':
-
- // The behaviour further depends on the character after the
- // U+0023 NUMBER SIGN:
- switch($this->character($this->char + 1)) {
- // U+0078 LATIN SMALL LETTER X
- // U+0058 LATIN CAPITAL LETTER X
- case 'x':
- case 'X':
- // Follow the steps below, but using the range of
- // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
- // NINE, U+0061 LATIN SMALL LETTER A through to U+0066
- // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER
- // A, through to U+0046 LATIN CAPITAL LETTER F (in other
- // words, 0-9, A-F, a-f).
- $char = 1;
- $char_class = '0-9A-Fa-f';
- break;
-
- // Anything else
- default:
- // Follow the steps below, but using the range of
- // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
- // NINE (i.e. just 0-9).
- $char = 0;
- $char_class = '0-9';
- break;
- }
-
- // Consume as many characters as match the range of characters
- // given above.
- $this->char++;
- $e_name = $this->characters($char_class, $this->char + $char + 1);
- $entity = $this->character($start, $this->char);
- $cond = strlen($e_name) > 0;
-
- // The rest of the parsing happens bellow.
- break;
-
- // Anything else
- default:
- // Consume the maximum number of characters possible, with the
- // consumed characters case-sensitively matching one of the
- // identifiers in the first column of the entities table.
- $e_name = $this->characters('0-9A-Za-z;', $this->char + 1);
- $len = strlen($e_name);
-
- for($c = 1; $c <= $len; $c++) {
- $id = substr($e_name, 0, $c);
- $this->char++;
-
- if(in_array($id, $this->entities)) {
- if ($e_name[$c-1] !== ';') {
- if ($c < $len && $e_name[$c] == ';') {
- $this->char++; // consume extra semicolon
- }
- }
- $entity = $id;
- break;
- }
- }
-
- $cond = isset($entity);
- // The rest of the parsing happens bellow.
- break;
- }
-
- if(!$cond) {
- // If no match can be made, then this is a parse error. No
- // characters are consumed, and nothing is returned.
- $this->char = $start;
- return false;
- }
-
- // Return a character token for the character corresponding to the
- // entity name (as given by the second column of the entities table).
- return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8');
- }
-
- private function emitToken($token) {
- $emit = $this->tree->emitToken($token);
-
- if(is_int($emit)) {
- $this->content_model = $emit;
-
- } elseif($token['type'] === self::ENDTAG) {
- $this->content_model = self::PCDATA;
- }
- }
-
- private function EOF() {
- $this->state = null;
- $this->tree->emitToken(array(
- 'type' => self::EOF
- ));
- }
-}
-
-class HTML5TreeConstructer {
- public $stack = array();
-
- private $phase;
- private $mode;
- private $dom;
- private $foster_parent = null;
- private $a_formatting = array();
-
- private $head_pointer = null;
- private $form_pointer = null;
-
- private $scoping = array('button','caption','html','marquee','object','table','td','th');
- private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u');
- private $special = array('address','area','base','basefont','bgsound',
- 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl',
- 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5',
- 'h6','head','hr','iframe','image','img','input','isindex','li','link',
- 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup',
- 'option','p','param','plaintext','pre','script','select','spacer','style',
- 'tbody','textarea','tfoot','thead','title','tr','ul','wbr');
-
- // The different phases.
- const INIT_PHASE = 0;
- const ROOT_PHASE = 1;
- const MAIN_PHASE = 2;
- const END_PHASE = 3;
-
- // The different insertion modes for the main phase.
- const BEFOR_HEAD = 0;
- const IN_HEAD = 1;
- const AFTER_HEAD = 2;
- const IN_BODY = 3;
- const IN_TABLE = 4;
- const IN_CAPTION = 5;
- const IN_CGROUP = 6;
- const IN_TBODY = 7;
- const IN_ROW = 8;
- const IN_CELL = 9;
- const IN_SELECT = 10;
- const AFTER_BODY = 11;
- const IN_FRAME = 12;
- const AFTR_FRAME = 13;
-
- // The different types of elements.
- const SPECIAL = 0;
- const SCOPING = 1;
- const FORMATTING = 2;
- const PHRASING = 3;
-
- const MARKER = 0;
-
- public function __construct() {
- $this->phase = self::INIT_PHASE;
- $this->mode = self::BEFOR_HEAD;
- $this->dom = new DOMDocument;
-
- $this->dom->encoding = 'UTF-8';
- $this->dom->preserveWhiteSpace = true;
- $this->dom->substituteEntities = true;
- $this->dom->strictErrorChecking = false;
- }
-
- // Process tag tokens
- public function emitToken($token) {
- switch($this->phase) {
- case self::INIT_PHASE: return $this->initPhase($token); break;
- case self::ROOT_PHASE: return $this->rootElementPhase($token); break;
- case self::MAIN_PHASE: return $this->mainPhase($token); break;
- case self::END_PHASE : return $this->trailingEndPhase($token); break;
- }
- }
-
- private function initPhase($token) {
- /* Initially, the tree construction stage must handle each token
- emitted from the tokenisation stage as follows: */
-
- /* A DOCTYPE token that is marked as being in error
- A comment token
- A start tag token
- An end tag token
- A character token that is not one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE
- An end-of-file token */
- if((isset($token['error']) && $token['error']) ||
- $token['type'] === HTML5::COMMENT ||
- $token['type'] === HTML5::STARTTAG ||
- $token['type'] === HTML5::ENDTAG ||
- $token['type'] === HTML5::EOF ||
- ($token['type'] === HTML5::CHARACTR && isset($token['data']) &&
- !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) {
- /* This specification does not define how to handle this case. In
- particular, user agents may ignore the entirety of this specification
- altogether for such documents, and instead invoke special parse modes
- with a greater emphasis on backwards compatibility. */
-
- $this->phase = self::ROOT_PHASE;
- return $this->rootElementPhase($token);
-
- /* A DOCTYPE token marked as being correct */
- } elseif(isset($token['error']) && !$token['error']) {
- /* Append a DocumentType node to the Document node, with the name
- attribute set to the name given in the DOCTYPE token (which will be
- "HTML"), and the other attributes specific to DocumentType objects
- set to null, empty lists, or the empty string as appropriate. */
- $doctype = new DOMDocumentType(null, null, 'HTML');
-
- /* Then, switch to the root element phase of the tree construction
- stage. */
- $this->phase = self::ROOT_PHASE;
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/',
- $token['data'])) {
- /* Append that character to the Document node. */
- $text = $this->dom->createTextNode($token['data']);
- $this->dom->appendChild($text);
- }
- }
-
- private function rootElementPhase($token) {
- /* After the initial phase, as each token is emitted from the tokenisation
- stage, it must be processed as described in this section. */
-
- /* A DOCTYPE token */
- if($token['type'] === HTML5::DOCTYPE) {
- // Parse error. Ignore the token.
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the Document object with the data
- attribute set to the data given in the comment token. */
- $comment = $this->dom->createComment($token['data']);
- $this->dom->appendChild($comment);
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- } elseif($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append that character to the Document node. */
- $text = $this->dom->createTextNode($token['data']);
- $this->dom->appendChild($text);
-
- /* A character token that is not one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED
- (FF), or U+0020 SPACE
- A start tag token
- An end tag token
- An end-of-file token */
- } elseif(($token['type'] === HTML5::CHARACTR &&
- !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
- $token['type'] === HTML5::STARTTAG ||
- $token['type'] === HTML5::ENDTAG ||
- $token['type'] === HTML5::EOF) {
- /* Create an HTMLElement node with the tag name html, in the HTML
- namespace. Append it to the Document object. Switch to the main
- phase and reprocess the current token. */
- $html = $this->dom->createElement('html');
- $this->dom->appendChild($html);
- $this->stack[] = $html;
-
- $this->phase = self::MAIN_PHASE;
- return $this->mainPhase($token);
- }
- }
-
- private function mainPhase($token) {
- /* Tokens in the main phase must be handled as follows: */
-
- /* A DOCTYPE token */
- if($token['type'] === HTML5::DOCTYPE) {
- // Parse error. Ignore the token.
-
- /* A start tag token with the tag name "html" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') {
- /* If this start tag token was not the first start tag token, then
- it is a parse error. */
-
- /* For each attribute on the token, check to see if the attribute
- is already present on the top element of the stack of open elements.
- If it is not, add the attribute and its corresponding value to that
- element. */
- foreach($token['attr'] as $attr) {
- if(!$this->stack[0]->hasAttribute($attr['name'])) {
- $this->stack[0]->setAttribute($attr['name'], $attr['value']);
- }
- }
-
- /* An end-of-file token */
- } elseif($token['type'] === HTML5::EOF) {
- /* Generate implied end tags. */
- $this->generateImpliedEndTags();
-
- /* Anything else. */
- } else {
- /* Depends on the insertion mode: */
- switch($this->mode) {
- case self::BEFOR_HEAD: return $this->beforeHead($token); break;
- case self::IN_HEAD: return $this->inHead($token); break;
- case self::AFTER_HEAD: return $this->afterHead($token); break;
- case self::IN_BODY: return $this->inBody($token); break;
- case self::IN_TABLE: return $this->inTable($token); break;
- case self::IN_CAPTION: return $this->inCaption($token); break;
- case self::IN_CGROUP: return $this->inColumnGroup($token); break;
- case self::IN_TBODY: return $this->inTableBody($token); break;
- case self::IN_ROW: return $this->inRow($token); break;
- case self::IN_CELL: return $this->inCell($token); break;
- case self::IN_SELECT: return $this->inSelect($token); break;
- case self::AFTER_BODY: return $this->afterBody($token); break;
- case self::IN_FRAME: return $this->inFrameset($token); break;
- case self::AFTR_FRAME: return $this->afterFrameset($token); break;
- case self::END_PHASE: return $this->trailingEndPhase($token); break;
- }
- }
- }
-
- private function beforeHead($token) {
- /* Handle the token as follows: */
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append the character to the current node. */
- $this->insertText($token['data']);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data attribute
- set to the data given in the comment token. */
- $this->insertComment($token['data']);
-
- /* A start tag token with the tag name "head" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') {
- /* Create an element for the token, append the new element to the
- current node and push it onto the stack of open elements. */
- $element = $this->insertElement($token);
-
- /* Set the head element pointer to this new element node. */
- $this->head_pointer = $element;
-
- /* Change the insertion mode to "in head". */
- $this->mode = self::IN_HEAD;
-
- /* A start tag token whose tag name is one of: "base", "link", "meta",
- "script", "style", "title". Or an end tag with the tag name "html".
- Or a character token that is not one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE. Or any other start tag token */
- } elseif($token['type'] === HTML5::STARTTAG ||
- ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') ||
- ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/',
- $token['data']))) {
- /* Act as if a start tag token with the tag name "head" and no
- attributes had been seen, then reprocess the current token. */
- $this->beforeHead(array(
- 'name' => 'head',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- return $this->inHead($token);
-
- /* Any other end tag */
- } elseif($token['type'] === HTML5::ENDTAG) {
- /* Parse error. Ignore the token. */
- }
- }
-
- private function inHead($token) {
- /* Handle the token as follows: */
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE.
-
- THIS DIFFERS FROM THE SPEC: If the current node is either a title, style
- or script element, append the character to the current node regardless
- of its content. */
- if(($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || (
- $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName,
- array('title', 'style', 'script')))) {
- /* Append the character to the current node. */
- $this->insertText($token['data']);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data attribute
- set to the data given in the comment token. */
- $this->insertComment($token['data']);
-
- } elseif($token['type'] === HTML5::ENDTAG &&
- in_array($token['name'], array('title', 'style', 'script'))) {
- array_pop($this->stack);
- return HTML5::PCDATA;
-
- /* A start tag with the tag name "title" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') {
- /* Create an element for the token and append the new element to the
- node pointed to by the head element pointer, or, if that is null
- (innerHTML case), to the current node. */
- if($this->head_pointer !== null) {
- $element = $this->insertElement($token, false);
- $this->head_pointer->appendChild($element);
-
- } else {
- $element = $this->insertElement($token);
- }
-
- /* Switch the tokeniser's content model flag to the RCDATA state. */
- return HTML5::RCDATA;
-
- /* A start tag with the tag name "style" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') {
- /* Create an element for the token and append the new element to the
- node pointed to by the head element pointer, or, if that is null
- (innerHTML case), to the current node. */
- if($this->head_pointer !== null) {
- $element = $this->insertElement($token, false);
- $this->head_pointer->appendChild($element);
-
- } else {
- $this->insertElement($token);
- }
-
- /* Switch the tokeniser's content model flag to the CDATA state. */
- return HTML5::CDATA;
-
- /* A start tag with the tag name "script" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') {
- /* Create an element for the token. */
- $element = $this->insertElement($token, false);
- $this->head_pointer->appendChild($element);
-
- /* Switch the tokeniser's content model flag to the CDATA state. */
- return HTML5::CDATA;
-
- /* A start tag with the tag name "base", "link", or "meta" */
- } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('base', 'link', 'meta'))) {
- /* Create an element for the token and append the new element to the
- node pointed to by the head element pointer, or, if that is null
- (innerHTML case), to the current node. */
- if($this->head_pointer !== null) {
- $element = $this->insertElement($token, false);
- $this->head_pointer->appendChild($element);
- array_pop($this->stack);
-
- } else {
- $this->insertElement($token);
- }
-
- /* An end tag with the tag name "head" */
- } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') {
- /* If the current node is a head element, pop the current node off
- the stack of open elements. */
- if($this->head_pointer->isSameNode(end($this->stack))) {
- array_pop($this->stack);
-
- /* Otherwise, this is a parse error. */
- } else {
- // k
- }
-
- /* Change the insertion mode to "after head". */
- $this->mode = self::AFTER_HEAD;
-
- /* A start tag with the tag name "head" or an end tag except "html". */
- } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') ||
- ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) {
- // Parse error. Ignore the token.
-
- /* Anything else */
- } else {
- /* If the current node is a head element, act as if an end tag
- token with the tag name "head" had been seen. */
- if($this->head_pointer->isSameNode(end($this->stack))) {
- $this->inHead(array(
- 'name' => 'head',
- 'type' => HTML5::ENDTAG
- ));
-
- /* Otherwise, change the insertion mode to "after head". */
- } else {
- $this->mode = self::AFTER_HEAD;
- }
-
- /* Then, reprocess the current token. */
- return $this->afterHead($token);
- }
- }
-
- private function afterHead($token) {
- /* Handle the token as follows: */
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append the character to the current node. */
- $this->insertText($token['data']);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data attribute
- set to the data given in the comment token. */
- $this->insertComment($token['data']);
-
- /* A start tag token with the tag name "body" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') {
- /* Insert a body element for the token. */
- $this->insertElement($token);
-
- /* Change the insertion mode to "in body". */
- $this->mode = self::IN_BODY;
-
- /* A start tag token with the tag name "frameset" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') {
- /* Insert a frameset element for the token. */
- $this->insertElement($token);
-
- /* Change the insertion mode to "in frameset". */
- $this->mode = self::IN_FRAME;
-
- /* A start tag token whose tag name is one of: "base", "link", "meta",
- "script", "style", "title" */
- } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('base', 'link', 'meta', 'script', 'style', 'title'))) {
- /* Parse error. Switch the insertion mode back to "in head" and
- reprocess the token. */
- $this->mode = self::IN_HEAD;
- return $this->inHead($token);
-
- /* Anything else */
- } else {
- /* Act as if a start tag token with the tag name "body" and no
- attributes had been seen, and then reprocess the current token. */
- $this->afterHead(array(
- 'name' => 'body',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- return $this->inBody($token);
- }
- }
-
- private function inBody($token) {
- /* Handle the token as follows: */
-
- switch($token['type']) {
- /* A character token */
- case HTML5::CHARACTR:
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Append the token's character to the current node. */
- $this->insertText($token['data']);
- break;
-
- /* A comment token */
- case HTML5::COMMENT:
- /* Append a Comment node to the current node with the data
- attribute set to the data given in the comment token. */
- $this->insertComment($token['data']);
- break;
-
- case HTML5::STARTTAG:
- switch($token['name']) {
- /* A start tag token whose tag name is one of: "script",
- "style" */
- case 'script': case 'style':
- /* Process the token as if the insertion mode had been "in
- head". */
- return $this->inHead($token);
- break;
-
- /* A start tag token whose tag name is one of: "base", "link",
- "meta", "title" */
- case 'base': case 'link': case 'meta': case 'title':
- /* Parse error. Process the token as if the insertion mode
- had been "in head". */
- return $this->inHead($token);
- break;
-
- /* A start tag token with the tag name "body" */
- case 'body':
- /* Parse error. If the second element on the stack of open
- elements is not a body element, or, if the stack of open
- elements has only one node on it, then ignore the token.
- (innerHTML case) */
- if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') {
- // Ignore
-
- /* Otherwise, for each attribute on the token, check to see
- if the attribute is already present on the body element (the
- second element) on the stack of open elements. If it is not,
- add the attribute and its corresponding value to that
- element. */
- } else {
- foreach($token['attr'] as $attr) {
- if(!$this->stack[1]->hasAttribute($attr['name'])) {
- $this->stack[1]->setAttribute($attr['name'], $attr['value']);
- }
- }
- }
- break;
-
- /* A start tag whose tag name is one of: "address",
- "blockquote", "center", "dir", "div", "dl", "fieldset",
- "listing", "menu", "ol", "p", "ul" */
- case 'address': case 'blockquote': case 'center': case 'dir':
- case 'div': case 'dl': case 'fieldset': case 'listing':
- case 'menu': case 'ol': case 'p': case 'ul':
- /* If the stack of open elements has a p element in scope,
- then act as if an end tag with the tag name p had been
- seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
- break;
-
- /* A start tag whose tag name is "form" */
- case 'form':
- /* If the form element pointer is not null, ignore the
- token with a parse error. */
- if($this->form_pointer !== null) {
- // Ignore.
-
- /* Otherwise: */
- } else {
- /* If the stack of open elements has a p element in
- scope, then act as if an end tag with the tag name p
- had been seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token, and set the
- form element pointer to point to the element created. */
- $element = $this->insertElement($token);
- $this->form_pointer = $element;
- }
- break;
-
- /* A start tag whose tag name is "li", "dd" or "dt" */
- case 'li': case 'dd': case 'dt':
- /* If the stack of open elements has a p element in scope,
- then act as if an end tag with the tag name p had been
- seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- $stack_length = count($this->stack) - 1;
-
- for($n = $stack_length; 0 <= $n; $n--) {
- /* 1. Initialise node to be the current node (the
- bottommost node of the stack). */
- $stop = false;
- $node = $this->stack[$n];
- $cat = $this->getElementCategory($node->tagName);
-
- /* 2. If node is an li, dd or dt element, then pop all
- the nodes from the current node up to node, including
- node, then stop this algorithm. */
- if($token['name'] === $node->tagName || ($token['name'] !== 'li'
- && ($node->tagName === 'dd' || $node->tagName === 'dt'))) {
- for($x = $stack_length; $x >= $n ; $x--) {
- array_pop($this->stack);
- }
-
- break;
- }
-
- /* 3. If node is not in the formatting category, and is
- not in the phrasing category, and is not an address or
- div element, then stop this algorithm. */
- if($cat !== self::FORMATTING && $cat !== self::PHRASING &&
- $node->tagName !== 'address' && $node->tagName !== 'div') {
- break;
- }
- }
-
- /* Finally, insert an HTML element with the same tag
- name as the token's. */
- $this->insertElement($token);
- break;
-
- /* A start tag token whose tag name is "plaintext" */
- case 'plaintext':
- /* If the stack of open elements has a p element in scope,
- then act as if an end tag with the tag name p had been
- seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- return HTML5::PLAINTEXT;
- break;
-
- /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
- "h5", "h6" */
- case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
- /* If the stack of open elements has a p element in scope,
- then act as if an end tag with the tag name p had been seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* If the stack of open elements has in scope an element whose
- tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
- this is a parse error; pop elements from the stack until an
- element with one of those tag names has been popped from the
- stack. */
- while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) {
- array_pop($this->stack);
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
- break;
-
- /* A start tag whose tag name is "a" */
- case 'a':
- /* If the list of active formatting elements contains
- an element whose tag name is "a" between the end of the
- list and the last marker on the list (or the start of
- the list if there is no marker on the list), then this
- is a parse error; act as if an end tag with the tag name
- "a" had been seen, then remove that element from the list
- of active formatting elements and the stack of open
- elements if the end tag didn't already remove it (it
- might not have if the element is not in table scope). */
- $leng = count($this->a_formatting);
-
- for($n = $leng - 1; $n >= 0; $n--) {
- if($this->a_formatting[$n] === self::MARKER) {
- break;
-
- } elseif($this->a_formatting[$n]->nodeName === 'a') {
- $this->emitToken(array(
- 'name' => 'a',
- 'type' => HTML5::ENDTAG
- ));
- break;
- }
- }
-
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $el = $this->insertElement($token);
-
- /* Add that element to the list of active formatting
- elements. */
- $this->a_formatting[] = $el;
- break;
-
- /* A start tag whose tag name is one of: "b", "big", "em", "font",
- "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
- case 'b': case 'big': case 'em': case 'font': case 'i':
- case 'nobr': case 's': case 'small': case 'strike':
- case 'strong': case 'tt': case 'u':
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $el = $this->insertElement($token);
-
- /* Add that element to the list of active formatting
- elements. */
- $this->a_formatting[] = $el;
- break;
-
- /* A start tag token whose tag name is "button" */
- case 'button':
- /* If the stack of open elements has a button element in scope,
- then this is a parse error; act as if an end tag with the tag
- name "button" had been seen, then reprocess the token. (We don't
- do that. Unnecessary.) */
- if($this->elementInScope('button')) {
- $this->inBody(array(
- 'name' => 'button',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Insert a marker at the end of the list of active
- formatting elements. */
- $this->a_formatting[] = self::MARKER;
- break;
-
- /* A start tag token whose tag name is one of: "marquee", "object" */
- case 'marquee': case 'object':
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Insert a marker at the end of the list of active
- formatting elements. */
- $this->a_formatting[] = self::MARKER;
- break;
-
- /* A start tag token whose tag name is "xmp" */
- case 'xmp':
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Switch the content model flag to the CDATA state. */
- return HTML5::CDATA;
- break;
-
- /* A start tag whose tag name is "table" */
- case 'table':
- /* If the stack of open elements has a p element in scope,
- then act as if an end tag with the tag name p had been seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Change the insertion mode to "in table". */
- $this->mode = self::IN_TABLE;
- break;
-
- /* A start tag whose tag name is one of: "area", "basefont",
- "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
- case 'area': case 'basefont': case 'bgsound': case 'br':
- case 'embed': case 'img': case 'param': case 'spacer':
- case 'wbr':
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Immediately pop the current node off the stack of open elements. */
- array_pop($this->stack);
- break;
-
- /* A start tag whose tag name is "hr" */
- case 'hr':
- /* If the stack of open elements has a p element in scope,
- then act as if an end tag with the tag name p had been seen. */
- if($this->elementInScope('p')) {
- $this->emitToken(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Immediately pop the current node off the stack of open elements. */
- array_pop($this->stack);
- break;
-
- /* A start tag whose tag name is "image" */
- case 'image':
- /* Parse error. Change the token's tag name to "img" and
- reprocess it. (Don't ask.) */
- $token['name'] = 'img';
- return $this->inBody($token);
- break;
-
- /* A start tag whose tag name is "input" */
- case 'input':
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an input element for the token. */
- $element = $this->insertElement($token, false);
-
- /* If the form element pointer is not null, then associate the
- input element with the form element pointed to by the form
- element pointer. */
- $this->form_pointer !== null
- ? $this->form_pointer->appendChild($element)
- : end($this->stack)->appendChild($element);
-
- /* Pop that input element off the stack of open elements. */
- array_pop($this->stack);
- break;
-
- /* A start tag whose tag name is "isindex" */
- case 'isindex':
- /* Parse error. */
- // w/e
-
- /* If the form element pointer is not null,
- then ignore the token. */
- if($this->form_pointer === null) {
- /* Act as if a start tag token with the tag name "form" had
- been seen. */
- $this->inBody(array(
- 'name' => 'body',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- /* Act as if a start tag token with the tag name "hr" had
- been seen. */
- $this->inBody(array(
- 'name' => 'hr',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- /* Act as if a start tag token with the tag name "p" had
- been seen. */
- $this->inBody(array(
- 'name' => 'p',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- /* Act as if a start tag token with the tag name "label"
- had been seen. */
- $this->inBody(array(
- 'name' => 'label',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- /* Act as if a stream of character tokens had been seen. */
- $this->insertText('This is a searchable index. '.
- 'Insert your search keywords here: ');
-
- /* Act as if a start tag token with the tag name "input"
- had been seen, with all the attributes from the "isindex"
- token, except with the "name" attribute set to the value
- "isindex" (ignoring any explicit "name" attribute). */
- $attr = $token['attr'];
- $attr[] = array('name' => 'name', 'value' => 'isindex');
-
- $this->inBody(array(
- 'name' => 'input',
- 'type' => HTML5::STARTTAG,
- 'attr' => $attr
- ));
-
- /* Act as if a stream of character tokens had been seen
- (see below for what they should say). */
- $this->insertText('This is a searchable index. '.
- 'Insert your search keywords here: ');
-
- /* Act as if an end tag token with the tag name "label"
- had been seen. */
- $this->inBody(array(
- 'name' => 'label',
- 'type' => HTML5::ENDTAG
- ));
-
- /* Act as if an end tag token with the tag name "p" had
- been seen. */
- $this->inBody(array(
- 'name' => 'p',
- 'type' => HTML5::ENDTAG
- ));
-
- /* Act as if a start tag token with the tag name "hr" had
- been seen. */
- $this->inBody(array(
- 'name' => 'hr',
- 'type' => HTML5::ENDTAG
- ));
-
- /* Act as if an end tag token with the tag name "form" had
- been seen. */
- $this->inBody(array(
- 'name' => 'form',
- 'type' => HTML5::ENDTAG
- ));
- }
- break;
-
- /* A start tag whose tag name is "textarea" */
- case 'textarea':
- $this->insertElement($token);
-
- /* Switch the tokeniser's content model flag to the
- RCDATA state. */
- return HTML5::RCDATA;
- break;
-
- /* A start tag whose tag name is one of: "iframe", "noembed",
- "noframes" */
- case 'iframe': case 'noembed': case 'noframes':
- $this->insertElement($token);
-
- /* Switch the tokeniser's content model flag to the CDATA state. */
- return HTML5::CDATA;
- break;
-
- /* A start tag whose tag name is "select" */
- case 'select':
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Change the insertion mode to "in select". */
- $this->mode = self::IN_SELECT;
- break;
-
- /* A start or end tag whose tag name is one of: "caption", "col",
- "colgroup", "frame", "frameset", "head", "option", "optgroup",
- "tbody", "td", "tfoot", "th", "thead", "tr". */
- case 'caption': case 'col': case 'colgroup': case 'frame':
- case 'frameset': case 'head': case 'option': case 'optgroup':
- case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead':
- case 'tr':
- // Parse error. Ignore the token.
- break;
-
- /* A start or end tag whose tag name is one of: "event-source",
- "section", "nav", "article", "aside", "header", "footer",
- "datagrid", "command" */
- case 'event-source': case 'section': case 'nav': case 'article':
- case 'aside': case 'header': case 'footer': case 'datagrid':
- case 'command':
- // Work in progress!
- break;
-
- /* A start tag token not covered by the previous entries */
- default:
- /* Reconstruct the active formatting elements, if any. */
- $this->reconstructActiveFormattingElements();
-
- $this->insertElement($token);
- break;
- }
- break;
-
- case HTML5::ENDTAG:
- switch($token['name']) {
- /* An end tag with the tag name "body" */
- case 'body':
- /* If the second element in the stack of open elements is
- not a body element, this is a parse error. Ignore the token.
- (innerHTML case) */
- if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') {
- // Ignore.
-
- /* If the current node is not the body element, then this
- is a parse error. */
- } elseif(end($this->stack)->nodeName !== 'body') {
- // Parse error.
- }
-
- /* Change the insertion mode to "after body". */
- $this->mode = self::AFTER_BODY;
- break;
-
- /* An end tag with the tag name "html" */
- case 'html':
- /* Act as if an end tag with tag name "body" had been seen,
- then, if that token wasn't ignored, reprocess the current
- token. */
- $this->inBody(array(
- 'name' => 'body',
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->afterBody($token);
- break;
-
- /* An end tag whose tag name is one of: "address", "blockquote",
- "center", "dir", "div", "dl", "fieldset", "listing", "menu",
- "ol", "pre", "ul" */
- case 'address': case 'blockquote': case 'center': case 'dir':
- case 'div': case 'dl': case 'fieldset': case 'listing':
- case 'menu': case 'ol': case 'pre': case 'ul':
- /* If the stack of open elements has an element in scope
- with the same tag name as that of the token, then generate
- implied end tags. */
- if($this->elementInScope($token['name'])) {
- $this->generateImpliedEndTags();
-
- /* Now, if the current node is not an element with
- the same tag name as that of the token, then this
- is a parse error. */
- // w/e
-
- /* If the stack of open elements has an element in
- scope with the same tag name as that of the token,
- then pop elements from this stack until an element
- with that tag name has been popped from the stack. */
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- if($this->stack[$n]->nodeName === $token['name']) {
- $n = -1;
- }
-
- array_pop($this->stack);
- }
- }
- break;
-
- /* An end tag whose tag name is "form" */
- case 'form':
- /* If the stack of open elements has an element in scope
- with the same tag name as that of the token, then generate
- implied end tags. */
- if($this->elementInScope($token['name'])) {
- $this->generateImpliedEndTags();
-
- }
-
- if(end($this->stack)->nodeName !== $token['name']) {
- /* Now, if the current node is not an element with the
- same tag name as that of the token, then this is a parse
- error. */
- // w/e
-
- } else {
- /* Otherwise, if the current node is an element with
- the same tag name as that of the token pop that element
- from the stack. */
- array_pop($this->stack);
- }
-
- /* In any case, set the form element pointer to null. */
- $this->form_pointer = null;
- break;
-
- /* An end tag whose tag name is "p" */
- case 'p':
- /* If the stack of open elements has a p element in scope,
- then generate implied end tags, except for p elements. */
- if($this->elementInScope('p')) {
- $this->generateImpliedEndTags(array('p'));
-
- /* If the current node is not a p element, then this is
- a parse error. */
- // k
-
- /* If the stack of open elements has a p element in
- scope, then pop elements from this stack until the stack
- no longer has a p element in scope. */
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- if($this->elementInScope('p')) {
- array_pop($this->stack);
-
- } else {
- break;
- }
- }
- }
- break;
-
- /* An end tag whose tag name is "dd", "dt", or "li" */
- case 'dd': case 'dt': case 'li':
- /* If the stack of open elements has an element in scope
- whose tag name matches the tag name of the token, then
- generate implied end tags, except for elements with the
- same tag name as the token. */
- if($this->elementInScope($token['name'])) {
- $this->generateImpliedEndTags(array($token['name']));
-
- /* If the current node is not an element with the same
- tag name as the token, then this is a parse error. */
- // w/e
-
- /* If the stack of open elements has an element in scope
- whose tag name matches the tag name of the token, then
- pop elements from this stack until an element with that
- tag name has been popped from the stack. */
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- if($this->stack[$n]->nodeName === $token['name']) {
- $n = -1;
- }
-
- array_pop($this->stack);
- }
- }
- break;
-
- /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
- "h5", "h6" */
- case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
- $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
-
- /* If the stack of open elements has in scope an element whose
- tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
- generate implied end tags. */
- if($this->elementInScope($elements)) {
- $this->generateImpliedEndTags();
-
- /* Now, if the current node is not an element with the same
- tag name as that of the token, then this is a parse error. */
- // w/e
-
- /* If the stack of open elements has in scope an element
- whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
- "h6", then pop elements from the stack until an element
- with one of those tag names has been popped from the stack. */
- while($this->elementInScope($elements)) {
- array_pop($this->stack);
- }
- }
- break;
-
- /* An end tag whose tag name is one of: "a", "b", "big", "em",
- "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
- case 'a': case 'b': case 'big': case 'em': case 'font':
- case 'i': case 'nobr': case 's': case 'small': case 'strike':
- case 'strong': case 'tt': case 'u':
- /* 1. Let the formatting element be the last element in
- the list of active formatting elements that:
- * is between the end of the list and the last scope
- marker in the list, if any, or the start of the list
- otherwise, and
- * has the same tag name as the token.
- */
- while(true) {
- for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
- if($this->a_formatting[$a] === self::MARKER) {
- break;
-
- } elseif($this->a_formatting[$a]->tagName === $token['name']) {
- $formatting_element = $this->a_formatting[$a];
- $in_stack = in_array($formatting_element, $this->stack, true);
- $fe_af_pos = $a;
- break;
- }
- }
-
- /* If there is no such node, or, if that node is
- also in the stack of open elements but the element
- is not in scope, then this is a parse error. Abort
- these steps. The token is ignored. */
- if(!isset($formatting_element) || ($in_stack &&
- !$this->elementInScope($token['name']))) {
- break;
-
- /* Otherwise, if there is such a node, but that node
- is not in the stack of open elements, then this is a
- parse error; remove the element from the list, and
- abort these steps. */
- } elseif(isset($formatting_element) && !$in_stack) {
- unset($this->a_formatting[$fe_af_pos]);
- $this->a_formatting = array_merge($this->a_formatting);
- break;
- }
-
- /* 2. Let the furthest block be the topmost node in the
- stack of open elements that is lower in the stack
- than the formatting element, and is not an element in
- the phrasing or formatting categories. There might
- not be one. */
- $fe_s_pos = array_search($formatting_element, $this->stack, true);
- $length = count($this->stack);
-
- for($s = $fe_s_pos + 1; $s < $length; $s++) {
- $category = $this->getElementCategory($this->stack[$s]->nodeName);
-
- if($category !== self::PHRASING && $category !== self::FORMATTING) {
- $furthest_block = $this->stack[$s];
- }
- }
-
- /* 3. If there is no furthest block, then the UA must
- skip the subsequent steps and instead just pop all
- the nodes from the bottom of the stack of open
- elements, from the current node up to the formatting
- element, and remove the formatting element from the
- list of active formatting elements. */
- if(!isset($furthest_block)) {
- for($n = $length - 1; $n >= $fe_s_pos; $n--) {
- array_pop($this->stack);
- }
-
- unset($this->a_formatting[$fe_af_pos]);
- $this->a_formatting = array_merge($this->a_formatting);
- break;
- }
-
- /* 4. Let the common ancestor be the element
- immediately above the formatting element in the stack
- of open elements. */
- $common_ancestor = $this->stack[$fe_s_pos - 1];
-
- /* 5. If the furthest block has a parent node, then
- remove the furthest block from its parent node. */
- if($furthest_block->parentNode !== null) {
- $furthest_block->parentNode->removeChild($furthest_block);
- }
-
- /* 6. Let a bookmark note the position of the
- formatting element in the list of active formatting
- elements relative to the elements on either side
- of it in the list. */
- $bookmark = $fe_af_pos;
-
- /* 7. Let node and last node be the furthest block.
- Follow these steps: */
- $node = $furthest_block;
- $last_node = $furthest_block;
-
- while(true) {
- for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
- /* 7.1 Let node be the element immediately
- prior to node in the stack of open elements. */
- $node = $this->stack[$n];
-
- /* 7.2 If node is not in the list of active
- formatting elements, then remove node from
- the stack of open elements and then go back
- to step 1. */
- if(!in_array($node, $this->a_formatting, true)) {
- unset($this->stack[$n]);
- $this->stack = array_merge($this->stack);
-
- } else {
- break;
- }
- }
-
- /* 7.3 Otherwise, if node is the formatting
- element, then go to the next step in the overall
- algorithm. */
- if($node === $formatting_element) {
- break;
-
- /* 7.4 Otherwise, if last node is the furthest
- block, then move the aforementioned bookmark to
- be immediately after the node in the list of
- active formatting elements. */
- } elseif($last_node === $furthest_block) {
- $bookmark = array_search($node, $this->a_formatting, true) + 1;
- }
-
- /* 7.5 If node has any children, perform a
- shallow clone of node, replace the entry for
- node in the list of active formatting elements
- with an entry for the clone, replace the entry
- for node in the stack of open elements with an
- entry for the clone, and let node be the clone. */
- if($node->hasChildNodes()) {
- $clone = $node->cloneNode();
- $s_pos = array_search($node, $this->stack, true);
- $a_pos = array_search($node, $this->a_formatting, true);
-
- $this->stack[$s_pos] = $clone;
- $this->a_formatting[$a_pos] = $clone;
- $node = $clone;
- }
-
- /* 7.6 Insert last node into node, first removing
- it from its previous parent node if any. */
- if($last_node->parentNode !== null) {
- $last_node->parentNode->removeChild($last_node);
- }
-
- $node->appendChild($last_node);
-
- /* 7.7 Let last node be node. */
- $last_node = $node;
- }
-
- /* 8. Insert whatever last node ended up being in
- the previous step into the common ancestor node,
- first removing it from its previous parent node if
- any. */
- if($last_node->parentNode !== null) {
- $last_node->parentNode->removeChild($last_node);
- }
-
- $common_ancestor->appendChild($last_node);
-
- /* 9. Perform a shallow clone of the formatting
- element. */
- $clone = $formatting_element->cloneNode();
-
- /* 10. Take all of the child nodes of the furthest
- block and append them to the clone created in the
- last step. */
- while($furthest_block->hasChildNodes()) {
- $child = $furthest_block->firstChild;
- $furthest_block->removeChild($child);
- $clone->appendChild($child);
- }
-
- /* 11. Append that clone to the furthest block. */
- $furthest_block->appendChild($clone);
-
- /* 12. Remove the formatting element from the list
- of active formatting elements, and insert the clone
- into the list of active formatting elements at the
- position of the aforementioned bookmark. */
- $fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
- unset($this->a_formatting[$fe_af_pos]);
- $this->a_formatting = array_merge($this->a_formatting);
-
- $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
- $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting));
- $this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
-
- /* 13. Remove the formatting element from the stack
- of open elements, and insert the clone into the stack
- of open elements immediately after (i.e. in a more
- deeply nested position than) the position of the
- furthest block in that stack. */
- $fe_s_pos = array_search($formatting_element, $this->stack, true);
- $fb_s_pos = array_search($furthest_block, $this->stack, true);
- unset($this->stack[$fe_s_pos]);
-
- $s_part1 = array_slice($this->stack, 0, $fb_s_pos);
- $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack));
- $this->stack = array_merge($s_part1, array($clone), $s_part2);
-
- /* 14. Jump back to step 1 in this series of steps. */
- unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
- }
- break;
-
- /* An end tag token whose tag name is one of: "button",
- "marquee", "object" */
- case 'button': case 'marquee': case 'object':
- /* If the stack of open elements has an element in scope whose
- tag name matches the tag name of the token, then generate implied
- tags. */
- if($this->elementInScope($token['name'])) {
- $this->generateImpliedEndTags();
-
- /* Now, if the current node is not an element with the same
- tag name as the token, then this is a parse error. */
- // k
-
- /* Now, if the stack of open elements has an element in scope
- whose tag name matches the tag name of the token, then pop
- elements from the stack until that element has been popped from
- the stack, and clear the list of active formatting elements up
- to the last marker. */
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- if($this->stack[$n]->nodeName === $token['name']) {
- $n = -1;
- }
-
- array_pop($this->stack);
- }
-
- $marker = end(array_keys($this->a_formatting, self::MARKER, true));
-
- for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
- array_pop($this->a_formatting);
- }
- }
- break;
-
- /* Or an end tag whose tag name is one of: "area", "basefont",
- "bgsound", "br", "embed", "hr", "iframe", "image", "img",
- "input", "isindex", "noembed", "noframes", "param", "select",
- "spacer", "table", "textarea", "wbr" */
- case 'area': case 'basefont': case 'bgsound': case 'br':
- case 'embed': case 'hr': case 'iframe': case 'image':
- case 'img': case 'input': case 'isindex': case 'noembed':
- case 'noframes': case 'param': case 'select': case 'spacer':
- case 'table': case 'textarea': case 'wbr':
- // Parse error. Ignore the token.
- break;
-
- /* An end tag token not covered by the previous entries */
- default:
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- /* Initialise node to be the current node (the bottommost
- node of the stack). */
- $node = end($this->stack);
-
- /* If node has the same tag name as the end tag token,
- then: */
- if($token['name'] === $node->nodeName) {
- /* Generate implied end tags. */
- $this->generateImpliedEndTags();
-
- /* If the tag name of the end tag token does not
- match the tag name of the current node, this is a
- parse error. */
- // k
-
- /* Pop all the nodes from the current node up to
- node, including node, then stop this algorithm. */
- for($x = count($this->stack) - $n; $x >= $n; $x--) {
- array_pop($this->stack);
- }
-
- } else {
- $category = $this->getElementCategory($node);
-
- if($category !== self::SPECIAL && $category !== self::SCOPING) {
- /* Otherwise, if node is in neither the formatting
- category nor the phrasing category, then this is a
- parse error. Stop this algorithm. The end tag token
- is ignored. */
- return false;
- }
- }
- }
- break;
- }
- break;
- }
- }
-
- private function inTable($token) {
- $clear = array('html', 'table');
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append the character to the current node. */
- $text = $this->dom->createTextNode($token['data']);
- end($this->stack)->appendChild($text);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data
- attribute set to the data given in the comment token. */
- $comment = $this->dom->createComment($token['data']);
- end($this->stack)->appendChild($comment);
-
- /* A start tag whose tag name is "caption" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- $token['name'] === 'caption') {
- /* Clear the stack back to a table context. */
- $this->clearStackToTableContext($clear);
-
- /* Insert a marker at the end of the list of active
- formatting elements. */
- $this->a_formatting[] = self::MARKER;
-
- /* Insert an HTML element for the token, then switch the
- insertion mode to "in caption". */
- $this->insertElement($token);
- $this->mode = self::IN_CAPTION;
-
- /* A start tag whose tag name is "colgroup" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- $token['name'] === 'colgroup') {
- /* Clear the stack back to a table context. */
- $this->clearStackToTableContext($clear);
-
- /* Insert an HTML element for the token, then switch the
- insertion mode to "in column group". */
- $this->insertElement($token);
- $this->mode = self::IN_CGROUP;
-
- /* A start tag whose tag name is "col" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- $token['name'] === 'col') {
- $this->inTable(array(
- 'name' => 'colgroup',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- $this->inColumnGroup($token);
-
- /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
- } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('tbody', 'tfoot', 'thead'))) {
- /* Clear the stack back to a table context. */
- $this->clearStackToTableContext($clear);
-
- /* Insert an HTML element for the token, then switch the insertion
- mode to "in table body". */
- $this->insertElement($token);
- $this->mode = self::IN_TBODY;
-
- /* A start tag whose tag name is one of: "td", "th", "tr" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- in_array($token['name'], array('td', 'th', 'tr'))) {
- /* Act as if a start tag token with the tag name "tbody" had been
- seen, then reprocess the current token. */
- $this->inTable(array(
- 'name' => 'tbody',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- return $this->inTableBody($token);
-
- /* A start tag whose tag name is "table" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- $token['name'] === 'table') {
- /* Parse error. Act as if an end tag token with the tag name "table"
- had been seen, then, if that token wasn't ignored, reprocess the
- current token. */
- $this->inTable(array(
- 'name' => 'table',
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->mainPhase($token);
-
- /* An end tag whose tag name is "table" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- $token['name'] === 'table') {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as the token, this is a parse error.
- Ignore the token. (innerHTML case) */
- if(!$this->elementInScope($token['name'], true)) {
- return false;
-
- /* Otherwise: */
- } else {
- /* Generate implied end tags. */
- $this->generateImpliedEndTags();
-
- /* Now, if the current node is not a table element, then this
- is a parse error. */
- // w/e
-
- /* Pop elements from this stack until a table element has been
- popped from the stack. */
- while(true) {
- $current = end($this->stack)->nodeName;
- array_pop($this->stack);
-
- if($current === 'table') {
- break;
- }
- }
-
- /* Reset the insertion mode appropriately. */
- $this->resetInsertionMode();
- }
-
- /* An end tag whose tag name is one of: "body", "caption", "col",
- "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
- } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
- array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
- 'tfoot', 'th', 'thead', 'tr'))) {
- // Parse error. Ignore the token.
-
- /* Anything else */
- } else {
- /* Parse error. Process the token as if the insertion mode was "in
- body", with the following exception: */
-
- /* If the current node is a table, tbody, tfoot, thead, or tr
- element, then, whenever a node would be inserted into the current
- node, it must instead be inserted into the foster parent element. */
- if(in_array(end($this->stack)->nodeName,
- array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
- /* The foster parent element is the parent element of the last
- table element in the stack of open elements, if there is a
- table element and it has such a parent element. If there is no
- table element in the stack of open elements (innerHTML case),
- then the foster parent element is the first element in the
- stack of open elements (the html element). Otherwise, if there
- is a table element in the stack of open elements, but the last
- table element in the stack of open elements has no parent, or
- its parent node is not an element, then the foster parent
- element is the element before the last table element in the
- stack of open elements. */
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- if($this->stack[$n]->nodeName === 'table') {
- $table = $this->stack[$n];
- break;
- }
- }
-
- if(isset($table) && $table->parentNode !== null) {
- $this->foster_parent = $table->parentNode;
-
- } elseif(!isset($table)) {
- $this->foster_parent = $this->stack[0];
-
- } elseif(isset($table) && ($table->parentNode === null ||
- $table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
- $this->foster_parent = $this->stack[$n - 1];
- }
- }
-
- $this->inBody($token);
- }
- }
-
- private function inCaption($token) {
- /* An end tag whose tag name is "caption" */
- if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as the token, this is a parse error.
- Ignore the token. (innerHTML case) */
- if(!$this->elementInScope($token['name'], true)) {
- // Ignore
-
- /* Otherwise: */
- } else {
- /* Generate implied end tags. */
- $this->generateImpliedEndTags();
-
- /* Now, if the current node is not a caption element, then this
- is a parse error. */
- // w/e
-
- /* Pop elements from this stack until a caption element has
- been popped from the stack. */
- while(true) {
- $node = end($this->stack)->nodeName;
- array_pop($this->stack);
-
- if($node === 'caption') {
- break;
- }
- }
-
- /* Clear the list of active formatting elements up to the last
- marker. */
- $this->clearTheActiveFormattingElementsUpToTheLastMarker();
-
- /* Switch the insertion mode to "in table". */
- $this->mode = self::IN_TABLE;
- }
-
- /* A start tag whose tag name is one of: "caption", "col", "colgroup",
- "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
- name is "table" */
- } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
- 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG &&
- $token['name'] === 'table')) {
- /* Parse error. Act as if an end tag with the tag name "caption"
- had been seen, then, if that token wasn't ignored, reprocess the
- current token. */
- $this->inCaption(array(
- 'name' => 'caption',
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->inTable($token);
-
- /* An end tag whose tag name is one of: "body", "col", "colgroup",
- "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
- } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
- array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
- 'thead', 'tr'))) {
- // Parse error. Ignore the token.
-
- /* Anything else */
- } else {
- /* Process the token as if the insertion mode was "in body". */
- $this->inBody($token);
- }
- }
-
- private function inColumnGroup($token) {
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append the character to the current node. */
- $text = $this->dom->createTextNode($token['data']);
- end($this->stack)->appendChild($text);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data
- attribute set to the data given in the comment token. */
- $comment = $this->dom->createComment($token['data']);
- end($this->stack)->appendChild($comment);
-
- /* A start tag whose tag name is "col" */
- } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') {
- /* Insert a col element for the token. Immediately pop the current
- node off the stack of open elements. */
- $this->insertElement($token);
- array_pop($this->stack);
-
- /* An end tag whose tag name is "colgroup" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- $token['name'] === 'colgroup') {
- /* If the current node is the root html element, then this is a
- parse error, ignore the token. (innerHTML case) */
- if(end($this->stack)->nodeName === 'html') {
- // Ignore
-
- /* Otherwise, pop the current node (which will be a colgroup
- element) from the stack of open elements. Switch the insertion
- mode to "in table". */
- } else {
- array_pop($this->stack);
- $this->mode = self::IN_TABLE;
- }
-
- /* An end tag whose tag name is "col" */
- } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') {
- /* Parse error. Ignore the token. */
-
- /* Anything else */
- } else {
- /* Act as if an end tag with the tag name "colgroup" had been seen,
- and then, if that token wasn't ignored, reprocess the current token. */
- $this->inColumnGroup(array(
- 'name' => 'colgroup',
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->inTable($token);
- }
- }
-
- private function inTableBody($token) {
- $clear = array('tbody', 'tfoot', 'thead', 'html');
-
- /* A start tag whose tag name is "tr" */
- if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') {
- /* Clear the stack back to a table body context. */
- $this->clearStackToTableContext($clear);
-
- /* Insert a tr element for the token, then switch the insertion
- mode to "in row". */
- $this->insertElement($token);
- $this->mode = self::IN_ROW;
-
- /* A start tag whose tag name is one of: "th", "td" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- ($token['name'] === 'th' || $token['name'] === 'td')) {
- /* Parse error. Act as if a start tag with the tag name "tr" had
- been seen, then reprocess the current token. */
- $this->inTableBody(array(
- 'name' => 'tr',
- 'type' => HTML5::STARTTAG,
- 'attr' => array()
- ));
-
- return $this->inRow($token);
-
- /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as the token, this is a parse error.
- Ignore the token. */
- if(!$this->elementInScope($token['name'], true)) {
- // Ignore
-
- /* Otherwise: */
- } else {
- /* Clear the stack back to a table body context. */
- $this->clearStackToTableContext($clear);
-
- /* Pop the current node from the stack of open elements. Switch
- the insertion mode to "in table". */
- array_pop($this->stack);
- $this->mode = self::IN_TABLE;
- }
-
- /* A start tag whose tag name is one of: "caption", "col", "colgroup",
- "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
- } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) ||
- ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) {
- /* If the stack of open elements does not have a tbody, thead, or
- tfoot element in table scope, this is a parse error. Ignore the
- token. (innerHTML case) */
- if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) {
- // Ignore.
-
- /* Otherwise: */
- } else {
- /* Clear the stack back to a table body context. */
- $this->clearStackToTableContext($clear);
-
- /* Act as if an end tag with the same tag name as the current
- node ("tbody", "tfoot", or "thead") had been seen, then
- reprocess the current token. */
- $this->inTableBody(array(
- 'name' => end($this->stack)->nodeName,
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->mainPhase($token);
- }
-
- /* An end tag whose tag name is one of: "body", "caption", "col",
- "colgroup", "html", "td", "th", "tr" */
- } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
- array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
- /* Parse error. Ignore the token. */
-
- /* Anything else */
- } else {
- /* Process the token as if the insertion mode was "in table". */
- $this->inTable($token);
- }
- }
-
- private function inRow($token) {
- $clear = array('tr', 'html');
-
- /* A start tag whose tag name is one of: "th", "td" */
- if($token['type'] === HTML5::STARTTAG &&
- ($token['name'] === 'th' || $token['name'] === 'td')) {
- /* Clear the stack back to a table row context. */
- $this->clearStackToTableContext($clear);
-
- /* Insert an HTML element for the token, then switch the insertion
- mode to "in cell". */
- $this->insertElement($token);
- $this->mode = self::IN_CELL;
-
- /* Insert a marker at the end of the list of active formatting
- elements. */
- $this->a_formatting[] = self::MARKER;
-
- /* An end tag whose tag name is "tr" */
- } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as the token, this is a parse error.
- Ignore the token. (innerHTML case) */
- if(!$this->elementInScope($token['name'], true)) {
- // Ignore.
-
- /* Otherwise: */
- } else {
- /* Clear the stack back to a table row context. */
- $this->clearStackToTableContext($clear);
-
- /* Pop the current node (which will be a tr element) from the
- stack of open elements. Switch the insertion mode to "in table
- body". */
- array_pop($this->stack);
- $this->mode = self::IN_TBODY;
- }
-
- /* A start tag whose tag name is one of: "caption", "col", "colgroup",
- "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
- } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) {
- /* Act as if an end tag with the tag name "tr" had been seen, then,
- if that token wasn't ignored, reprocess the current token. */
- $this->inRow(array(
- 'name' => 'tr',
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->inCell($token);
-
- /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as the token, this is a parse error.
- Ignore the token. */
- if(!$this->elementInScope($token['name'], true)) {
- // Ignore.
-
- /* Otherwise: */
- } else {
- /* Otherwise, act as if an end tag with the tag name "tr" had
- been seen, then reprocess the current token. */
- $this->inRow(array(
- 'name' => 'tr',
- 'type' => HTML5::ENDTAG
- ));
-
- return $this->inCell($token);
- }
-
- /* An end tag whose tag name is one of: "body", "caption", "col",
- "colgroup", "html", "td", "th" */
- } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
- array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
- /* Parse error. Ignore the token. */
-
- /* Anything else */
- } else {
- /* Process the token as if the insertion mode was "in table". */
- $this->inTable($token);
- }
- }
-
- private function inCell($token) {
- /* An end tag whose tag name is one of: "td", "th" */
- if($token['type'] === HTML5::ENDTAG &&
- ($token['name'] === 'td' || $token['name'] === 'th')) {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as that of the token, then this is a
- parse error and the token must be ignored. */
- if(!$this->elementInScope($token['name'], true)) {
- // Ignore.
-
- /* Otherwise: */
- } else {
- /* Generate implied end tags, except for elements with the same
- tag name as the token. */
- $this->generateImpliedEndTags(array($token['name']));
-
- /* Now, if the current node is not an element with the same tag
- name as the token, then this is a parse error. */
- // k
-
- /* Pop elements from this stack until an element with the same
- tag name as the token has been popped from the stack. */
- while(true) {
- $node = end($this->stack)->nodeName;
- array_pop($this->stack);
-
- if($node === $token['name']) {
- break;
- }
- }
-
- /* Clear the list of active formatting elements up to the last
- marker. */
- $this->clearTheActiveFormattingElementsUpToTheLastMarker();
-
- /* Switch the insertion mode to "in row". (The current node
- will be a tr element at this point.) */
- $this->mode = self::IN_ROW;
- }
-
- /* A start tag whose tag name is one of: "caption", "col", "colgroup",
- "tbody", "td", "tfoot", "th", "thead", "tr" */
- } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
- 'thead', 'tr'))) {
- /* If the stack of open elements does not have a td or th element
- in table scope, then this is a parse error; ignore the token.
- (innerHTML case) */
- if(!$this->elementInScope(array('td', 'th'), true)) {
- // Ignore.
-
- /* Otherwise, close the cell (see below) and reprocess the current
- token. */
- } else {
- $this->closeCell();
- return $this->inRow($token);
- }
-
- /* A start tag whose tag name is one of: "caption", "col", "colgroup",
- "tbody", "td", "tfoot", "th", "thead", "tr" */
- } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
- array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
- 'thead', 'tr'))) {
- /* If the stack of open elements does not have a td or th element
- in table scope, then this is a parse error; ignore the token.
- (innerHTML case) */
- if(!$this->elementInScope(array('td', 'th'), true)) {
- // Ignore.
-
- /* Otherwise, close the cell (see below) and reprocess the current
- token. */
- } else {
- $this->closeCell();
- return $this->inRow($token);
- }
-
- /* An end tag whose tag name is one of: "body", "caption", "col",
- "colgroup", "html" */
- } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
- array('body', 'caption', 'col', 'colgroup', 'html'))) {
- /* Parse error. Ignore the token. */
-
- /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
- "thead", "tr" */
- } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
- array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as that of the token (which can only
- happen for "tbody", "tfoot" and "thead", or, in the innerHTML case),
- then this is a parse error and the token must be ignored. */
- if(!$this->elementInScope($token['name'], true)) {
- // Ignore.
-
- /* Otherwise, close the cell (see below) and reprocess the current
- token. */
- } else {
- $this->closeCell();
- return $this->inRow($token);
- }
-
- /* Anything else */
- } else {
- /* Process the token as if the insertion mode was "in body". */
- $this->inBody($token);
- }
- }
-
- private function inSelect($token) {
- /* Handle the token as follows: */
-
- /* A character token */
- if($token['type'] === HTML5::CHARACTR) {
- /* Append the token's character to the current node. */
- $this->insertText($token['data']);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data
- attribute set to the data given in the comment token. */
- $this->insertComment($token['data']);
-
- /* A start tag token whose tag name is "option" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- $token['name'] === 'option') {
- /* If the current node is an option element, act as if an end tag
- with the tag name "option" had been seen. */
- if(end($this->stack)->nodeName === 'option') {
- $this->inSelect(array(
- 'name' => 'option',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* A start tag token whose tag name is "optgroup" */
- } elseif($token['type'] === HTML5::STARTTAG &&
- $token['name'] === 'optgroup') {
- /* If the current node is an option element, act as if an end tag
- with the tag name "option" had been seen. */
- if(end($this->stack)->nodeName === 'option') {
- $this->inSelect(array(
- 'name' => 'option',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* If the current node is an optgroup element, act as if an end tag
- with the tag name "optgroup" had been seen. */
- if(end($this->stack)->nodeName === 'optgroup') {
- $this->inSelect(array(
- 'name' => 'optgroup',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* An end tag token whose tag name is "optgroup" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- $token['name'] === 'optgroup') {
- /* First, if the current node is an option element, and the node
- immediately before it in the stack of open elements is an optgroup
- element, then act as if an end tag with the tag name "option" had
- been seen. */
- $elements_in_stack = count($this->stack);
-
- if($this->stack[$elements_in_stack - 1]->nodeName === 'option' &&
- $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') {
- $this->inSelect(array(
- 'name' => 'option',
- 'type' => HTML5::ENDTAG
- ));
- }
-
- /* If the current node is an optgroup element, then pop that node
- from the stack of open elements. Otherwise, this is a parse error,
- ignore the token. */
- if($this->stack[$elements_in_stack - 1] === 'optgroup') {
- array_pop($this->stack);
- }
-
- /* An end tag token whose tag name is "option" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- $token['name'] === 'option') {
- /* If the current node is an option element, then pop that node
- from the stack of open elements. Otherwise, this is a parse error,
- ignore the token. */
- if(end($this->stack)->nodeName === 'option') {
- array_pop($this->stack);
- }
-
- /* An end tag whose tag name is "select" */
- } elseif($token['type'] === HTML5::ENDTAG &&
- $token['name'] === 'select') {
- /* If the stack of open elements does not have an element in table
- scope with the same tag name as the token, this is a parse error.
- Ignore the token. (innerHTML case) */
- if(!$this->elementInScope($token['name'], true)) {
- // w/e
-
- /* Otherwise: */
- } else {
- /* Pop elements from the stack of open elements until a select
- element has been popped from the stack. */
- while(true) {
- $current = end($this->stack)->nodeName;
- array_pop($this->stack);
-
- if($current === 'select') {
- break;
- }
- }
-
- /* Reset the insertion mode appropriately. */
- $this->resetInsertionMode();
- }
-
- /* A start tag whose tag name is "select" */
- } elseif($token['name'] === 'select' &&
- $token['type'] === HTML5::STARTTAG) {
- /* Parse error. Act as if the token had been an end tag with the
- tag name "select" instead. */
- $this->inSelect(array(
- 'name' => 'select',
- 'type' => HTML5::ENDTAG
- ));
-
- /* An end tag whose tag name is one of: "caption", "table", "tbody",
- "tfoot", "thead", "tr", "td", "th" */
- } elseif(in_array($token['name'], array('caption', 'table', 'tbody',
- 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) {
- /* Parse error. */
- // w/e
-
- /* If the stack of open elements has an element in table scope with
- the same tag name as that of the token, then act as if an end tag
- with the tag name "select" had been seen, and reprocess the token.
- Otherwise, ignore the token. */
- if($this->elementInScope($token['name'], true)) {
- $this->inSelect(array(
- 'name' => 'select',
- 'type' => HTML5::ENDTAG
- ));
-
- $this->mainPhase($token);
- }
-
- /* Anything else */
- } else {
- /* Parse error. Ignore the token. */
- }
- }
-
- private function afterBody($token) {
- /* Handle the token as follows: */
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Process the token as it would be processed if the insertion mode
- was "in body". */
- $this->inBody($token);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the first element in the stack of open
- elements (the html element), with the data attribute set to the
- data given in the comment token. */
- $comment = $this->dom->createComment($token['data']);
- $this->stack[0]->appendChild($comment);
-
- /* An end tag with the tag name "html" */
- } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') {
- /* If the parser was originally created in order to handle the
- setting of an element's innerHTML attribute, this is a parse error;
- ignore the token. (The element will be an html element in this
- case.) (innerHTML case) */
-
- /* Otherwise, switch to the trailing end phase. */
- $this->phase = self::END_PHASE;
-
- /* Anything else */
- } else {
- /* Parse error. Set the insertion mode to "in body" and reprocess
- the token. */
- $this->mode = self::IN_BODY;
- return $this->inBody($token);
- }
- }
-
- private function inFrameset($token) {
- /* Handle the token as follows: */
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append the character to the current node. */
- $this->insertText($token['data']);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data
- attribute set to the data given in the comment token. */
- $this->insertComment($token['data']);
-
- /* A start tag with the tag name "frameset" */
- } elseif($token['name'] === 'frameset' &&
- $token['type'] === HTML5::STARTTAG) {
- $this->insertElement($token);
-
- /* An end tag with the tag name "frameset" */
- } elseif($token['name'] === 'frameset' &&
- $token['type'] === HTML5::ENDTAG) {
- /* If the current node is the root html element, then this is a
- parse error; ignore the token. (innerHTML case) */
- if(end($this->stack)->nodeName === 'html') {
- // Ignore
-
- } else {
- /* Otherwise, pop the current node from the stack of open
- elements. */
- array_pop($this->stack);
-
- /* If the parser was not originally created in order to handle
- the setting of an element's innerHTML attribute (innerHTML case),
- and the current node is no longer a frameset element, then change
- the insertion mode to "after frameset". */
- $this->mode = self::AFTR_FRAME;
- }
-
- /* A start tag with the tag name "frame" */
- } elseif($token['name'] === 'frame' &&
- $token['type'] === HTML5::STARTTAG) {
- /* Insert an HTML element for the token. */
- $this->insertElement($token);
-
- /* Immediately pop the current node off the stack of open elements. */
- array_pop($this->stack);
-
- /* A start tag with the tag name "noframes" */
- } elseif($token['name'] === 'noframes' &&
- $token['type'] === HTML5::STARTTAG) {
- /* Process the token as if the insertion mode had been "in body". */
- $this->inBody($token);
-
- /* Anything else */
- } else {
- /* Parse error. Ignore the token. */
- }
- }
-
- private function afterFrameset($token) {
- /* Handle the token as follows: */
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
- if($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Append the character to the current node. */
- $this->insertText($token['data']);
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the current node with the data
- attribute set to the data given in the comment token. */
- $this->insertComment($token['data']);
-
- /* An end tag with the tag name "html" */
- } elseif($token['name'] === 'html' &&
- $token['type'] === HTML5::ENDTAG) {
- /* Switch to the trailing end phase. */
- $this->phase = self::END_PHASE;
-
- /* A start tag with the tag name "noframes" */
- } elseif($token['name'] === 'noframes' &&
- $token['type'] === HTML5::STARTTAG) {
- /* Process the token as if the insertion mode had been "in body". */
- $this->inBody($token);
-
- /* Anything else */
- } else {
- /* Parse error. Ignore the token. */
- }
- }
-
- private function trailingEndPhase($token) {
- /* After the main phase, as each token is emitted from the tokenisation
- stage, it must be processed as described in this section. */
-
- /* A DOCTYPE token */
- if($token['type'] === HTML5::DOCTYPE) {
- // Parse error. Ignore the token.
-
- /* A comment token */
- } elseif($token['type'] === HTML5::COMMENT) {
- /* Append a Comment node to the Document object with the data
- attribute set to the data given in the comment token. */
- $comment = $this->dom->createComment($token['data']);
- $this->dom->appendChild($comment);
-
- /* A character token that is one of one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE */
- } elseif($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
- /* Process the token as it would be processed in the main phase. */
- $this->mainPhase($token);
-
- /* A character token that is not one of U+0009 CHARACTER TABULATION,
- U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
- or U+0020 SPACE. Or a start tag token. Or an end tag token. */
- } elseif(($token['type'] === HTML5::CHARACTR &&
- preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
- $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) {
- /* Parse error. Switch back to the main phase and reprocess the
- token. */
- $this->phase = self::MAIN_PHASE;
- return $this->mainPhase($token);
-
- /* An end-of-file token */
- } elseif($token['type'] === HTML5::EOF) {
- /* OMG DONE!! */
- }
- }
-
- private function insertElement($token, $append = true) {
- $el = $this->dom->createElement($token['name']);
-
- foreach($token['attr'] as $attr) {
- if(!$el->hasAttribute($attr['name'])) {
- $el->setAttribute($attr['name'], $attr['value']);
- }
- }
-
- $this->appendToRealParent($el);
- $this->stack[] = $el;
-
- return $el;
- }
-
- private function insertText($data) {
- $text = $this->dom->createTextNode($data);
- $this->appendToRealParent($text);
- }
-
- private function insertComment($data) {
- $comment = $this->dom->createComment($data);
- $this->appendToRealParent($comment);
- }
-
- private function appendToRealParent($node) {
- if($this->foster_parent === null) {
- end($this->stack)->appendChild($node);
-
- } elseif($this->foster_parent !== null) {
- /* If the foster parent element is the parent element of the
- last table element in the stack of open elements, then the new
- node must be inserted immediately before the last table element
- in the stack of open elements in the foster parent element;
- otherwise, the new node must be appended to the foster parent
- element. */
- for($n = count($this->stack) - 1; $n >= 0; $n--) {
- if($this->stack[$n]->nodeName === 'table' &&
- $this->stack[$n]->parentNode !== null) {
- $table = $this->stack[$n];
- break;
- }
- }
-
- if(isset($table) && $this->foster_parent->isSameNode($table->parentNode))
- $this->foster_parent->insertBefore($node, $table);
- else
- $this->foster_parent->appendChild($node);
-
- $this->foster_parent = null;
- }
- }
-
- private function elementInScope($el, $table = false) {
- if(is_array($el)) {
- foreach($el as $element) {
- if($this->elementInScope($element, $table)) {
- return true;
- }
- }
-
- return false;
- }
-
- $leng = count($this->stack);
-
- for($n = 0; $n < $leng; $n++) {
- /* 1. Initialise node to be the current node (the bottommost node of
- the stack). */
- $node = $this->stack[$leng - 1 - $n];
-
- if($node->tagName === $el) {
- /* 2. If node is the target node, terminate in a match state. */
- return true;
-
- } elseif($node->tagName === 'table') {
- /* 3. Otherwise, if node is a table element, terminate in a failure
- state. */
- return false;
-
- } elseif($table === true && in_array($node->tagName, array('caption', 'td',
- 'th', 'button', 'marquee', 'object'))) {
- /* 4. Otherwise, if the algorithm is the "has an element in scope"
- variant (rather than the "has an element in table scope" variant),
- and node is one of the following, terminate in a failure state. */
- return false;
-
- } elseif($node === $node->ownerDocument->documentElement) {
- /* 5. Otherwise, if node is an html element (root element), terminate
- in a failure state. (This can only happen if the node is the topmost
- node of the stack of open elements, and prevents the next step from
- being invoked if there are no more elements in the stack.) */
- return false;
- }
-
- /* Otherwise, set node to the previous entry in the stack of open
- elements and return to step 2. (This will never fail, since the loop
- will always terminate in the previous step if the top of the stack
- is reached.) */
- }
- }
-
- private function reconstructActiveFormattingElements() {
- /* 1. If there are no entries in the list of active formatting elements,
- then there is nothing to reconstruct; stop this algorithm. */
- $formatting_elements = count($this->a_formatting);
-
- if($formatting_elements === 0) {
- return false;
- }
-
- /* 3. Let entry be the last (most recently added) element in the list
- of active formatting elements. */
- $entry = end($this->a_formatting);
-
- /* 2. If the last (most recently added) entry in the list of active
- formatting elements is a marker, or if it is an element that is in the
- stack of open elements, then there is nothing to reconstruct; stop this
- algorithm. */
- if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
- return false;
- }
-
- for($a = $formatting_elements - 1; $a >= 0; true) {
- /* 4. If there are no entries before entry in the list of active
- formatting elements, then jump to step 8. */
- if($a === 0) {
- $step_seven = false;
- break;
- }
-
- /* 5. Let entry be the entry one earlier than entry in the list of
- active formatting elements. */
- $a--;
- $entry = $this->a_formatting[$a];
-
- /* 6. If entry is neither a marker nor an element that is also in
- thetack of open elements, go to step 4. */
- if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
- break;
- }
- }
-
- while(true) {
- /* 7. Let entry be the element one later than entry in the list of
- active formatting elements. */
- if(isset($step_seven) && $step_seven === true) {
- $a++;
- $entry = $this->a_formatting[$a];
- }
-
- /* 8. Perform a shallow clone of the element entry to obtain clone. */
- $clone = $entry->cloneNode();
-
- /* 9. Append clone to the current node and push it onto the stack
- of open elements so that it is the new current node. */
- end($this->stack)->appendChild($clone);
- $this->stack[] = $clone;
-
- /* 10. Replace the entry for entry in the list with an entry for
- clone. */
- $this->a_formatting[$a] = $clone;
-
- /* 11. If the entry for clone in the list of active formatting
- elements is not the last entry in the list, return to step 7. */
- if(end($this->a_formatting) !== $clone) {
- $step_seven = true;
- } else {
- break;
- }
- }
- }
-
- private function clearTheActiveFormattingElementsUpToTheLastMarker() {
- /* When the steps below require the UA to clear the list of active
- formatting elements up to the last marker, the UA must perform the
- following steps: */
-
- while(true) {
- /* 1. Let entry be the last (most recently added) entry in the list
- of active formatting elements. */
- $entry = end($this->a_formatting);
-
- /* 2. Remove entry from the list of active formatting elements. */
- array_pop($this->a_formatting);
-
- /* 3. If entry was a marker, then stop the algorithm at this point.
- The list has been cleared up to the last marker. */
- if($entry === self::MARKER) {
- break;
- }
- }
- }
-
- private function generateImpliedEndTags($exclude = array()) {
- /* When the steps below require the UA to generate implied end tags,
- then, if the current node is a dd element, a dt element, an li element,
- a p element, a td element, a th element, or a tr element, the UA must
- act as if an end tag with the respective tag name had been seen and
- then generate implied end tags again. */
- $node = end($this->stack);
- $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
-
- while(in_array(end($this->stack)->nodeName, $elements)) {
- array_pop($this->stack);
- }
- }
-
- private function getElementCategory($node) {
- $name = $node->tagName;
- if(in_array($name, $this->special))
- return self::SPECIAL;
-
- elseif(in_array($name, $this->scoping))
- return self::SCOPING;
-
- elseif(in_array($name, $this->formatting))
- return self::FORMATTING;
-
- else
- return self::PHRASING;
- }
-
- private function clearStackToTableContext($elements) {
- /* When the steps above require the UA to clear the stack back to a
- table context, it means that the UA must, while the current node is not
- a table element or an html element, pop elements from the stack of open
- elements. If this causes any elements to be popped from the stack, then
- this is a parse error. */
- while(true) {
- $node = end($this->stack)->nodeName;
-
- if(in_array($node, $elements)) {
- break;
- } else {
- array_pop($this->stack);
- }
- }
- }
-
- private function resetInsertionMode() {
- /* 1. Let last be false. */
- $last = false;
- $leng = count($this->stack);
-
- for($n = $leng - 1; $n >= 0; $n--) {
- /* 2. Let node be the last node in the stack of open elements. */
- $node = $this->stack[$n];
-
- /* 3. If node is the first node in the stack of open elements, then
- set last to true. If the element whose innerHTML attribute is being
- set is neither a td element nor a th element, then set node to the
- element whose innerHTML attribute is being set. (innerHTML case) */
- if($this->stack[0]->isSameNode($node)) {
- $last = true;
- }
-
- /* 4. If node is a select element, then switch the insertion mode to
- "in select" and abort these steps. (innerHTML case) */
- if($node->nodeName === 'select') {
- $this->mode = self::IN_SELECT;
- break;
-
- /* 5. If node is a td or th element, then switch the insertion mode
- to "in cell" and abort these steps. */
- } elseif($node->nodeName === 'td' || $node->nodeName === 'th') {
- $this->mode = self::IN_CELL;
- break;
-
- /* 6. If node is a tr element, then switch the insertion mode to
- "in row" and abort these steps. */
- } elseif($node->nodeName === 'tr') {
- $this->mode = self::IN_ROW;
- break;
-
- /* 7. If node is a tbody, thead, or tfoot element, then switch the
- insertion mode to "in table body" and abort these steps. */
- } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) {
- $this->mode = self::IN_TBODY;
- break;
-
- /* 8. If node is a caption element, then switch the insertion mode
- to "in caption" and abort these steps. */
- } elseif($node->nodeName === 'caption') {
- $this->mode = self::IN_CAPTION;
- break;
-
- /* 9. If node is a colgroup element, then switch the insertion mode
- to "in column group" and abort these steps. (innerHTML case) */
- } elseif($node->nodeName === 'colgroup') {
- $this->mode = self::IN_CGROUP;
- break;
-
- /* 10. If node is a table element, then switch the insertion mode
- to "in table" and abort these steps. */
- } elseif($node->nodeName === 'table') {
- $this->mode = self::IN_TABLE;
- break;
-
- /* 11. If node is a head element, then switch the insertion mode
- to "in body" ("in body"! not "in head"!) and abort these steps.
- (innerHTML case) */
- } elseif($node->nodeName === 'head') {
- $this->mode = self::IN_BODY;
- break;
-
- /* 12. If node is a body element, then switch the insertion mode to
- "in body" and abort these steps. */
- } elseif($node->nodeName === 'body') {
- $this->mode = self::IN_BODY;
- break;
-
- /* 13. If node is a frameset element, then switch the insertion
- mode to "in frameset" and abort these steps. (innerHTML case) */
- } elseif($node->nodeName === 'frameset') {
- $this->mode = self::IN_FRAME;
- break;
-
- /* 14. If node is an html element, then: if the head element
- pointer is null, switch the insertion mode to "before head",
- otherwise, switch the insertion mode to "after head". In either
- case, abort these steps. (innerHTML case) */
- } elseif($node->nodeName === 'html') {
- $this->mode = ($this->head_pointer === null)
- ? self::BEFOR_HEAD
- : self::AFTER_HEAD;
-
- break;
-
- /* 15. If last is true, then set the insertion mode to "in body"
- and abort these steps. (innerHTML case) */
- } elseif($last) {
- $this->mode = self::IN_BODY;
- break;
- }
- }
- }
-
- private function closeCell() {
- /* If the stack of open elements has a td or th element in table scope,
- then act as if an end tag token with that tag name had been seen. */
- foreach(array('td', 'th') as $cell) {
- if($this->elementInScope($cell, true)) {
- $this->inCell(array(
- 'name' => $cell,
- 'type' => HTML5::ENDTAG
- ));
-
- break;
- }
- }
- }
-
- public function save() {
- return $this->dom;
- }
-}
-?>
+normalize($html, $config, $context);
+ $new_html = $this->wrapHTML($new_html, $config, $context);
+ try {
+ $parser = new HTML5($new_html);
+ $doc = $parser->save();
+ } catch (DOMException $e) {
+ // Uh oh, it failed. Punt to DirectLex.
+ $lexer = new HTMLPurifier_Lexer_DirectLex();
+ $context->register('PH5PError', $e); // save the error, so we can detect it
+ return $lexer->tokenizeHTML($html, $config, $context); // use original HTML
+ }
+ $tokens = array();
+ $this->tokenizeDOM(
+ $doc->getElementsByTagName('html')->item(0)-> //
+ getElementsByTagName('body')->item(0)-> //
+ getElementsByTagName('div')->item(0) //
+ , $tokens);
+ return $tokens;
+ }
+
+}
+
+/*
+
+Copyright 2007 Jeroen van der Meer
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+class HTML5 {
+ private $data;
+ private $char;
+ private $EOF;
+ private $state;
+ private $tree;
+ private $token;
+ private $content_model;
+ private $escape = false;
+ private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute',
+ 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;',
+ 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;',
+ 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;',
+ 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;',
+ 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;',
+ 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;',
+ 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;',
+ 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;',
+ 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN',
+ 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;',
+ 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;',
+ 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig',
+ 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;',
+ 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;',
+ 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil',
+ 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;',
+ 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;',
+ 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;',
+ 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth',
+ 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12',
+ 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt',
+ 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc',
+ 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;',
+ 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;',
+ 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;',
+ 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro',
+ 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;',
+ 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;',
+ 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;',
+ 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash',
+ 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;',
+ 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;',
+ 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;',
+ 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;',
+ 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;',
+ 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;',
+ 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;',
+ 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;',
+ 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc',
+ 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;',
+ 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;');
+
+ const PCDATA = 0;
+ const RCDATA = 1;
+ const CDATA = 2;
+ const PLAINTEXT = 3;
+
+ const DOCTYPE = 0;
+ const STARTTAG = 1;
+ const ENDTAG = 2;
+ const COMMENT = 3;
+ const CHARACTR = 4;
+ const EOF = 5;
+
+ public function __construct($data) {
+ $data = str_replace("\r\n", "\n", $data);
+ $data = str_replace("\r", null, $data);
+
+ $this->data = $data;
+ $this->char = -1;
+ $this->EOF = strlen($data);
+ $this->tree = new HTML5TreeConstructer;
+ $this->content_model = self::PCDATA;
+
+ $this->state = 'data';
+
+ while($this->state !== null) {
+ $this->{$this->state.'State'}();
+ }
+ }
+
+ public function save() {
+ return $this->tree->save();
+ }
+
+ private function char() {
+ return ($this->char < $this->EOF)
+ ? $this->data[$this->char]
+ : false;
+ }
+
+ private function character($s, $l = 0) {
+ if($s + $l < $this->EOF) {
+ if($l === 0) {
+ return $this->data[$s];
+ } else {
+ return substr($this->data, $s, $l);
+ }
+ }
+ }
+
+ private function characters($char_class, $start) {
+ return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start));
+ }
+
+ private function dataState() {
+ // Consume the next input character
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) {
+ /* U+0026 AMPERSAND (&)
+ When the content model flag is set to one of the PCDATA or RCDATA
+ states: switch to the entity data state. Otherwise: treat it as per
+ the "anything else" entry below. */
+ $this->state = 'entityData';
+
+ } elseif($char === '-') {
+ /* If the content model flag is set to either the RCDATA state or
+ the CDATA state, and the escape flag is false, and there are at
+ least three characters before this one in the input stream, and the
+ last four characters in the input stream, including this one, are
+ U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
+ and U+002D HYPHEN-MINUS (""),
+ set the escape flag to false. */
+ if(($this->content_model === self::RCDATA ||
+ $this->content_model === self::CDATA) && $this->escape === true &&
+ $this->character($this->char, 3) === '-->') {
+ $this->escape = false;
+ }
+
+ /* In any case, emit the input character as a character token.
+ Stay in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Emit an end-of-file token. */
+ $this->EOF();
+
+ } elseif($this->content_model === self::PLAINTEXT) {
+ /* When the content model flag is set to the PLAINTEXT state
+ THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of
+ the text and emit it as a character token. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => substr($this->data, $this->char)
+ ));
+
+ $this->EOF();
+
+ } else {
+ /* Anything else
+ THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that
+ otherwise would also be treated as a character token and emit it
+ as a single character token. Stay in the data state. */
+ $len = strcspn($this->data, '<&', $this->char);
+ $char = substr($this->data, $this->char, $len);
+ $this->char += $len - 1;
+
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ $this->state = 'data';
+ }
+ }
+
+ private function entityDataState() {
+ // Attempt to consume an entity.
+ $entity = $this->entity();
+
+ // If nothing is returned, emit a U+0026 AMPERSAND character token.
+ // Otherwise, emit the character token that was returned.
+ $char = (!$entity) ? '&' : $entity;
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => $char
+ ));
+
+ // Finally, switch to the data state.
+ $this->state = 'data';
+ }
+
+ private function tagOpenState() {
+ switch($this->content_model) {
+ case self::RCDATA:
+ case self::CDATA:
+ /* If the next input character is a U+002F SOLIDUS (/) character,
+ consume it and switch to the close tag open state. If the next
+ input character is not a U+002F SOLIDUS (/) character, emit a
+ U+003C LESS-THAN SIGN character token and switch to the data
+ state to process the next input character. */
+ if($this->character($this->char + 1) === '/') {
+ $this->char++;
+ $this->state = 'closeTagOpen';
+
+ } else {
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '<'
+ ));
+
+ $this->state = 'data';
+ }
+ break;
+
+ case self::PCDATA:
+ // If the content model flag is set to the PCDATA state
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '!') {
+ /* U+0021 EXCLAMATION MARK (!)
+ Switch to the markup declaration open state. */
+ $this->state = 'markupDeclarationOpen';
+
+ } elseif($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Switch to the close tag open state. */
+ $this->state = 'closeTagOpen';
+
+ } elseif(preg_match('/^[A-Za-z]$/', $char)) {
+ /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
+ Create a new start tag token, set its tag name to the lowercase
+ version of the input character (add 0x0020 to the character's code
+ point), then switch to the tag name state. (Don't emit the token
+ yet; further details will be filled in before it is emitted.) */
+ $this->token = array(
+ 'name' => strtolower($char),
+ 'type' => self::STARTTAG,
+ 'attr' => array()
+ );
+
+ $this->state = 'tagName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and a
+ U+003E GREATER-THAN SIGN character token. Switch to the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '<>'
+ ));
+
+ $this->state = 'data';
+
+ } elseif($char === '?') {
+ /* U+003F QUESTION MARK (?)
+ Parse error. Switch to the bogus comment state. */
+ $this->state = 'bogusComment';
+
+ } else {
+ /* Anything else
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and
+ reconsume the current input character in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => '<'
+ ));
+
+ $this->char--;
+ $this->state = 'data';
+ }
+ break;
+ }
+ }
+
+ private function closeTagOpenState() {
+ $next_node = strtolower($this->characters('A-Za-z', $this->char + 1));
+ $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName;
+
+ if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) &&
+ (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/',
+ $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) {
+ /* If the content model flag is set to the RCDATA or CDATA states then
+ examine the next few characters. If they do not match the tag name of
+ the last start tag token emitted (case insensitively), or if they do but
+ they are not immediately followed by one of the following characters:
+ * U+0009 CHARACTER TABULATION
+ * U+000A LINE FEED (LF)
+ * U+000B LINE TABULATION
+ * U+000C FORM FEED (FF)
+ * U+0020 SPACE
+ * U+003E GREATER-THAN SIGN (>)
+ * U+002F SOLIDUS (/)
+ * EOF
+ ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character
+ token, a U+002F SOLIDUS character token, and switch to the data state
+ to process the next input character. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => ''
+ ));
+
+ $this->state = 'data';
+
+ } else {
+ /* Otherwise, if the content model flag is set to the PCDATA state,
+ or if the next few characters do match that tag name, consume the
+ next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[A-Za-z]$/', $char)) {
+ /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
+ Create a new end tag token, set its tag name to the lowercase version
+ of the input character (add 0x0020 to the character's code point), then
+ switch to the tag name state. (Don't emit the token yet; further details
+ will be filled in before it is emitted.) */
+ $this->token = array(
+ 'name' => strtolower($char),
+ 'type' => self::ENDTAG
+ );
+
+ $this->state = 'tagName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Parse error. Switch to the data state. */
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
+ SOLIDUS character token. Reconsume the EOF character in the data state. */
+ $this->emitToken(array(
+ 'type' => self::CHARACTR,
+ 'data' => ''
+ ));
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Parse error. Switch to the bogus comment state. */
+ $this->state = 'bogusComment';
+ }
+ }
+ }
+
+ private function tagNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } elseif($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current tag token's tag name.
+ Stay in the tag name state. */
+ $this->token['name'] .= strtolower($char);
+ $this->state = 'tagName';
+ }
+ }
+
+ private function beforeAttributeNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '/') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Stay in the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Start a new attribute in the current tag token. Set that attribute's
+ name to the current input character, and its value to the empty string.
+ Switch to the attribute name state. */
+ $this->token['attr'][] = array(
+ 'name' => strtolower($char),
+ 'value' => null
+ );
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function attributeNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute name state. */
+ $this->state = 'afterAttributeName';
+
+ } elseif($char === '=') {
+ /* U+003D EQUALS SIGN (=)
+ Switch to the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '/' && $this->character($this->char + 1) !== '>') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the before
+ attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's name.
+ Stay in the attribute name state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['name'] .= strtolower($char);
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function afterAttributeNameState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the after attribute name state. */
+ $this->state = 'afterAttributeName';
+
+ } elseif($char === '=') {
+ /* U+003D EQUALS SIGN (=)
+ Switch to the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '/' && $this->character($this->char + 1) !== '>') {
+ /* U+002F SOLIDUS (/)
+ Parse error unless this is a permitted slash. Switch to the
+ before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the EOF
+ character in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Start a new attribute in the current tag token. Set that attribute's
+ name to the current input character, and its value to the empty string.
+ Switch to the attribute name state. */
+ $this->token['attr'][] = array(
+ 'name' => strtolower($char),
+ 'value' => null
+ );
+
+ $this->state = 'attributeName';
+ }
+ }
+
+ private function beforeAttributeValueState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Stay in the before attribute value state. */
+ $this->state = 'beforeAttributeValue';
+
+ } elseif($char === '"') {
+ /* U+0022 QUOTATION MARK (")
+ Switch to the attribute value (double-quoted) state. */
+ $this->state = 'attributeValueDoubleQuoted';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the attribute value (unquoted) state and reconsume
+ this input character. */
+ $this->char--;
+ $this->state = 'attributeValueUnquoted';
+
+ } elseif($char === '\'') {
+ /* U+0027 APOSTROPHE (')
+ Switch to the attribute value (single-quoted) state. */
+ $this->state = 'attributeValueSingleQuoted';
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Switch to the attribute value (unquoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueUnquoted';
+ }
+ }
+
+ private function attributeValueDoubleQuotedState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if($char === '"') {
+ /* U+0022 QUOTATION MARK (")
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState('double');
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the character
+ in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (double-quoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueDoubleQuoted';
+ }
+ }
+
+ private function attributeValueSingleQuotedState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if($char === '\'') {
+ /* U+0022 QUOTATION MARK (')
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState('single');
+
+ } elseif($this->char === $this->EOF) {
+ /* EOF
+ Parse error. Emit the current tag token. Reconsume the character
+ in the data state. */
+ $this->emitToken($this->token);
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (single-quoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueSingleQuoted';
+ }
+ }
+
+ private function attributeValueUnquotedState() {
+ // Consume the next input character:
+ $this->char++;
+ $char = $this->character($this->char);
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ /* U+0009 CHARACTER TABULATION
+ U+000A LINE FEED (LF)
+ U+000B LINE TABULATION
+ U+000C FORM FEED (FF)
+ U+0020 SPACE
+ Switch to the before attribute name state. */
+ $this->state = 'beforeAttributeName';
+
+ } elseif($char === '&') {
+ /* U+0026 AMPERSAND (&)
+ Switch to the entity in attribute value state. */
+ $this->entityInAttributeValueState();
+
+ } elseif($char === '>') {
+ /* U+003E GREATER-THAN SIGN (>)
+ Emit the current tag token. Switch to the data state. */
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } else {
+ /* Anything else
+ Append the current input character to the current attribute's value.
+ Stay in the attribute value (unquoted) state. */
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+
+ $this->state = 'attributeValueUnquoted';
+ }
+ }
+
+ private function entityInAttributeValueState() {
+ // Attempt to consume an entity.
+ $entity = $this->entity();
+
+ // If nothing is returned, append a U+0026 AMPERSAND character to the
+ // current attribute's value. Otherwise, emit the character token that
+ // was returned.
+ $char = (!$entity)
+ ? '&'
+ : $entity;
+
+ $last = count($this->token['attr']) - 1;
+ $this->token['attr'][$last]['value'] .= $char;
+ }
+
+ private function bogusCommentState() {
+ /* Consume every character up to the first U+003E GREATER-THAN SIGN
+ character (>) or the end of the file (EOF), whichever comes first. Emit
+ a comment token whose data is the concatenation of all the characters
+ starting from and including the character that caused the state machine
+ to switch into the bogus comment state, up to and including the last
+ consumed character before the U+003E character, if any, or up to the
+ end of the file otherwise. (If the comment was started by the end of
+ the file (EOF), the token is empty.) */
+ $data = $this->characters('^>', $this->char);
+ $this->emitToken(array(
+ 'data' => $data,
+ 'type' => self::COMMENT
+ ));
+
+ $this->char += strlen($data);
+
+ /* Switch to the data state. */
+ $this->state = 'data';
+
+ /* If the end of the file was reached, reconsume the EOF character. */
+ if($this->char === $this->EOF) {
+ $this->char = $this->EOF - 1;
+ }
+ }
+
+ private function markupDeclarationOpenState() {
+ /* If the next two characters are both U+002D HYPHEN-MINUS (-)
+ characters, consume those two characters, create a comment token whose
+ data is the empty string, and switch to the comment state. */
+ if($this->character($this->char + 1, 2) === '--') {
+ $this->char += 2;
+ $this->state = 'comment';
+ $this->token = array(
+ 'data' => null,
+ 'type' => self::COMMENT
+ );
+
+ /* Otherwise if the next seven chacacters are a case-insensitive match
+ for the word "DOCTYPE", then consume those characters and switch to the
+ DOCTYPE state. */
+ } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') {
+ $this->char += 7;
+ $this->state = 'doctype';
+
+ /* Otherwise, is is a parse error. Switch to the bogus comment state.
+ The next character that is consumed, if any, is the first character
+ that will be in the comment. */
+ } else {
+ $this->char++;
+ $this->state = 'bogusComment';
+ }
+ }
+
+ private function commentState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ /* U+002D HYPHEN-MINUS (-) */
+ if($char === '-') {
+ /* Switch to the comment dash state */
+ $this->state = 'commentDash';
+
+ /* EOF */
+ } elseif($this->char === $this->EOF) {
+ /* Parse error. Emit the comment token. Reconsume the EOF character
+ in the data state. */
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ /* Anything else */
+ } else {
+ /* Append the input character to the comment token's data. Stay in
+ the comment state. */
+ $this->token['data'] .= $char;
+ }
+ }
+
+ private function commentDashState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ /* U+002D HYPHEN-MINUS (-) */
+ if($char === '-') {
+ /* Switch to the comment end state */
+ $this->state = 'commentEnd';
+
+ /* EOF */
+ } elseif($this->char === $this->EOF) {
+ /* Parse error. Emit the comment token. Reconsume the EOF character
+ in the data state. */
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ /* Anything else */
+ } else {
+ /* Append a U+002D HYPHEN-MINUS (-) character and the input
+ character to the comment token's data. Switch to the comment state. */
+ $this->token['data'] .= '-'.$char;
+ $this->state = 'comment';
+ }
+ }
+
+ private function commentEndState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($char === '-') {
+ $this->token['data'] .= '-';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['data'] .= '--'.$char;
+ $this->state = 'comment';
+ }
+ }
+
+ private function doctypeState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ $this->state = 'beforeDoctypeName';
+
+ } else {
+ $this->char--;
+ $this->state = 'beforeDoctypeName';
+ }
+ }
+
+ private function beforeDoctypeNameState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ // Stay in the before DOCTYPE name state.
+
+ } elseif(preg_match('/^[a-z]$/', $char)) {
+ $this->token = array(
+ 'name' => strtoupper($char),
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ );
+
+ $this->state = 'doctypeName';
+
+ } elseif($char === '>') {
+ $this->emitToken(array(
+ 'name' => null,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ ));
+
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken(array(
+ 'name' => null,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ ));
+
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token = array(
+ 'name' => $char,
+ 'type' => self::DOCTYPE,
+ 'error' => true
+ );
+
+ $this->state = 'doctypeName';
+ }
+ }
+
+ private function doctypeNameState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ $this->state = 'AfterDoctypeName';
+
+ } elseif($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif(preg_match('/^[a-z]$/', $char)) {
+ $this->token['name'] .= strtoupper($char);
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['name'] .= $char;
+ }
+
+ $this->token['error'] = ($this->token['name'] === 'HTML')
+ ? false
+ : true;
+ }
+
+ private function afterDoctypeNameState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) {
+ // Stay in the DOCTYPE name state.
+
+ } elseif($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ $this->token['error'] = true;
+ $this->state = 'bogusDoctype';
+ }
+ }
+
+ private function bogusDoctypeState() {
+ /* Consume the next input character: */
+ $this->char++;
+ $char = $this->char();
+
+ if($char === '>') {
+ $this->emitToken($this->token);
+ $this->state = 'data';
+
+ } elseif($this->char === $this->EOF) {
+ $this->emitToken($this->token);
+ $this->char--;
+ $this->state = 'data';
+
+ } else {
+ // Stay in the bogus DOCTYPE state.
+ }
+ }
+
+ private function entity() {
+ $start = $this->char;
+
+ // This section defines how to consume an entity. This definition is
+ // used when parsing entities in text and in attributes.
+
+ // The behaviour depends on the identity of the next character (the
+ // one immediately after the U+0026 AMPERSAND character):
+
+ switch($this->character($this->char + 1)) {
+ // U+0023 NUMBER SIGN (#)
+ case '#':
+
+ // The behaviour further depends on the character after the
+ // U+0023 NUMBER SIGN:
+ switch($this->character($this->char + 1)) {
+ // U+0078 LATIN SMALL LETTER X
+ // U+0058 LATIN CAPITAL LETTER X
+ case 'x':
+ case 'X':
+ // Follow the steps below, but using the range of
+ // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
+ // NINE, U+0061 LATIN SMALL LETTER A through to U+0066
+ // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER
+ // A, through to U+0046 LATIN CAPITAL LETTER F (in other
+ // words, 0-9, A-F, a-f).
+ $char = 1;
+ $char_class = '0-9A-Fa-f';
+ break;
+
+ // Anything else
+ default:
+ // Follow the steps below, but using the range of
+ // characters U+0030 DIGIT ZERO through to U+0039 DIGIT
+ // NINE (i.e. just 0-9).
+ $char = 0;
+ $char_class = '0-9';
+ break;
+ }
+
+ // Consume as many characters as match the range of characters
+ // given above.
+ $this->char++;
+ $e_name = $this->characters($char_class, $this->char + $char + 1);
+ $entity = $this->character($start, $this->char);
+ $cond = strlen($e_name) > 0;
+
+ // The rest of the parsing happens bellow.
+ break;
+
+ // Anything else
+ default:
+ // Consume the maximum number of characters possible, with the
+ // consumed characters case-sensitively matching one of the
+ // identifiers in the first column of the entities table.
+ $e_name = $this->characters('0-9A-Za-z;', $this->char + 1);
+ $len = strlen($e_name);
+
+ for($c = 1; $c <= $len; $c++) {
+ $id = substr($e_name, 0, $c);
+ $this->char++;
+
+ if(in_array($id, $this->entities)) {
+ if ($e_name[$c-1] !== ';') {
+ if ($c < $len && $e_name[$c] == ';') {
+ $this->char++; // consume extra semicolon
+ }
+ }
+ $entity = $id;
+ break;
+ }
+ }
+
+ $cond = isset($entity);
+ // The rest of the parsing happens bellow.
+ break;
+ }
+
+ if(!$cond) {
+ // If no match can be made, then this is a parse error. No
+ // characters are consumed, and nothing is returned.
+ $this->char = $start;
+ return false;
+ }
+
+ // Return a character token for the character corresponding to the
+ // entity name (as given by the second column of the entities table).
+ return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8');
+ }
+
+ private function emitToken($token) {
+ $emit = $this->tree->emitToken($token);
+
+ if(is_int($emit)) {
+ $this->content_model = $emit;
+
+ } elseif($token['type'] === self::ENDTAG) {
+ $this->content_model = self::PCDATA;
+ }
+ }
+
+ private function EOF() {
+ $this->state = null;
+ $this->tree->emitToken(array(
+ 'type' => self::EOF
+ ));
+ }
+}
+
+class HTML5TreeConstructer {
+ public $stack = array();
+
+ private $phase;
+ private $mode;
+ private $dom;
+ private $foster_parent = null;
+ private $a_formatting = array();
+
+ private $head_pointer = null;
+ private $form_pointer = null;
+
+ private $scoping = array('button','caption','html','marquee','object','table','td','th');
+ private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u');
+ private $special = array('address','area','base','basefont','bgsound',
+ 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl',
+ 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5',
+ 'h6','head','hr','iframe','image','img','input','isindex','li','link',
+ 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup',
+ 'option','p','param','plaintext','pre','script','select','spacer','style',
+ 'tbody','textarea','tfoot','thead','title','tr','ul','wbr');
+
+ // The different phases.
+ const INIT_PHASE = 0;
+ const ROOT_PHASE = 1;
+ const MAIN_PHASE = 2;
+ const END_PHASE = 3;
+
+ // The different insertion modes for the main phase.
+ const BEFOR_HEAD = 0;
+ const IN_HEAD = 1;
+ const AFTER_HEAD = 2;
+ const IN_BODY = 3;
+ const IN_TABLE = 4;
+ const IN_CAPTION = 5;
+ const IN_CGROUP = 6;
+ const IN_TBODY = 7;
+ const IN_ROW = 8;
+ const IN_CELL = 9;
+ const IN_SELECT = 10;
+ const AFTER_BODY = 11;
+ const IN_FRAME = 12;
+ const AFTR_FRAME = 13;
+
+ // The different types of elements.
+ const SPECIAL = 0;
+ const SCOPING = 1;
+ const FORMATTING = 2;
+ const PHRASING = 3;
+
+ const MARKER = 0;
+
+ public function __construct() {
+ $this->phase = self::INIT_PHASE;
+ $this->mode = self::BEFOR_HEAD;
+ $this->dom = new DOMDocument;
+
+ $this->dom->encoding = 'UTF-8';
+ $this->dom->preserveWhiteSpace = true;
+ $this->dom->substituteEntities = true;
+ $this->dom->strictErrorChecking = false;
+ }
+
+ // Process tag tokens
+ public function emitToken($token) {
+ switch($this->phase) {
+ case self::INIT_PHASE: return $this->initPhase($token); break;
+ case self::ROOT_PHASE: return $this->rootElementPhase($token); break;
+ case self::MAIN_PHASE: return $this->mainPhase($token); break;
+ case self::END_PHASE : return $this->trailingEndPhase($token); break;
+ }
+ }
+
+ private function initPhase($token) {
+ /* Initially, the tree construction stage must handle each token
+ emitted from the tokenisation stage as follows: */
+
+ /* A DOCTYPE token that is marked as being in error
+ A comment token
+ A start tag token
+ An end tag token
+ A character token that is not one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE
+ An end-of-file token */
+ if((isset($token['error']) && $token['error']) ||
+ $token['type'] === HTML5::COMMENT ||
+ $token['type'] === HTML5::STARTTAG ||
+ $token['type'] === HTML5::ENDTAG ||
+ $token['type'] === HTML5::EOF ||
+ ($token['type'] === HTML5::CHARACTR && isset($token['data']) &&
+ !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) {
+ /* This specification does not define how to handle this case. In
+ particular, user agents may ignore the entirety of this specification
+ altogether for such documents, and instead invoke special parse modes
+ with a greater emphasis on backwards compatibility. */
+
+ $this->phase = self::ROOT_PHASE;
+ return $this->rootElementPhase($token);
+
+ /* A DOCTYPE token marked as being correct */
+ } elseif(isset($token['error']) && !$token['error']) {
+ /* Append a DocumentType node to the Document node, with the name
+ attribute set to the name given in the DOCTYPE token (which will be
+ "HTML"), and the other attributes specific to DocumentType objects
+ set to null, empty lists, or the empty string as appropriate. */
+ $doctype = new DOMDocumentType(null, null, 'HTML');
+
+ /* Then, switch to the root element phase of the tree construction
+ stage. */
+ $this->phase = self::ROOT_PHASE;
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/',
+ $token['data'])) {
+ /* Append that character to the Document node. */
+ $text = $this->dom->createTextNode($token['data']);
+ $this->dom->appendChild($text);
+ }
+ }
+
+ private function rootElementPhase($token) {
+ /* After the initial phase, as each token is emitted from the tokenisation
+ stage, it must be processed as described in this section. */
+
+ /* A DOCTYPE token */
+ if($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the Document object with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->dom->appendChild($comment);
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append that character to the Document node. */
+ $text = $this->dom->createTextNode($token['data']);
+ $this->dom->appendChild($text);
+
+ /* A character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED
+ (FF), or U+0020 SPACE
+ A start tag token
+ An end tag token
+ An end-of-file token */
+ } elseif(($token['type'] === HTML5::CHARACTR &&
+ !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
+ $token['type'] === HTML5::STARTTAG ||
+ $token['type'] === HTML5::ENDTAG ||
+ $token['type'] === HTML5::EOF) {
+ /* Create an HTMLElement node with the tag name html, in the HTML
+ namespace. Append it to the Document object. Switch to the main
+ phase and reprocess the current token. */
+ $html = $this->dom->createElement('html');
+ $this->dom->appendChild($html);
+ $this->stack[] = $html;
+
+ $this->phase = self::MAIN_PHASE;
+ return $this->mainPhase($token);
+ }
+ }
+
+ private function mainPhase($token) {
+ /* Tokens in the main phase must be handled as follows: */
+
+ /* A DOCTYPE token */
+ if($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A start tag token with the tag name "html" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') {
+ /* If this start tag token was not the first start tag token, then
+ it is a parse error. */
+
+ /* For each attribute on the token, check to see if the attribute
+ is already present on the top element of the stack of open elements.
+ If it is not, add the attribute and its corresponding value to that
+ element. */
+ foreach($token['attr'] as $attr) {
+ if(!$this->stack[0]->hasAttribute($attr['name'])) {
+ $this->stack[0]->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+
+ /* An end-of-file token */
+ } elseif($token['type'] === HTML5::EOF) {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Anything else. */
+ } else {
+ /* Depends on the insertion mode: */
+ switch($this->mode) {
+ case self::BEFOR_HEAD: return $this->beforeHead($token); break;
+ case self::IN_HEAD: return $this->inHead($token); break;
+ case self::AFTER_HEAD: return $this->afterHead($token); break;
+ case self::IN_BODY: return $this->inBody($token); break;
+ case self::IN_TABLE: return $this->inTable($token); break;
+ case self::IN_CAPTION: return $this->inCaption($token); break;
+ case self::IN_CGROUP: return $this->inColumnGroup($token); break;
+ case self::IN_TBODY: return $this->inTableBody($token); break;
+ case self::IN_ROW: return $this->inRow($token); break;
+ case self::IN_CELL: return $this->inCell($token); break;
+ case self::IN_SELECT: return $this->inSelect($token); break;
+ case self::AFTER_BODY: return $this->afterBody($token); break;
+ case self::IN_FRAME: return $this->inFrameset($token); break;
+ case self::AFTR_FRAME: return $this->afterFrameset($token); break;
+ case self::END_PHASE: return $this->trailingEndPhase($token); break;
+ }
+ }
+ }
+
+ private function beforeHead($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token with the tag name "head" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') {
+ /* Create an element for the token, append the new element to the
+ current node and push it onto the stack of open elements. */
+ $element = $this->insertElement($token);
+
+ /* Set the head element pointer to this new element node. */
+ $this->head_pointer = $element;
+
+ /* Change the insertion mode to "in head". */
+ $this->mode = self::IN_HEAD;
+
+ /* A start tag token whose tag name is one of: "base", "link", "meta",
+ "script", "style", "title". Or an end tag with the tag name "html".
+ Or a character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE. Or any other start tag token */
+ } elseif($token['type'] === HTML5::STARTTAG ||
+ ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') ||
+ ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/',
+ $token['data']))) {
+ /* Act as if a start tag token with the tag name "head" and no
+ attributes had been seen, then reprocess the current token. */
+ $this->beforeHead(array(
+ 'name' => 'head',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inHead($token);
+
+ /* Any other end tag */
+ } elseif($token['type'] === HTML5::ENDTAG) {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function inHead($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE.
+
+ THIS DIFFERS FROM THE SPEC: If the current node is either a title, style
+ or script element, append the character to the current node regardless
+ of its content. */
+ if(($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || (
+ $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName,
+ array('title', 'style', 'script')))) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('title', 'style', 'script'))) {
+ array_pop($this->stack);
+ return HTML5::PCDATA;
+
+ /* A start tag with the tag name "title" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ } else {
+ $element = $this->insertElement($token);
+ }
+
+ /* Switch the tokeniser's content model flag to the RCDATA state. */
+ return HTML5::RCDATA;
+
+ /* A start tag with the tag name "style" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ } else {
+ $this->insertElement($token);
+ }
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+
+ /* A start tag with the tag name "script" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') {
+ /* Create an element for the token. */
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+
+ /* A start tag with the tag name "base", "link", or "meta" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('base', 'link', 'meta'))) {
+ /* Create an element for the token and append the new element to the
+ node pointed to by the head element pointer, or, if that is null
+ (innerHTML case), to the current node. */
+ if($this->head_pointer !== null) {
+ $element = $this->insertElement($token, false);
+ $this->head_pointer->appendChild($element);
+ array_pop($this->stack);
+
+ } else {
+ $this->insertElement($token);
+ }
+
+ /* An end tag with the tag name "head" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') {
+ /* If the current node is a head element, pop the current node off
+ the stack of open elements. */
+ if($this->head_pointer->isSameNode(end($this->stack))) {
+ array_pop($this->stack);
+
+ /* Otherwise, this is a parse error. */
+ } else {
+ // k
+ }
+
+ /* Change the insertion mode to "after head". */
+ $this->mode = self::AFTER_HEAD;
+
+ /* A start tag with the tag name "head" or an end tag except "html". */
+ } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') ||
+ ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* If the current node is a head element, act as if an end tag
+ token with the tag name "head" had been seen. */
+ if($this->head_pointer->isSameNode(end($this->stack))) {
+ $this->inHead(array(
+ 'name' => 'head',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Otherwise, change the insertion mode to "after head". */
+ } else {
+ $this->mode = self::AFTER_HEAD;
+ }
+
+ /* Then, reprocess the current token. */
+ return $this->afterHead($token);
+ }
+ }
+
+ private function afterHead($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data attribute
+ set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token with the tag name "body" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') {
+ /* Insert a body element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in body". */
+ $this->mode = self::IN_BODY;
+
+ /* A start tag token with the tag name "frameset" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') {
+ /* Insert a frameset element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in frameset". */
+ $this->mode = self::IN_FRAME;
+
+ /* A start tag token whose tag name is one of: "base", "link", "meta",
+ "script", "style", "title" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('base', 'link', 'meta', 'script', 'style', 'title'))) {
+ /* Parse error. Switch the insertion mode back to "in head" and
+ reprocess the token. */
+ $this->mode = self::IN_HEAD;
+ return $this->inHead($token);
+
+ /* Anything else */
+ } else {
+ /* Act as if a start tag token with the tag name "body" and no
+ attributes had been seen, and then reprocess the current token. */
+ $this->afterHead(array(
+ 'name' => 'body',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inBody($token);
+ }
+ }
+
+ private function inBody($token) {
+ /* Handle the token as follows: */
+
+ switch($token['type']) {
+ /* A character token */
+ case HTML5::CHARACTR:
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Append the token's character to the current node. */
+ $this->insertText($token['data']);
+ break;
+
+ /* A comment token */
+ case HTML5::COMMENT:
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+ break;
+
+ case HTML5::STARTTAG:
+ switch($token['name']) {
+ /* A start tag token whose tag name is one of: "script",
+ "style" */
+ case 'script': case 'style':
+ /* Process the token as if the insertion mode had been "in
+ head". */
+ return $this->inHead($token);
+ break;
+
+ /* A start tag token whose tag name is one of: "base", "link",
+ "meta", "title" */
+ case 'base': case 'link': case 'meta': case 'title':
+ /* Parse error. Process the token as if the insertion mode
+ had been "in head". */
+ return $this->inHead($token);
+ break;
+
+ /* A start tag token with the tag name "body" */
+ case 'body':
+ /* Parse error. If the second element on the stack of open
+ elements is not a body element, or, if the stack of open
+ elements has only one node on it, then ignore the token.
+ (innerHTML case) */
+ if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') {
+ // Ignore
+
+ /* Otherwise, for each attribute on the token, check to see
+ if the attribute is already present on the body element (the
+ second element) on the stack of open elements. If it is not,
+ add the attribute and its corresponding value to that
+ element. */
+ } else {
+ foreach($token['attr'] as $attr) {
+ if(!$this->stack[1]->hasAttribute($attr['name'])) {
+ $this->stack[1]->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+ }
+ break;
+
+ /* A start tag whose tag name is one of: "address",
+ "blockquote", "center", "dir", "div", "dl", "fieldset",
+ "listing", "menu", "ol", "p", "ul" */
+ case 'address': case 'blockquote': case 'center': case 'dir':
+ case 'div': case 'dl': case 'fieldset': case 'listing':
+ case 'menu': case 'ol': case 'p': case 'ul':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag whose tag name is "form" */
+ case 'form':
+ /* If the form element pointer is not null, ignore the
+ token with a parse error. */
+ if($this->form_pointer !== null) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* If the stack of open elements has a p element in
+ scope, then act as if an end tag with the tag name p
+ had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token, and set the
+ form element pointer to point to the element created. */
+ $element = $this->insertElement($token);
+ $this->form_pointer = $element;
+ }
+ break;
+
+ /* A start tag whose tag name is "li", "dd" or "dt" */
+ case 'li': case 'dd': case 'dt':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ $stack_length = count($this->stack) - 1;
+
+ for($n = $stack_length; 0 <= $n; $n--) {
+ /* 1. Initialise node to be the current node (the
+ bottommost node of the stack). */
+ $stop = false;
+ $node = $this->stack[$n];
+ $cat = $this->getElementCategory($node->tagName);
+
+ /* 2. If node is an li, dd or dt element, then pop all
+ the nodes from the current node up to node, including
+ node, then stop this algorithm. */
+ if($token['name'] === $node->tagName || ($token['name'] !== 'li'
+ && ($node->tagName === 'dd' || $node->tagName === 'dt'))) {
+ for($x = $stack_length; $x >= $n ; $x--) {
+ array_pop($this->stack);
+ }
+
+ break;
+ }
+
+ /* 3. If node is not in the formatting category, and is
+ not in the phrasing category, and is not an address or
+ div element, then stop this algorithm. */
+ if($cat !== self::FORMATTING && $cat !== self::PHRASING &&
+ $node->tagName !== 'address' && $node->tagName !== 'div') {
+ break;
+ }
+ }
+
+ /* Finally, insert an HTML element with the same tag
+ name as the token's. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag token whose tag name is "plaintext" */
+ case 'plaintext':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been
+ seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ return HTML5::PLAINTEXT;
+ break;
+
+ /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4",
+ "h5", "h6" */
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* If the stack of open elements has in scope an element whose
+ tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
+ this is a parse error; pop elements from the stack until an
+ element with one of those tag names has been popped from the
+ stack. */
+ while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) {
+ array_pop($this->stack);
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+ break;
+
+ /* A start tag whose tag name is "a" */
+ case 'a':
+ /* If the list of active formatting elements contains
+ an element whose tag name is "a" between the end of the
+ list and the last marker on the list (or the start of
+ the list if there is no marker on the list), then this
+ is a parse error; act as if an end tag with the tag name
+ "a" had been seen, then remove that element from the list
+ of active formatting elements and the stack of open
+ elements if the end tag didn't already remove it (it
+ might not have if the element is not in table scope). */
+ $leng = count($this->a_formatting);
+
+ for($n = $leng - 1; $n >= 0; $n--) {
+ if($this->a_formatting[$n] === self::MARKER) {
+ break;
+
+ } elseif($this->a_formatting[$n]->nodeName === 'a') {
+ $this->emitToken(array(
+ 'name' => 'a',
+ 'type' => HTML5::ENDTAG
+ ));
+ break;
+ }
+ }
+
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $el = $this->insertElement($token);
+
+ /* Add that element to the list of active formatting
+ elements. */
+ $this->a_formatting[] = $el;
+ break;
+
+ /* A start tag whose tag name is one of: "b", "big", "em", "font",
+ "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
+ case 'b': case 'big': case 'em': case 'font': case 'i':
+ case 'nobr': case 's': case 'small': case 'strike':
+ case 'strong': case 'tt': case 'u':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $el = $this->insertElement($token);
+
+ /* Add that element to the list of active formatting
+ elements. */
+ $this->a_formatting[] = $el;
+ break;
+
+ /* A start tag token whose tag name is "button" */
+ case 'button':
+ /* If the stack of open elements has a button element in scope,
+ then this is a parse error; act as if an end tag with the tag
+ name "button" had been seen, then reprocess the token. (We don't
+ do that. Unnecessary.) */
+ if($this->elementInScope('button')) {
+ $this->inBody(array(
+ 'name' => 'button',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+ break;
+
+ /* A start tag token whose tag name is one of: "marquee", "object" */
+ case 'marquee': case 'object':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+ break;
+
+ /* A start tag token whose tag name is "xmp" */
+ case 'xmp':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Switch the content model flag to the CDATA state. */
+ return HTML5::CDATA;
+ break;
+
+ /* A start tag whose tag name is "table" */
+ case 'table':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in table". */
+ $this->mode = self::IN_TABLE;
+ break;
+
+ /* A start tag whose tag name is one of: "area", "basefont",
+ "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */
+ case 'area': case 'basefont': case 'bgsound': case 'br':
+ case 'embed': case 'img': case 'param': case 'spacer':
+ case 'wbr':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "hr" */
+ case 'hr':
+ /* If the stack of open elements has a p element in scope,
+ then act as if an end tag with the tag name p had been seen. */
+ if($this->elementInScope('p')) {
+ $this->emitToken(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "image" */
+ case 'image':
+ /* Parse error. Change the token's tag name to "img" and
+ reprocess it. (Don't ask.) */
+ $token['name'] = 'img';
+ return $this->inBody($token);
+ break;
+
+ /* A start tag whose tag name is "input" */
+ case 'input':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an input element for the token. */
+ $element = $this->insertElement($token, false);
+
+ /* If the form element pointer is not null, then associate the
+ input element with the form element pointed to by the form
+ element pointer. */
+ $this->form_pointer !== null
+ ? $this->form_pointer->appendChild($element)
+ : end($this->stack)->appendChild($element);
+
+ /* Pop that input element off the stack of open elements. */
+ array_pop($this->stack);
+ break;
+
+ /* A start tag whose tag name is "isindex" */
+ case 'isindex':
+ /* Parse error. */
+ // w/e
+
+ /* If the form element pointer is not null,
+ then ignore the token. */
+ if($this->form_pointer === null) {
+ /* Act as if a start tag token with the tag name "form" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'body',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a start tag token with the tag name "hr" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'hr',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a start tag token with the tag name "p" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'p',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a start tag token with the tag name "label"
+ had been seen. */
+ $this->inBody(array(
+ 'name' => 'label',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ /* Act as if a stream of character tokens had been seen. */
+ $this->insertText('This is a searchable index. '.
+ 'Insert your search keywords here: ');
+
+ /* Act as if a start tag token with the tag name "input"
+ had been seen, with all the attributes from the "isindex"
+ token, except with the "name" attribute set to the value
+ "isindex" (ignoring any explicit "name" attribute). */
+ $attr = $token['attr'];
+ $attr[] = array('name' => 'name', 'value' => 'isindex');
+
+ $this->inBody(array(
+ 'name' => 'input',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => $attr
+ ));
+
+ /* Act as if a stream of character tokens had been seen
+ (see below for what they should say). */
+ $this->insertText('This is a searchable index. '.
+ 'Insert your search keywords here: ');
+
+ /* Act as if an end tag token with the tag name "label"
+ had been seen. */
+ $this->inBody(array(
+ 'name' => 'label',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Act as if an end tag token with the tag name "p" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'p',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Act as if a start tag token with the tag name "hr" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'hr',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* Act as if an end tag token with the tag name "form" had
+ been seen. */
+ $this->inBody(array(
+ 'name' => 'form',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+ break;
+
+ /* A start tag whose tag name is "textarea" */
+ case 'textarea':
+ $this->insertElement($token);
+
+ /* Switch the tokeniser's content model flag to the
+ RCDATA state. */
+ return HTML5::RCDATA;
+ break;
+
+ /* A start tag whose tag name is one of: "iframe", "noembed",
+ "noframes" */
+ case 'iframe': case 'noembed': case 'noframes':
+ $this->insertElement($token);
+
+ /* Switch the tokeniser's content model flag to the CDATA state. */
+ return HTML5::CDATA;
+ break;
+
+ /* A start tag whose tag name is "select" */
+ case 'select':
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Change the insertion mode to "in select". */
+ $this->mode = self::IN_SELECT;
+ break;
+
+ /* A start or end tag whose tag name is one of: "caption", "col",
+ "colgroup", "frame", "frameset", "head", "option", "optgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr". */
+ case 'caption': case 'col': case 'colgroup': case 'frame':
+ case 'frameset': case 'head': case 'option': case 'optgroup':
+ case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead':
+ case 'tr':
+ // Parse error. Ignore the token.
+ break;
+
+ /* A start or end tag whose tag name is one of: "event-source",
+ "section", "nav", "article", "aside", "header", "footer",
+ "datagrid", "command" */
+ case 'event-source': case 'section': case 'nav': case 'article':
+ case 'aside': case 'header': case 'footer': case 'datagrid':
+ case 'command':
+ // Work in progress!
+ break;
+
+ /* A start tag token not covered by the previous entries */
+ default:
+ /* Reconstruct the active formatting elements, if any. */
+ $this->reconstructActiveFormattingElements();
+
+ $this->insertElement($token, true, true);
+ break;
+ }
+ break;
+
+ case HTML5::ENDTAG:
+ switch($token['name']) {
+ /* An end tag with the tag name "body" */
+ case 'body':
+ /* If the second element in the stack of open elements is
+ not a body element, this is a parse error. Ignore the token.
+ (innerHTML case) */
+ if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') {
+ // Ignore.
+
+ /* If the current node is not the body element, then this
+ is a parse error. */
+ } elseif(end($this->stack)->nodeName !== 'body') {
+ // Parse error.
+ }
+
+ /* Change the insertion mode to "after body". */
+ $this->mode = self::AFTER_BODY;
+ break;
+
+ /* An end tag with the tag name "html" */
+ case 'html':
+ /* Act as if an end tag with tag name "body" had been seen,
+ then, if that token wasn't ignored, reprocess the current
+ token. */
+ $this->inBody(array(
+ 'name' => 'body',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->afterBody($token);
+ break;
+
+ /* An end tag whose tag name is one of: "address", "blockquote",
+ "center", "dir", "div", "dl", "fieldset", "listing", "menu",
+ "ol", "pre", "ul" */
+ case 'address': case 'blockquote': case 'center': case 'dir':
+ case 'div': case 'dl': case 'fieldset': case 'listing':
+ case 'menu': case 'ol': case 'pre': case 'ul':
+ /* If the stack of open elements has an element in scope
+ with the same tag name as that of the token, then generate
+ implied end tags. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with
+ the same tag name as that of the token, then this
+ is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in
+ scope with the same tag name as that of the token,
+ then pop elements from this stack until an element
+ with that tag name has been popped from the stack. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is "form" */
+ case 'form':
+ /* If the stack of open elements has an element in scope
+ with the same tag name as that of the token, then generate
+ implied end tags. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ }
+
+ if(end($this->stack)->nodeName !== $token['name']) {
+ /* Now, if the current node is not an element with the
+ same tag name as that of the token, then this is a parse
+ error. */
+ // w/e
+
+ } else {
+ /* Otherwise, if the current node is an element with
+ the same tag name as that of the token pop that element
+ from the stack. */
+ array_pop($this->stack);
+ }
+
+ /* In any case, set the form element pointer to null. */
+ $this->form_pointer = null;
+ break;
+
+ /* An end tag whose tag name is "p" */
+ case 'p':
+ /* If the stack of open elements has a p element in scope,
+ then generate implied end tags, except for p elements. */
+ if($this->elementInScope('p')) {
+ $this->generateImpliedEndTags(array('p'));
+
+ /* If the current node is not a p element, then this is
+ a parse error. */
+ // k
+
+ /* If the stack of open elements has a p element in
+ scope, then pop elements from this stack until the stack
+ no longer has a p element in scope. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->elementInScope('p')) {
+ array_pop($this->stack);
+
+ } else {
+ break;
+ }
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is "dd", "dt", or "li" */
+ case 'dd': case 'dt': case 'li':
+ /* If the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then
+ generate implied end tags, except for elements with the
+ same tag name as the token. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags(array($token['name']));
+
+ /* If the current node is not an element with the same
+ tag name as the token, then this is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then
+ pop elements from this stack until an element with that
+ tag name has been popped from the stack. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4",
+ "h5", "h6" */
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
+ $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
+
+ /* If the stack of open elements has in scope an element whose
+ tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then
+ generate implied end tags. */
+ if($this->elementInScope($elements)) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with the same
+ tag name as that of the token, then this is a parse error. */
+ // w/e
+
+ /* If the stack of open elements has in scope an element
+ whose tag name is one of "h1", "h2", "h3", "h4", "h5", or
+ "h6", then pop elements from the stack until an element
+ with one of those tag names has been popped from the stack. */
+ while($this->elementInScope($elements)) {
+ array_pop($this->stack);
+ }
+ }
+ break;
+
+ /* An end tag whose tag name is one of: "a", "b", "big", "em",
+ "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */
+ case 'a': case 'b': case 'big': case 'em': case 'font':
+ case 'i': case 'nobr': case 's': case 'small': case 'strike':
+ case 'strong': case 'tt': case 'u':
+ /* 1. Let the formatting element be the last element in
+ the list of active formatting elements that:
+ * is between the end of the list and the last scope
+ marker in the list, if any, or the start of the list
+ otherwise, and
+ * has the same tag name as the token.
+ */
+ while(true) {
+ for($a = count($this->a_formatting) - 1; $a >= 0; $a--) {
+ if($this->a_formatting[$a] === self::MARKER) {
+ break;
+
+ } elseif($this->a_formatting[$a]->tagName === $token['name']) {
+ $formatting_element = $this->a_formatting[$a];
+ $in_stack = in_array($formatting_element, $this->stack, true);
+ $fe_af_pos = $a;
+ break;
+ }
+ }
+
+ /* If there is no such node, or, if that node is
+ also in the stack of open elements but the element
+ is not in scope, then this is a parse error. Abort
+ these steps. The token is ignored. */
+ if(!isset($formatting_element) || ($in_stack &&
+ !$this->elementInScope($token['name']))) {
+ break;
+
+ /* Otherwise, if there is such a node, but that node
+ is not in the stack of open elements, then this is a
+ parse error; remove the element from the list, and
+ abort these steps. */
+ } elseif(isset($formatting_element) && !$in_stack) {
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+ break;
+ }
+
+ /* 2. Let the furthest block be the topmost node in the
+ stack of open elements that is lower in the stack
+ than the formatting element, and is not an element in
+ the phrasing or formatting categories. There might
+ not be one. */
+ $fe_s_pos = array_search($formatting_element, $this->stack, true);
+ $length = count($this->stack);
+
+ for($s = $fe_s_pos + 1; $s < $length; $s++) {
+ $category = $this->getElementCategory($this->stack[$s]->nodeName);
+
+ if($category !== self::PHRASING && $category !== self::FORMATTING) {
+ $furthest_block = $this->stack[$s];
+ }
+ }
+
+ /* 3. If there is no furthest block, then the UA must
+ skip the subsequent steps and instead just pop all
+ the nodes from the bottom of the stack of open
+ elements, from the current node up to the formatting
+ element, and remove the formatting element from the
+ list of active formatting elements. */
+ if(!isset($furthest_block)) {
+ for($n = $length - 1; $n >= $fe_s_pos; $n--) {
+ array_pop($this->stack);
+ }
+
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+ break;
+ }
+
+ /* 4. Let the common ancestor be the element
+ immediately above the formatting element in the stack
+ of open elements. */
+ $common_ancestor = $this->stack[$fe_s_pos - 1];
+
+ /* 5. If the furthest block has a parent node, then
+ remove the furthest block from its parent node. */
+ if($furthest_block->parentNode !== null) {
+ $furthest_block->parentNode->removeChild($furthest_block);
+ }
+
+ /* 6. Let a bookmark note the position of the
+ formatting element in the list of active formatting
+ elements relative to the elements on either side
+ of it in the list. */
+ $bookmark = $fe_af_pos;
+
+ /* 7. Let node and last node be the furthest block.
+ Follow these steps: */
+ $node = $furthest_block;
+ $last_node = $furthest_block;
+
+ while(true) {
+ for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) {
+ /* 7.1 Let node be the element immediately
+ prior to node in the stack of open elements. */
+ $node = $this->stack[$n];
+
+ /* 7.2 If node is not in the list of active
+ formatting elements, then remove node from
+ the stack of open elements and then go back
+ to step 1. */
+ if(!in_array($node, $this->a_formatting, true)) {
+ unset($this->stack[$n]);
+ $this->stack = array_merge($this->stack);
+
+ } else {
+ break;
+ }
+ }
+
+ /* 7.3 Otherwise, if node is the formatting
+ element, then go to the next step in the overall
+ algorithm. */
+ if($node === $formatting_element) {
+ break;
+
+ /* 7.4 Otherwise, if last node is the furthest
+ block, then move the aforementioned bookmark to
+ be immediately after the node in the list of
+ active formatting elements. */
+ } elseif($last_node === $furthest_block) {
+ $bookmark = array_search($node, $this->a_formatting, true) + 1;
+ }
+
+ /* 7.5 If node has any children, perform a
+ shallow clone of node, replace the entry for
+ node in the list of active formatting elements
+ with an entry for the clone, replace the entry
+ for node in the stack of open elements with an
+ entry for the clone, and let node be the clone. */
+ if($node->hasChildNodes()) {
+ $clone = $node->cloneNode();
+ $s_pos = array_search($node, $this->stack, true);
+ $a_pos = array_search($node, $this->a_formatting, true);
+
+ $this->stack[$s_pos] = $clone;
+ $this->a_formatting[$a_pos] = $clone;
+ $node = $clone;
+ }
+
+ /* 7.6 Insert last node into node, first removing
+ it from its previous parent node if any. */
+ if($last_node->parentNode !== null) {
+ $last_node->parentNode->removeChild($last_node);
+ }
+
+ $node->appendChild($last_node);
+
+ /* 7.7 Let last node be node. */
+ $last_node = $node;
+ }
+
+ /* 8. Insert whatever last node ended up being in
+ the previous step into the common ancestor node,
+ first removing it from its previous parent node if
+ any. */
+ if($last_node->parentNode !== null) {
+ $last_node->parentNode->removeChild($last_node);
+ }
+
+ $common_ancestor->appendChild($last_node);
+
+ /* 9. Perform a shallow clone of the formatting
+ element. */
+ $clone = $formatting_element->cloneNode();
+
+ /* 10. Take all of the child nodes of the furthest
+ block and append them to the clone created in the
+ last step. */
+ while($furthest_block->hasChildNodes()) {
+ $child = $furthest_block->firstChild;
+ $furthest_block->removeChild($child);
+ $clone->appendChild($child);
+ }
+
+ /* 11. Append that clone to the furthest block. */
+ $furthest_block->appendChild($clone);
+
+ /* 12. Remove the formatting element from the list
+ of active formatting elements, and insert the clone
+ into the list of active formatting elements at the
+ position of the aforementioned bookmark. */
+ $fe_af_pos = array_search($formatting_element, $this->a_formatting, true);
+ unset($this->a_formatting[$fe_af_pos]);
+ $this->a_formatting = array_merge($this->a_formatting);
+
+ $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1);
+ $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting));
+ $this->a_formatting = array_merge($af_part1, array($clone), $af_part2);
+
+ /* 13. Remove the formatting element from the stack
+ of open elements, and insert the clone into the stack
+ of open elements immediately after (i.e. in a more
+ deeply nested position than) the position of the
+ furthest block in that stack. */
+ $fe_s_pos = array_search($formatting_element, $this->stack, true);
+ $fb_s_pos = array_search($furthest_block, $this->stack, true);
+ unset($this->stack[$fe_s_pos]);
+
+ $s_part1 = array_slice($this->stack, 0, $fb_s_pos);
+ $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack));
+ $this->stack = array_merge($s_part1, array($clone), $s_part2);
+
+ /* 14. Jump back to step 1 in this series of steps. */
+ unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block);
+ }
+ break;
+
+ /* An end tag token whose tag name is one of: "button",
+ "marquee", "object" */
+ case 'button': case 'marquee': case 'object':
+ /* If the stack of open elements has an element in scope whose
+ tag name matches the tag name of the token, then generate implied
+ tags. */
+ if($this->elementInScope($token['name'])) {
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not an element with the same
+ tag name as the token, then this is a parse error. */
+ // k
+
+ /* Now, if the stack of open elements has an element in scope
+ whose tag name matches the tag name of the token, then pop
+ elements from the stack until that element has been popped from
+ the stack, and clear the list of active formatting elements up
+ to the last marker. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === $token['name']) {
+ $n = -1;
+ }
+
+ array_pop($this->stack);
+ }
+
+ $marker = end(array_keys($this->a_formatting, self::MARKER, true));
+
+ for($n = count($this->a_formatting) - 1; $n > $marker; $n--) {
+ array_pop($this->a_formatting);
+ }
+ }
+ break;
+
+ /* Or an end tag whose tag name is one of: "area", "basefont",
+ "bgsound", "br", "embed", "hr", "iframe", "image", "img",
+ "input", "isindex", "noembed", "noframes", "param", "select",
+ "spacer", "table", "textarea", "wbr" */
+ case 'area': case 'basefont': case 'bgsound': case 'br':
+ case 'embed': case 'hr': case 'iframe': case 'image':
+ case 'img': case 'input': case 'isindex': case 'noembed':
+ case 'noframes': case 'param': case 'select': case 'spacer':
+ case 'table': case 'textarea': case 'wbr':
+ // Parse error. Ignore the token.
+ break;
+
+ /* An end tag token not covered by the previous entries */
+ default:
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ /* Initialise node to be the current node (the bottommost
+ node of the stack). */
+ $node = end($this->stack);
+
+ /* If node has the same tag name as the end tag token,
+ then: */
+ if($token['name'] === $node->nodeName) {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* If the tag name of the end tag token does not
+ match the tag name of the current node, this is a
+ parse error. */
+ // k
+
+ /* Pop all the nodes from the current node up to
+ node, including node, then stop this algorithm. */
+ for($x = count($this->stack) - $n; $x >= $n; $x--) {
+ array_pop($this->stack);
+ }
+
+ } else {
+ $category = $this->getElementCategory($node);
+
+ if($category !== self::SPECIAL && $category !== self::SCOPING) {
+ /* Otherwise, if node is in neither the formatting
+ category nor the phrasing category, then this is a
+ parse error. Stop this algorithm. The end tag token
+ is ignored. */
+ return false;
+ }
+ }
+ }
+ break;
+ }
+ break;
+ }
+ }
+
+ private function inTable($token) {
+ $clear = array('html', 'table');
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $text = $this->dom->createTextNode($token['data']);
+ end($this->stack)->appendChild($text);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ end($this->stack)->appendChild($comment);
+
+ /* A start tag whose tag name is "caption" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'caption') {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert a marker at the end of the list of active
+ formatting elements. */
+ $this->a_formatting[] = self::MARKER;
+
+ /* Insert an HTML element for the token, then switch the
+ insertion mode to "in caption". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CAPTION;
+
+ /* A start tag whose tag name is "colgroup" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'colgroup') {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the
+ insertion mode to "in column group". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CGROUP;
+
+ /* A start tag whose tag name is "col" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'col') {
+ $this->inTable(array(
+ 'name' => 'colgroup',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ $this->inColumnGroup($token);
+
+ /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('tbody', 'tfoot', 'thead'))) {
+ /* Clear the stack back to a table context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the insertion
+ mode to "in table body". */
+ $this->insertElement($token);
+ $this->mode = self::IN_TBODY;
+
+ /* A start tag whose tag name is one of: "td", "th", "tr" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ in_array($token['name'], array('td', 'th', 'tr'))) {
+ /* Act as if a start tag token with the tag name "tbody" had been
+ seen, then reprocess the current token. */
+ $this->inTable(array(
+ 'name' => 'tbody',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inTableBody($token);
+
+ /* A start tag whose tag name is "table" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'table') {
+ /* Parse error. Act as if an end tag token with the tag name "table"
+ had been seen, then, if that token wasn't ignored, reprocess the
+ current token. */
+ $this->inTable(array(
+ 'name' => 'table',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->mainPhase($token);
+
+ /* An end tag whose tag name is "table" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'table') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ return false;
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not a table element, then this
+ is a parse error. */
+ // w/e
+
+ /* Pop elements from this stack until a table element has been
+ popped from the stack. */
+ while(true) {
+ $current = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($current === 'table') {
+ break;
+ }
+ }
+
+ /* Reset the insertion mode appropriately. */
+ $this->resetInsertionMode();
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td',
+ 'tfoot', 'th', 'thead', 'tr'))) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* Parse error. Process the token as if the insertion mode was "in
+ body", with the following exception: */
+
+ /* If the current node is a table, tbody, tfoot, thead, or tr
+ element, then, whenever a node would be inserted into the current
+ node, it must instead be inserted into the foster parent element. */
+ if(in_array(end($this->stack)->nodeName,
+ array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
+ /* The foster parent element is the parent element of the last
+ table element in the stack of open elements, if there is a
+ table element and it has such a parent element. If there is no
+ table element in the stack of open elements (innerHTML case),
+ then the foster parent element is the first element in the
+ stack of open elements (the html element). Otherwise, if there
+ is a table element in the stack of open elements, but the last
+ table element in the stack of open elements has no parent, or
+ its parent node is not an element, then the foster parent
+ element is the element before the last table element in the
+ stack of open elements. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === 'table') {
+ $table = $this->stack[$n];
+ break;
+ }
+ }
+
+ if(isset($table) && $table->parentNode !== null) {
+ $this->foster_parent = $table->parentNode;
+
+ } elseif(!isset($table)) {
+ $this->foster_parent = $this->stack[0];
+
+ } elseif(isset($table) && ($table->parentNode === null ||
+ $table->parentNode->nodeType !== XML_ELEMENT_NODE)) {
+ $this->foster_parent = $this->stack[$n - 1];
+ }
+ }
+
+ $this->inBody($token);
+ }
+ }
+
+ private function inCaption($token) {
+ /* An end tag whose tag name is "caption" */
+ if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags. */
+ $this->generateImpliedEndTags();
+
+ /* Now, if the current node is not a caption element, then this
+ is a parse error. */
+ // w/e
+
+ /* Pop elements from this stack until a caption element has
+ been popped from the stack. */
+ while(true) {
+ $node = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($node === 'caption') {
+ break;
+ }
+ }
+
+ /* Clear the list of active formatting elements up to the last
+ marker. */
+ $this->clearTheActiveFormattingElementsUpToTheLastMarker();
+
+ /* Switch the insertion mode to "in table". */
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag
+ name is "table" */
+ } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'table')) {
+ /* Parse error. Act as if an end tag with the tag name "caption"
+ had been seen, then, if that token wasn't ignored, reprocess the
+ current token. */
+ $this->inCaption(array(
+ 'name' => 'caption',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inTable($token);
+
+ /* An end tag whose tag name is one of: "body", "col", "colgroup",
+ "html", "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th',
+ 'thead', 'tr'))) {
+ // Parse error. Ignore the token.
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in body". */
+ $this->inBody($token);
+ }
+ }
+
+ private function inColumnGroup($token) {
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $text = $this->dom->createTextNode($token['data']);
+ end($this->stack)->appendChild($text);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ end($this->stack)->appendChild($comment);
+
+ /* A start tag whose tag name is "col" */
+ } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') {
+ /* Insert a col element for the token. Immediately pop the current
+ node off the stack of open elements. */
+ $this->insertElement($token);
+ array_pop($this->stack);
+
+ /* An end tag whose tag name is "colgroup" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'colgroup') {
+ /* If the current node is the root html element, then this is a
+ parse error, ignore the token. (innerHTML case) */
+ if(end($this->stack)->nodeName === 'html') {
+ // Ignore
+
+ /* Otherwise, pop the current node (which will be a colgroup
+ element) from the stack of open elements. Switch the insertion
+ mode to "in table". */
+ } else {
+ array_pop($this->stack);
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* An end tag whose tag name is "col" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Act as if an end tag with the tag name "colgroup" had been seen,
+ and then, if that token wasn't ignored, reprocess the current token. */
+ $this->inColumnGroup(array(
+ 'name' => 'colgroup',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inTable($token);
+ }
+ }
+
+ private function inTableBody($token) {
+ $clear = array('tbody', 'tfoot', 'thead', 'html');
+
+ /* A start tag whose tag name is "tr" */
+ if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert a tr element for the token, then switch the insertion
+ mode to "in row". */
+ $this->insertElement($token);
+ $this->mode = self::IN_ROW;
+
+ /* A start tag whose tag name is one of: "th", "td" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ ($token['name'] === 'th' || $token['name'] === 'td')) {
+ /* Parse error. Act as if a start tag with the tag name "tr" had
+ been seen, then reprocess the current token. */
+ $this->inTableBody(array(
+ 'name' => 'tr',
+ 'type' => HTML5::STARTTAG,
+ 'attr' => array()
+ ));
+
+ return $this->inRow($token);
+
+ /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Pop the current node from the stack of open elements. Switch
+ the insertion mode to "in table". */
+ array_pop($this->stack);
+ $this->mode = self::IN_TABLE;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */
+ } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) ||
+ ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) {
+ /* If the stack of open elements does not have a tbody, thead, or
+ tfoot element in table scope, this is a parse error. Ignore the
+ token. (innerHTML case) */
+ if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table body context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Act as if an end tag with the same tag name as the current
+ node ("tbody", "tfoot", or "thead") had been seen, then
+ reprocess the current token. */
+ $this->inTableBody(array(
+ 'name' => end($this->stack)->nodeName,
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->mainPhase($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "td", "th", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in table". */
+ $this->inTable($token);
+ }
+ }
+
+ private function inRow($token) {
+ $clear = array('tr', 'html');
+
+ /* A start tag whose tag name is one of: "th", "td" */
+ if($token['type'] === HTML5::STARTTAG &&
+ ($token['name'] === 'th' || $token['name'] === 'td')) {
+ /* Clear the stack back to a table row context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Insert an HTML element for the token, then switch the insertion
+ mode to "in cell". */
+ $this->insertElement($token);
+ $this->mode = self::IN_CELL;
+
+ /* Insert a marker at the end of the list of active formatting
+ elements. */
+ $this->a_formatting[] = self::MARKER;
+
+ /* An end tag whose tag name is "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Clear the stack back to a table row context. */
+ $this->clearStackToTableContext($clear);
+
+ /* Pop the current node (which will be a tr element) from the
+ stack of open elements. Switch the insertion mode to "in table
+ body". */
+ array_pop($this->stack);
+ $this->mode = self::IN_TBODY;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) {
+ /* Act as if an end tag with the tag name "tr" had been seen, then,
+ if that token wasn't ignored, reprocess the current token. */
+ $this->inRow(array(
+ 'name' => 'tr',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inCell($token);
+
+ /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ in_array($token['name'], array('tbody', 'tfoot', 'thead'))) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Otherwise, act as if an end tag with the tag name "tr" had
+ been seen, then reprocess the current token. */
+ $this->inRow(array(
+ 'name' => 'tr',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ return $this->inCell($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html", "td", "th" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) {
+ /* Parse error. Ignore the token. */
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in table". */
+ $this->inTable($token);
+ }
+ }
+
+ private function inCell($token) {
+ /* An end tag whose tag name is one of: "td", "th" */
+ if($token['type'] === HTML5::ENDTAG &&
+ ($token['name'] === 'td' || $token['name'] === 'th')) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as that of the token, then this is a
+ parse error and the token must be ignored. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise: */
+ } else {
+ /* Generate implied end tags, except for elements with the same
+ tag name as the token. */
+ $this->generateImpliedEndTags(array($token['name']));
+
+ /* Now, if the current node is not an element with the same tag
+ name as the token, then this is a parse error. */
+ // k
+
+ /* Pop elements from this stack until an element with the same
+ tag name as the token has been popped from the stack. */
+ while(true) {
+ $node = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($node === $token['name']) {
+ break;
+ }
+ }
+
+ /* Clear the list of active formatting elements up to the last
+ marker. */
+ $this->clearTheActiveFormattingElementsUpToTheLastMarker();
+
+ /* Switch the insertion mode to "in row". (The current node
+ will be a tr element at this point.) */
+ $this->mode = self::IN_ROW;
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'tr'))) {
+ /* If the stack of open elements does not have a td or th element
+ in table scope, then this is a parse error; ignore the token.
+ (innerHTML case) */
+ if(!$this->elementInScope(array('td', 'th'), true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* A start tag whose tag name is one of: "caption", "col", "colgroup",
+ "tbody", "td", "tfoot", "th", "thead", "tr" */
+ } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'],
+ array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th',
+ 'thead', 'tr'))) {
+ /* If the stack of open elements does not have a td or th element
+ in table scope, then this is a parse error; ignore the token.
+ (innerHTML case) */
+ if(!$this->elementInScope(array('td', 'th'), true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* An end tag whose tag name is one of: "body", "caption", "col",
+ "colgroup", "html" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('body', 'caption', 'col', 'colgroup', 'html'))) {
+ /* Parse error. Ignore the token. */
+
+ /* An end tag whose tag name is one of: "table", "tbody", "tfoot",
+ "thead", "tr" */
+ } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'],
+ array('table', 'tbody', 'tfoot', 'thead', 'tr'))) {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as that of the token (which can only
+ happen for "tbody", "tfoot" and "thead", or, in the innerHTML case),
+ then this is a parse error and the token must be ignored. */
+ if(!$this->elementInScope($token['name'], true)) {
+ // Ignore.
+
+ /* Otherwise, close the cell (see below) and reprocess the current
+ token. */
+ } else {
+ $this->closeCell();
+ return $this->inRow($token);
+ }
+
+ /* Anything else */
+ } else {
+ /* Process the token as if the insertion mode was "in body". */
+ $this->inBody($token);
+ }
+ }
+
+ private function inSelect($token) {
+ /* Handle the token as follows: */
+
+ /* A character token */
+ if($token['type'] === HTML5::CHARACTR) {
+ /* Append the token's character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag token whose tag name is "option" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'option') {
+ /* If the current node is an option element, act as if an end tag
+ with the tag name "option" had been seen. */
+ if(end($this->stack)->nodeName === 'option') {
+ $this->inSelect(array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* A start tag token whose tag name is "optgroup" */
+ } elseif($token['type'] === HTML5::STARTTAG &&
+ $token['name'] === 'optgroup') {
+ /* If the current node is an option element, act as if an end tag
+ with the tag name "option" had been seen. */
+ if(end($this->stack)->nodeName === 'option') {
+ $this->inSelect(array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* If the current node is an optgroup element, act as if an end tag
+ with the tag name "optgroup" had been seen. */
+ if(end($this->stack)->nodeName === 'optgroup') {
+ $this->inSelect(array(
+ 'name' => 'optgroup',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* An end tag token whose tag name is "optgroup" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'optgroup') {
+ /* First, if the current node is an option element, and the node
+ immediately before it in the stack of open elements is an optgroup
+ element, then act as if an end tag with the tag name "option" had
+ been seen. */
+ $elements_in_stack = count($this->stack);
+
+ if($this->stack[$elements_in_stack - 1]->nodeName === 'option' &&
+ $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') {
+ $this->inSelect(array(
+ 'name' => 'option',
+ 'type' => HTML5::ENDTAG
+ ));
+ }
+
+ /* If the current node is an optgroup element, then pop that node
+ from the stack of open elements. Otherwise, this is a parse error,
+ ignore the token. */
+ if($this->stack[$elements_in_stack - 1] === 'optgroup') {
+ array_pop($this->stack);
+ }
+
+ /* An end tag token whose tag name is "option" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'option') {
+ /* If the current node is an option element, then pop that node
+ from the stack of open elements. Otherwise, this is a parse error,
+ ignore the token. */
+ if(end($this->stack)->nodeName === 'option') {
+ array_pop($this->stack);
+ }
+
+ /* An end tag whose tag name is "select" */
+ } elseif($token['type'] === HTML5::ENDTAG &&
+ $token['name'] === 'select') {
+ /* If the stack of open elements does not have an element in table
+ scope with the same tag name as the token, this is a parse error.
+ Ignore the token. (innerHTML case) */
+ if(!$this->elementInScope($token['name'], true)) {
+ // w/e
+
+ /* Otherwise: */
+ } else {
+ /* Pop elements from the stack of open elements until a select
+ element has been popped from the stack. */
+ while(true) {
+ $current = end($this->stack)->nodeName;
+ array_pop($this->stack);
+
+ if($current === 'select') {
+ break;
+ }
+ }
+
+ /* Reset the insertion mode appropriately. */
+ $this->resetInsertionMode();
+ }
+
+ /* A start tag whose tag name is "select" */
+ } elseif($token['name'] === 'select' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Parse error. Act as if the token had been an end tag with the
+ tag name "select" instead. */
+ $this->inSelect(array(
+ 'name' => 'select',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ /* An end tag whose tag name is one of: "caption", "table", "tbody",
+ "tfoot", "thead", "tr", "td", "th" */
+ } elseif(in_array($token['name'], array('caption', 'table', 'tbody',
+ 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) {
+ /* Parse error. */
+ // w/e
+
+ /* If the stack of open elements has an element in table scope with
+ the same tag name as that of the token, then act as if an end tag
+ with the tag name "select" had been seen, and reprocess the token.
+ Otherwise, ignore the token. */
+ if($this->elementInScope($token['name'], true)) {
+ $this->inSelect(array(
+ 'name' => 'select',
+ 'type' => HTML5::ENDTAG
+ ));
+
+ $this->mainPhase($token);
+ }
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function afterBody($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Process the token as it would be processed if the insertion mode
+ was "in body". */
+ $this->inBody($token);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the first element in the stack of open
+ elements (the html element), with the data attribute set to the
+ data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->stack[0]->appendChild($comment);
+
+ /* An end tag with the tag name "html" */
+ } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') {
+ /* If the parser was originally created in order to handle the
+ setting of an element's innerHTML attribute, this is a parse error;
+ ignore the token. (The element will be an html element in this
+ case.) (innerHTML case) */
+
+ /* Otherwise, switch to the trailing end phase. */
+ $this->phase = self::END_PHASE;
+
+ /* Anything else */
+ } else {
+ /* Parse error. Set the insertion mode to "in body" and reprocess
+ the token. */
+ $this->mode = self::IN_BODY;
+ return $this->inBody($token);
+ }
+ }
+
+ private function inFrameset($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* A start tag with the tag name "frameset" */
+ } elseif($token['name'] === 'frameset' &&
+ $token['type'] === HTML5::STARTTAG) {
+ $this->insertElement($token);
+
+ /* An end tag with the tag name "frameset" */
+ } elseif($token['name'] === 'frameset' &&
+ $token['type'] === HTML5::ENDTAG) {
+ /* If the current node is the root html element, then this is a
+ parse error; ignore the token. (innerHTML case) */
+ if(end($this->stack)->nodeName === 'html') {
+ // Ignore
+
+ } else {
+ /* Otherwise, pop the current node from the stack of open
+ elements. */
+ array_pop($this->stack);
+
+ /* If the parser was not originally created in order to handle
+ the setting of an element's innerHTML attribute (innerHTML case),
+ and the current node is no longer a frameset element, then change
+ the insertion mode to "after frameset". */
+ $this->mode = self::AFTR_FRAME;
+ }
+
+ /* A start tag with the tag name "frame" */
+ } elseif($token['name'] === 'frame' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Insert an HTML element for the token. */
+ $this->insertElement($token);
+
+ /* Immediately pop the current node off the stack of open elements. */
+ array_pop($this->stack);
+
+ /* A start tag with the tag name "noframes" */
+ } elseif($token['name'] === 'noframes' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Process the token as if the insertion mode had been "in body". */
+ $this->inBody($token);
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function afterFrameset($token) {
+ /* Handle the token as follows: */
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */
+ if($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Append the character to the current node. */
+ $this->insertText($token['data']);
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the current node with the data
+ attribute set to the data given in the comment token. */
+ $this->insertComment($token['data']);
+
+ /* An end tag with the tag name "html" */
+ } elseif($token['name'] === 'html' &&
+ $token['type'] === HTML5::ENDTAG) {
+ /* Switch to the trailing end phase. */
+ $this->phase = self::END_PHASE;
+
+ /* A start tag with the tag name "noframes" */
+ } elseif($token['name'] === 'noframes' &&
+ $token['type'] === HTML5::STARTTAG) {
+ /* Process the token as if the insertion mode had been "in body". */
+ $this->inBody($token);
+
+ /* Anything else */
+ } else {
+ /* Parse error. Ignore the token. */
+ }
+ }
+
+ private function trailingEndPhase($token) {
+ /* After the main phase, as each token is emitted from the tokenisation
+ stage, it must be processed as described in this section. */
+
+ /* A DOCTYPE token */
+ if($token['type'] === HTML5::DOCTYPE) {
+ // Parse error. Ignore the token.
+
+ /* A comment token */
+ } elseif($token['type'] === HTML5::COMMENT) {
+ /* Append a Comment node to the Document object with the data
+ attribute set to the data given in the comment token. */
+ $comment = $this->dom->createComment($token['data']);
+ $this->dom->appendChild($comment);
+
+ /* A character token that is one of one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE */
+ } elseif($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) {
+ /* Process the token as it would be processed in the main phase. */
+ $this->mainPhase($token);
+
+ /* A character token that is not one of U+0009 CHARACTER TABULATION,
+ U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF),
+ or U+0020 SPACE. Or a start tag token. Or an end tag token. */
+ } elseif(($token['type'] === HTML5::CHARACTR &&
+ preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) ||
+ $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) {
+ /* Parse error. Switch back to the main phase and reprocess the
+ token. */
+ $this->phase = self::MAIN_PHASE;
+ return $this->mainPhase($token);
+
+ /* An end-of-file token */
+ } elseif($token['type'] === HTML5::EOF) {
+ /* OMG DONE!! */
+ }
+ }
+
+ private function insertElement($token, $append = true, $check = false) {
+ // Proprietary workaround for libxml2's limitations with tag names
+ if ($check) {
+ // Slightly modified HTML5 tag-name modification,
+ // removing anything that's not an ASCII letter, digit, or hyphen
+ $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']);
+ // Remove leading hyphens and numbers
+ $token['name'] = ltrim($token['name'], '-0..9');
+ // In theory, this should ever be needed, but just in case
+ if ($token['name'] === '') $token['name'] = 'span'; // arbitrary generic choice
+ }
+
+ $el = $this->dom->createElement($token['name']);
+
+ foreach($token['attr'] as $attr) {
+ if(!$el->hasAttribute($attr['name'])) {
+ $el->setAttribute($attr['name'], $attr['value']);
+ }
+ }
+
+ $this->appendToRealParent($el);
+ $this->stack[] = $el;
+
+ return $el;
+ }
+
+ private function insertText($data) {
+ $text = $this->dom->createTextNode($data);
+ $this->appendToRealParent($text);
+ }
+
+ private function insertComment($data) {
+ $comment = $this->dom->createComment($data);
+ $this->appendToRealParent($comment);
+ }
+
+ private function appendToRealParent($node) {
+ if($this->foster_parent === null) {
+ end($this->stack)->appendChild($node);
+
+ } elseif($this->foster_parent !== null) {
+ /* If the foster parent element is the parent element of the
+ last table element in the stack of open elements, then the new
+ node must be inserted immediately before the last table element
+ in the stack of open elements in the foster parent element;
+ otherwise, the new node must be appended to the foster parent
+ element. */
+ for($n = count($this->stack) - 1; $n >= 0; $n--) {
+ if($this->stack[$n]->nodeName === 'table' &&
+ $this->stack[$n]->parentNode !== null) {
+ $table = $this->stack[$n];
+ break;
+ }
+ }
+
+ if(isset($table) && $this->foster_parent->isSameNode($table->parentNode))
+ $this->foster_parent->insertBefore($node, $table);
+ else
+ $this->foster_parent->appendChild($node);
+
+ $this->foster_parent = null;
+ }
+ }
+
+ private function elementInScope($el, $table = false) {
+ if(is_array($el)) {
+ foreach($el as $element) {
+ if($this->elementInScope($element, $table)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ $leng = count($this->stack);
+
+ for($n = 0; $n < $leng; $n++) {
+ /* 1. Initialise node to be the current node (the bottommost node of
+ the stack). */
+ $node = $this->stack[$leng - 1 - $n];
+
+ if($node->tagName === $el) {
+ /* 2. If node is the target node, terminate in a match state. */
+ return true;
+
+ } elseif($node->tagName === 'table') {
+ /* 3. Otherwise, if node is a table element, terminate in a failure
+ state. */
+ return false;
+
+ } elseif($table === true && in_array($node->tagName, array('caption', 'td',
+ 'th', 'button', 'marquee', 'object'))) {
+ /* 4. Otherwise, if the algorithm is the "has an element in scope"
+ variant (rather than the "has an element in table scope" variant),
+ and node is one of the following, terminate in a failure state. */
+ return false;
+
+ } elseif($node === $node->ownerDocument->documentElement) {
+ /* 5. Otherwise, if node is an html element (root element), terminate
+ in a failure state. (This can only happen if the node is the topmost
+ node of the stack of open elements, and prevents the next step from
+ being invoked if there are no more elements in the stack.) */
+ return false;
+ }
+
+ /* Otherwise, set node to the previous entry in the stack of open
+ elements and return to step 2. (This will never fail, since the loop
+ will always terminate in the previous step if the top of the stack
+ is reached.) */
+ }
+ }
+
+ private function reconstructActiveFormattingElements() {
+ /* 1. If there are no entries in the list of active formatting elements,
+ then there is nothing to reconstruct; stop this algorithm. */
+ $formatting_elements = count($this->a_formatting);
+
+ if($formatting_elements === 0) {
+ return false;
+ }
+
+ /* 3. Let entry be the last (most recently added) element in the list
+ of active formatting elements. */
+ $entry = end($this->a_formatting);
+
+ /* 2. If the last (most recently added) entry in the list of active
+ formatting elements is a marker, or if it is an element that is in the
+ stack of open elements, then there is nothing to reconstruct; stop this
+ algorithm. */
+ if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
+ return false;
+ }
+
+ for($a = $formatting_elements - 1; $a >= 0; true) {
+ /* 4. If there are no entries before entry in the list of active
+ formatting elements, then jump to step 8. */
+ if($a === 0) {
+ $step_seven = false;
+ break;
+ }
+
+ /* 5. Let entry be the entry one earlier than entry in the list of
+ active formatting elements. */
+ $a--;
+ $entry = $this->a_formatting[$a];
+
+ /* 6. If entry is neither a marker nor an element that is also in
+ thetack of open elements, go to step 4. */
+ if($entry === self::MARKER || in_array($entry, $this->stack, true)) {
+ break;
+ }
+ }
+
+ while(true) {
+ /* 7. Let entry be the element one later than entry in the list of
+ active formatting elements. */
+ if(isset($step_seven) && $step_seven === true) {
+ $a++;
+ $entry = $this->a_formatting[$a];
+ }
+
+ /* 8. Perform a shallow clone of the element entry to obtain clone. */
+ $clone = $entry->cloneNode();
+
+ /* 9. Append clone to the current node and push it onto the stack
+ of open elements so that it is the new current node. */
+ end($this->stack)->appendChild($clone);
+ $this->stack[] = $clone;
+
+ /* 10. Replace the entry for entry in the list with an entry for
+ clone. */
+ $this->a_formatting[$a] = $clone;
+
+ /* 11. If the entry for clone in the list of active formatting
+ elements is not the last entry in the list, return to step 7. */
+ if(end($this->a_formatting) !== $clone) {
+ $step_seven = true;
+ } else {
+ break;
+ }
+ }
+ }
+
+ private function clearTheActiveFormattingElementsUpToTheLastMarker() {
+ /* When the steps below require the UA to clear the list of active
+ formatting elements up to the last marker, the UA must perform the
+ following steps: */
+
+ while(true) {
+ /* 1. Let entry be the last (most recently added) entry in the list
+ of active formatting elements. */
+ $entry = end($this->a_formatting);
+
+ /* 2. Remove entry from the list of active formatting elements. */
+ array_pop($this->a_formatting);
+
+ /* 3. If entry was a marker, then stop the algorithm at this point.
+ The list has been cleared up to the last marker. */
+ if($entry === self::MARKER) {
+ break;
+ }
+ }
+ }
+
+ private function generateImpliedEndTags($exclude = array()) {
+ /* When the steps below require the UA to generate implied end tags,
+ then, if the current node is a dd element, a dt element, an li element,
+ a p element, a td element, a th element, or a tr element, the UA must
+ act as if an end tag with the respective tag name had been seen and
+ then generate implied end tags again. */
+ $node = end($this->stack);
+ $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude);
+
+ while(in_array(end($this->stack)->nodeName, $elements)) {
+ array_pop($this->stack);
+ }
+ }
+
+ private function getElementCategory($node) {
+ $name = $node->tagName;
+ if(in_array($name, $this->special))
+ return self::SPECIAL;
+
+ elseif(in_array($name, $this->scoping))
+ return self::SCOPING;
+
+ elseif(in_array($name, $this->formatting))
+ return self::FORMATTING;
+
+ else
+ return self::PHRASING;
+ }
+
+ private function clearStackToTableContext($elements) {
+ /* When the steps above require the UA to clear the stack back to a
+ table context, it means that the UA must, while the current node is not
+ a table element or an html element, pop elements from the stack of open
+ elements. If this causes any elements to be popped from the stack, then
+ this is a parse error. */
+ while(true) {
+ $node = end($this->stack)->nodeName;
+
+ if(in_array($node, $elements)) {
+ break;
+ } else {
+ array_pop($this->stack);
+ }
+ }
+ }
+
+ private function resetInsertionMode() {
+ /* 1. Let last be false. */
+ $last = false;
+ $leng = count($this->stack);
+
+ for($n = $leng - 1; $n >= 0; $n--) {
+ /* 2. Let node be the last node in the stack of open elements. */
+ $node = $this->stack[$n];
+
+ /* 3. If node is the first node in the stack of open elements, then
+ set last to true. If the element whose innerHTML attribute is being
+ set is neither a td element nor a th element, then set node to the
+ element whose innerHTML attribute is being set. (innerHTML case) */
+ if($this->stack[0]->isSameNode($node)) {
+ $last = true;
+ }
+
+ /* 4. If node is a select element, then switch the insertion mode to
+ "in select" and abort these steps. (innerHTML case) */
+ if($node->nodeName === 'select') {
+ $this->mode = self::IN_SELECT;
+ break;
+
+ /* 5. If node is a td or th element, then switch the insertion mode
+ to "in cell" and abort these steps. */
+ } elseif($node->nodeName === 'td' || $node->nodeName === 'th') {
+ $this->mode = self::IN_CELL;
+ break;
+
+ /* 6. If node is a tr element, then switch the insertion mode to
+ "in row" and abort these steps. */
+ } elseif($node->nodeName === 'tr') {
+ $this->mode = self::IN_ROW;
+ break;
+
+ /* 7. If node is a tbody, thead, or tfoot element, then switch the
+ insertion mode to "in table body" and abort these steps. */
+ } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) {
+ $this->mode = self::IN_TBODY;
+ break;
+
+ /* 8. If node is a caption element, then switch the insertion mode
+ to "in caption" and abort these steps. */
+ } elseif($node->nodeName === 'caption') {
+ $this->mode = self::IN_CAPTION;
+ break;
+
+ /* 9. If node is a colgroup element, then switch the insertion mode
+ to "in column group" and abort these steps. (innerHTML case) */
+ } elseif($node->nodeName === 'colgroup') {
+ $this->mode = self::IN_CGROUP;
+ break;
+
+ /* 10. If node is a table element, then switch the insertion mode
+ to "in table" and abort these steps. */
+ } elseif($node->nodeName === 'table') {
+ $this->mode = self::IN_TABLE;
+ break;
+
+ /* 11. If node is a head element, then switch the insertion mode
+ to "in body" ("in body"! not "in head"!) and abort these steps.
+ (innerHTML case) */
+ } elseif($node->nodeName === 'head') {
+ $this->mode = self::IN_BODY;
+ break;
+
+ /* 12. If node is a body element, then switch the insertion mode to
+ "in body" and abort these steps. */
+ } elseif($node->nodeName === 'body') {
+ $this->mode = self::IN_BODY;
+ break;
+
+ /* 13. If node is a frameset element, then switch the insertion
+ mode to "in frameset" and abort these steps. (innerHTML case) */
+ } elseif($node->nodeName === 'frameset') {
+ $this->mode = self::IN_FRAME;
+ break;
+
+ /* 14. If node is an html element, then: if the head element
+ pointer is null, switch the insertion mode to "before head",
+ otherwise, switch the insertion mode to "after head". In either
+ case, abort these steps. (innerHTML case) */
+ } elseif($node->nodeName === 'html') {
+ $this->mode = ($this->head_pointer === null)
+ ? self::BEFOR_HEAD
+ : self::AFTER_HEAD;
+
+ break;
+
+ /* 15. If last is true, then set the insertion mode to "in body"
+ and abort these steps. (innerHTML case) */
+ } elseif($last) {
+ $this->mode = self::IN_BODY;
+ break;
+ }
+ }
+ }
+
+ private function closeCell() {
+ /* If the stack of open elements has a td or th element in table scope,
+ then act as if an end tag token with that tag name had been seen. */
+ foreach(array('td', 'th') as $cell) {
+ if($this->elementInScope($cell, true)) {
+ $this->inCell(array(
+ 'name' => $cell,
+ 'type' => HTML5::ENDTAG
+ ));
+
+ break;
+ }
+ }
+ }
+
+ public function save() {
+ return $this->dom;
+ }
+}
+?>
diff --git a/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php b/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php
index c3b6c8e6794..a43c44f4c58 100644
--- a/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php
+++ b/lib/htmlpurifier/HTMLPurifier/PercentEncoder.php
@@ -10,16 +10,16 @@
*/
class HTMLPurifier_PercentEncoder
{
-
+
/**
* Reserved characters to preserve when using encode().
*/
- var $preserve = array();
-
+ protected $preserve = array();
+
/**
* String of characters that should be preserved while using encode().
*/
- function HTMLPurifier_PercentEncoder($preserve = false) {
+ public function __construct($preserve = false) {
// unreserved letters, ought to const-ify
for ($i = 48; $i <= 57; $i++) $this->preserve[$i] = true; // digits
for ($i = 65; $i <= 90; $i++) $this->preserve[$i] = true; // upper-case
@@ -28,7 +28,7 @@ class HTMLPurifier_PercentEncoder
$this->preserve[46] = true; // Period .
$this->preserve[95] = true; // Underscore _
$this->preserve[126]= true; // Tilde ~
-
+
// extra letters not to escape
if ($preserve !== false) {
for ($i = 0, $c = strlen($preserve); $i < $c; $i++) {
@@ -36,7 +36,7 @@ class HTMLPurifier_PercentEncoder
}
}
}
-
+
/**
* Our replacement for urlencode, it encodes all non-reserved characters,
* as well as any extra characters that were instructed to be preserved.
@@ -47,7 +47,7 @@ class HTMLPurifier_PercentEncoder
* @param $string String to be encoded
* @return Encoded string.
*/
- function encode($string) {
+ public function encode($string) {
$ret = '';
for ($i = 0, $c = strlen($string); $i < $c; $i++) {
if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) {
@@ -58,7 +58,7 @@ class HTMLPurifier_PercentEncoder
}
return $ret;
}
-
+
/**
* Fix up percent-encoding by decoding unreserved characters and normalizing.
* @warning This function is affected by $preserve, even though the
@@ -66,7 +66,7 @@ class HTMLPurifier_PercentEncoder
* characters. Be careful when reusing instances of PercentEncoder!
* @param $string String to normalize
*/
- function normalize($string) {
+ public function normalize($string) {
if ($string == '') return '';
$parts = explode('%', $string);
$ret = array_shift($parts);
@@ -92,6 +92,7 @@ class HTMLPurifier_PercentEncoder
}
return $ret;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Printer.php b/lib/htmlpurifier/HTMLPurifier/Printer.php
index 7e20daafe35..e7eb82e83e4 100644
--- a/lib/htmlpurifier/HTMLPurifier/Printer.php
+++ b/lib/htmlpurifier/HTMLPurifier/Printer.php
@@ -1,68 +1,63 @@
generator = new HTMLPurifier_Generator();
+ public function __construct() {
}
-
+
/**
* Give generator necessary configuration if possible
*/
- function prepareGenerator($config) {
- // hack for smoketests/configForm.php
- if (empty($config->conf['HTML'])) return;
+ public function prepareGenerator($config) {
+ $all = $config->getAll();
$context = new HTMLPurifier_Context();
- $this->generator->generateFromTokens(array(), $config, $context);
+ $this->generator = new HTMLPurifier_Generator($config, $context);
}
-
+
/**
* Main function that renders object or aspect of that object
* @note Parameters vary depending on printer
*/
// function render() {}
-
+
/**
* Returns a start tag
* @param $tag Tag name
* @param $attr Attribute array
*/
- function start($tag, $attr = array()) {
+ protected function start($tag, $attr = array()) {
return $this->generator->generateFromToken(
new HTMLPurifier_Token_Start($tag, $attr ? $attr : array())
);
}
-
+
/**
* Returns an end teg
* @param $tag Tag name
*/
- function end($tag) {
+ protected function end($tag) {
return $this->generator->generateFromToken(
new HTMLPurifier_Token_End($tag)
);
}
-
+
/**
* Prints a complete element with content inside
* @param $tag Tag name
@@ -70,30 +65,30 @@ class HTMLPurifier_Printer
* @param $attr Tag attributes
* @param $escape Bool whether or not to escape contents
*/
- function element($tag, $contents, $attr = array(), $escape = true) {
+ protected function element($tag, $contents, $attr = array(), $escape = true) {
return $this->start($tag, $attr) .
($escape ? $this->escape($contents) : $contents) .
$this->end($tag);
}
-
- function elementEmpty($tag, $attr = array()) {
+
+ protected function elementEmpty($tag, $attr = array()) {
return $this->generator->generateFromToken(
new HTMLPurifier_Token_Empty($tag, $attr)
);
}
-
- function text($text) {
+
+ protected function text($text) {
return $this->generator->generateFromToken(
new HTMLPurifier_Token_Text($text)
);
}
-
+
/**
* Prints a simple key/value row in a table.
* @param $name Key
* @param $value Value
*/
- function row($name, $value) {
+ protected function row($name, $value) {
if (is_bool($value)) $value = $value ? 'On' : 'Off';
return
$this->start('tr') . "\n" .
@@ -102,23 +97,23 @@ class HTMLPurifier_Printer
$this->end('tr')
;
}
-
+
/**
* Escapes a string for HTML output.
* @param $string String to escape
*/
- function escape($string) {
+ protected function escape($string) {
$string = HTMLPurifier_Encoder::cleanUTF8($string);
$string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8');
return $string;
}
-
+
/**
* Takes a list of strings and turns them into a single list
* @param $array List of strings
* @param $polite Bool whether or not to add an end before the last
*/
- function listify($array, $polite = false) {
+ protected function listify($array, $polite = false) {
if (empty($array)) return 'None';
$ret = '';
$i = count($array);
@@ -130,13 +125,13 @@ class HTMLPurifier_Printer
}
return $ret;
}
-
+
/**
* Retrieves the class of an object without prefixes, as well as metadata
* @param $obj Object to determine class of
* @param $prefix Further prefix to remove
*/
- function getClass($obj, $sec_prefix = '') {
+ protected function getClass($obj, $sec_prefix = '') {
static $five = null;
if ($five === null) $five = version_compare(PHP_VERSION, '5', '>=');
$prefix = 'HTMLPurifier_' . $sec_prefix;
@@ -152,21 +147,30 @@ class HTMLPurifier_Printer
}
$class .= implode(', ', $values);
break;
- case 'composite':
+ case 'css_composite':
$values = array();
foreach ($obj->defs as $def) {
$values[] = $this->getClass($def, $sec_prefix);
}
$class .= implode(', ', $values);
break;
- case 'multiple':
+ case 'css_multiple':
$class .= $this->getClass($obj->single, $sec_prefix) . ', ';
$class .= $obj->max;
break;
+ case 'css_denyelementdecorator':
+ $class .= $this->getClass($obj->def, $sec_prefix) . ', ';
+ $class .= $obj->element;
+ break;
+ case 'css_importantdecorator':
+ $class .= $this->getClass($obj->def, $sec_prefix);
+ if ($obj->allow) $class .= ', !important';
+ break;
}
$class .= ')';
return $class;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php b/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php
index 7d3ad61e98e..81f9865901d 100644
--- a/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php
+++ b/lib/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php
@@ -1,39 +1,38 @@
def = $config->getCSSDefinition();
$ret = '';
-
+
$ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer'));
$ret .= $this->start('table');
-
+
$ret .= $this->element('caption', 'Properties ($info)');
-
+
$ret .= $this->start('thead');
$ret .= $this->start('tr');
$ret .= $this->element('th', 'Property', array('class' => 'heavy'));
$ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;'));
$ret .= $this->end('tr');
$ret .= $this->end('thead');
-
+
ksort($this->def->info);
foreach ($this->def->info as $property => $obj) {
$name = $this->getClass($obj, 'AttrDef_');
$ret .= $this->row($property, $name);
}
-
+
$ret .= $this->end('table');
$ret .= $this->end('div');
-
+
return $ret;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css
index 0653bbb0c0c..3ff1a88aa42 100644
--- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css
+++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css
@@ -6,3 +6,5 @@
.hp-config .namespace th {text-align:center;}
.hp-config .verbose {display:none;}
.hp-config .controls {text-align:center;}
+
+/* vim: et sw=4 sts=4 */
diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js
index 119ca4a04d9..cba00c9b80b 100644
--- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js
+++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js
@@ -1,3 +1,5 @@
function toggleWriteability(id_of_patient, checked) {
document.getElementById(id_of_patient).disabled = checked;
-}
\ No newline at end of file
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php
index 21b8314bc52..02aa6568940 100644
--- a/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php
+++ b/lib/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php
@@ -1,99 +1,105 @@
docURL = $doc_url;
$this->name = $name;
$this->compress = $compress;
// initialize sub-printers
- $this->fields['default'] = new HTMLPurifier_Printer_ConfigForm_default();
- $this->fields['bool'] = new HTMLPurifier_Printer_ConfigForm_bool();
+ $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default();
+ $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool();
}
-
+
/**
* Sets default column and row size for textareas in sub-printers
* @param $cols Integer columns of textarea, null to use default
* @param $rows Integer rows of textarea, null to use default
*/
- function setTextareaDimensions($cols = null, $rows = null) {
+ public function setTextareaDimensions($cols = null, $rows = null) {
if ($cols) $this->fields['default']->cols = $cols;
if ($rows) $this->fields['default']->rows = $rows;
}
-
+
/**
* Retrieves styling, in case it is not accessible by webserver
*/
- function getCSS() {
+ public static function getCSS() {
return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css');
}
-
+
/**
* Retrieves JavaScript, in case it is not accessible by webserver
*/
- function getJavaScript() {
+ public static function getJavaScript() {
return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js');
}
-
+
/**
* Returns HTML output for a configuration form
- * @param $config Configuration object of current form state
+ * @param $config Configuration object of current form state, or an array
+ * where [0] has an HTML namespace and [1] is being rendered.
* @param $allowed Optional namespace(s) and directives to restrict form to.
*/
- function render($config, $allowed = true, $render_controls = true) {
+ public function render($config, $allowed = true, $render_controls = true) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+
$this->config = $config;
- $this->prepareGenerator($config);
-
- $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed);
+ $this->genConfig = $gen_config;
+ $this->prepareGenerator($gen_config);
+
+ $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def);
$all = array();
foreach ($allowed as $key) {
list($ns, $directive) = $key;
- $all[$ns][$directive] = $config->get($ns, $directive);
+ $all[$ns][$directive] = $config->get($ns .'.'. $directive);
}
-
+
$ret = '';
$ret .= $this->start('table', array('class' => 'hp-config'));
$ret .= $this->start('thead');
$ret .= $this->start('tr');
- $ret .= $this->element('th', 'Directive');
- $ret .= $this->element('th', 'Value');
+ $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive'));
+ $ret .= $this->element('th', 'Value', array('class' => 'hp-value'));
$ret .= $this->end('tr');
$ret .= $this->end('thead');
foreach ($all as $ns => $directives) {
@@ -112,14 +118,13 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer
$ret .= $this->end('table');
return $ret;
}
-
+
/**
* Renders a single namespace
* @param $ns String namespace name
* @param $directive Associative array of directives to values
- * @protected
*/
- function renderNamespace($ns, $directives) {
+ protected function renderNamespace($ns, $directives) {
$ret = '';
$ret .= $this->start('tbody', array('class' => 'namespace'));
$ret .= $this->start('tr');
@@ -135,7 +140,7 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer
$ret .= $this->start('a', array('href' => $url));
}
$attr = array('for' => "{$this->name}:$ns.$directive");
-
+
// crop directive name if it's too long
if (!$this->compress || (strlen($directive) < $this->compress)) {
$directive_disp = $directive;
@@ -143,7 +148,7 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer
$directive_disp = substr($directive, 0, $this->compress - 2) . '...';
$attr['title'] = $directive;
}
-
+
$ret .= $this->element(
'label',
$directive_disp,
@@ -152,23 +157,29 @@ class HTMLPurifier_Printer_ConfigForm extends HTMLPurifier_Printer
);
if ($this->docURL) $ret .= $this->end('a');
$ret .= $this->end('th');
-
+
$ret .= $this->start('td');
- $def = $this->config->def->info[$ns][$directive];
- $type = $def->type;
- if (!isset($this->fields[$type])) $type = 'default';
+ $def = $this->config->def->info["$ns.$directive"];
+ if (is_int($def)) {
+ $allow_null = $def < 0;
+ $type = abs($def);
+ } else {
+ $type = $def->type;
+ $allow_null = isset($def->allow_null);
+ }
+ if (!isset($this->fields[$type])) $type = 0; // default
$type_obj = $this->fields[$type];
- if ($def->allow_null) {
+ if ($allow_null) {
$type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj);
}
- $ret .= $type_obj->render($ns, $directive, $value, $this->name, $this->config);
+ $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config));
$ret .= $this->end('td');
$ret .= $this->end('tr');
}
$ret .= $this->end('tbody');
return $ret;
}
-
+
}
/**
@@ -178,16 +189,23 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer
/**
* Printer being decorated
*/
- var $obj;
+ protected $obj;
/**
* @param $obj Printer to decorate
*/
- function HTMLPurifier_Printer_ConfigForm_NullDecorator($obj) {
- parent::HTMLPurifier_Printer();
+ public function __construct($obj) {
+ parent::__construct();
$this->obj = $obj;
}
- function render($ns, $directive, $value, $name, $config) {
- $this->prepareGenerator($config);
+ public function render($ns, $directive, $value, $name, $config) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
+
$ret = '';
$ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive"));
$ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
@@ -201,11 +219,15 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer
'id' => "$name:Null_$ns.$directive",
'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!!
);
+ if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) {
+ // modify inline javascript slightly
+ $attr['onclick'] = "toggleWriteability('$name:Yes_$ns.$directive',checked);toggleWriteability('$name:No_$ns.$directive',checked)";
+ }
if ($value === null) $attr['checked'] = 'checked';
$ret .= $this->elementEmpty('input', $attr);
$ret .= $this->text(' or ');
$ret .= $this->elementEmpty('br');
- $ret .= $this->obj->render($ns, $directive, $value, $name, $config);
+ $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config));
return $ret;
}
}
@@ -214,25 +236,36 @@ class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer
* Swiss-army knife configuration form field printer
*/
class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer {
- var $cols = 18;
- var $rows = 5;
- function render($ns, $directive, $value, $name, $config) {
- $this->prepareGenerator($config);
+ public $cols = 18;
+ public $rows = 5;
+ public function render($ns, $directive, $value, $name, $config) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
// this should probably be split up a little
$ret = '';
- $def = $config->def->info[$ns][$directive];
+ $def = $config->def->info["$ns.$directive"];
+ if (is_int($def)) {
+ $type = abs($def);
+ } else {
+ $type = $def->type;
+ }
if (is_array($value)) {
- switch ($def->type) {
- case 'lookup':
+ switch ($type) {
+ case HTMLPurifier_VarParser::LOOKUP:
$array = $value;
$value = array();
foreach ($array as $val => $b) {
$value[] = $val;
}
- case 'list':
+ case HTMLPurifier_VarParser::ALIST:
$value = implode(PHP_EOL, $value);
break;
- case 'hash':
+ case HTMLPurifier_VarParser::HASH:
$nvalue = '';
foreach ($value as $i => $v) {
$nvalue .= "$i:$v" . PHP_EOL;
@@ -243,7 +276,7 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer {
$value = '';
}
}
- if ($def->type === 'mixed') {
+ if ($type === HTMLPurifier_VarParser::MIXED) {
return 'Not supported';
$value = serialize($value);
}
@@ -252,7 +285,7 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer {
'id' => "$name:$ns.$directive"
);
if ($value === null) $attr['disabled'] = 'disabled';
- if (is_array($def->allowed)) {
+ if (isset($def->allowed)) {
$ret .= $this->start('select', $attr);
foreach ($def->allowed as $val => $b) {
$attr = array();
@@ -261,8 +294,11 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer {
}
$ret .= $this->end('select');
} elseif (
- $def->type == 'text' || $def->type == 'itext' ||
- $def->type == 'list' || $def->type == 'hash' || $def->type == 'lookup'
+ $type === HTMLPurifier_VarParser::TEXT ||
+ $type === HTMLPurifier_VarParser::ITEXT ||
+ $type === HTMLPurifier_VarParser::ALIST ||
+ $type === HTMLPurifier_VarParser::HASH ||
+ $type === HTMLPurifier_VarParser::LOOKUP
) {
$attr['cols'] = $this->cols;
$attr['rows'] = $this->rows;
@@ -282,42 +318,51 @@ class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer {
* Bool form field printer
*/
class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer {
- function render($ns, $directive, $value, $name, $config) {
- $this->prepareGenerator($config);
+ public function render($ns, $directive, $value, $name, $config) {
+ if (is_array($config) && isset($config[0])) {
+ $gen_config = $config[0];
+ $config = $config[1];
+ } else {
+ $gen_config = $config;
+ }
+ $this->prepareGenerator($gen_config);
$ret = '';
$ret .= $this->start('div', array('id' => "$name:$ns.$directive"));
-
+
$ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive"));
$ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
$ret .= $this->text(' Yes');
$ret .= $this->end('label');
-
+
$attr = array(
'type' => 'radio',
'name' => "$name"."[$ns.$directive]",
'id' => "$name:Yes_$ns.$directive",
'value' => '1'
);
- if ($value) $attr['checked'] = 'checked';
+ if ($value === true) $attr['checked'] = 'checked';
+ if ($value === null) $attr['disabled'] = 'disabled';
$ret .= $this->elementEmpty('input', $attr);
-
+
$ret .= $this->start('label', array('for' => "$name:No_$ns.$directive"));
$ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose'));
$ret .= $this->text(' No');
$ret .= $this->end('label');
-
+
$attr = array(
'type' => 'radio',
'name' => "$name"."[$ns.$directive]",
'id' => "$name:No_$ns.$directive",
'value' => '0'
);
- if (!$value) $attr['checked'] = 'checked';
+ if ($value === false) $attr['checked'] = 'checked';
+ if ($value === null) $attr['disabled'] = 'disabled';
$ret .= $this->elementEmpty('input', $attr);
-
+
$ret .= $this->end('div');
-
+
return $ret;
}
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php b/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php
index 4a0a90f1525..8a8f126b81f 100644
--- a/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php
+++ b/lib/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php
@@ -1,37 +1,35 @@
config =& $config;
-
+
$this->def = $config->getHTMLDefinition();
-
+
$ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer'));
-
+
$ret .= $this->renderDoctype();
$ret .= $this->renderEnvironment();
$ret .= $this->renderContentSets();
$ret .= $this->renderInfo();
-
+
$ret .= $this->end('div');
-
+
return $ret;
}
-
+
/**
* Renders the Doctype table
*/
- function renderDoctype() {
+ protected function renderDoctype() {
$doctype = $this->def->doctype;
$ret = '';
$ret .= $this->start('table');
@@ -43,28 +41,28 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->end('table');
return $ret;
}
-
-
+
+
/**
* Renders environment table, which is miscellaneous info
*/
- function renderEnvironment() {
+ protected function renderEnvironment() {
$def = $this->def;
-
+
$ret = '';
-
+
$ret .= $this->start('table');
$ret .= $this->element('caption', 'Environment');
-
+
$ret .= $this->row('Parent of fragment', $def->info_parent);
$ret .= $this->renderChildren($def->info_parent_def->child);
$ret .= $this->row('Block wrap name', $def->info_block_wrapper);
-
+
$ret .= $this->start('tr');
$ret .= $this->element('th', 'Global attributes');
$ret .= $this->element('td', $this->listifyAttr($def->info_global_attr),0,0);
$ret .= $this->end('tr');
-
+
$ret .= $this->start('tr');
$ret .= $this->element('th', 'Tag transforms');
$list = array();
@@ -74,25 +72,25 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
}
$ret .= $this->element('td', $this->listify($list));
$ret .= $this->end('tr');
-
+
$ret .= $this->start('tr');
$ret .= $this->element('th', 'Pre-AttrTransform');
$ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre));
$ret .= $this->end('tr');
-
+
$ret .= $this->start('tr');
$ret .= $this->element('th', 'Post-AttrTransform');
$ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post));
$ret .= $this->end('tr');
-
+
$ret .= $this->end('table');
return $ret;
}
-
+
/**
* Renders the Content Sets table
*/
- function renderContentSets() {
+ protected function renderContentSets() {
$ret = '';
$ret .= $this->start('table');
$ret .= $this->element('caption', 'Content Sets');
@@ -105,11 +103,11 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->end('table');
return $ret;
}
-
+
/**
* Renders the Elements ($info) table
*/
- function renderInfo() {
+ protected function renderInfo() {
$ret = '';
$ret .= $this->start('table');
$ret .= $this->element('caption', 'Elements ($info)');
@@ -120,7 +118,7 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->end('tr');
foreach ($this->def->info as $name => $def) {
$ret .= $this->start('tr');
- $ret .= $this->element('th', "<$name>" . ($def->safe ? '' : ' (unsafe)'), array('class'=>'heavy' . ($def->safe ? '' : ' unsafe'), 'colspan' => 2));
+ $ret .= $this->element('th', "<$name>", array('class'=>'heavy', 'colspan' => 2));
$ret .= $this->end('tr');
$ret .= $this->start('tr');
$ret .= $this->element('th', 'Inline content');
@@ -154,22 +152,22 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->element('th', 'Allowed attributes');
$ret .= $this->element('td',$this->listifyAttr($def->attr), array(), 0);
$ret .= $this->end('tr');
-
+
if (!empty($def->required_attr)) {
$ret .= $this->row('Required attributes', $this->listify($def->required_attr));
}
-
+
$ret .= $this->renderChildren($def->child);
}
$ret .= $this->end('table');
return $ret;
}
-
- /**
+
+ /**
* Renders a row describing the allowed children of an element
* @param $def HTMLPurifier_ChildDef of pertinent element
*/
- function renderChildren($def) {
+ protected function renderChildren($def) {
$context = new HTMLPurifier_Context();
$ret = '';
$ret .= $this->start('tr');
@@ -190,9 +188,9 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
'tfoot', 'tbody', 'tr'));
}
$ret .= $this->element('th', 'Allowed children', $attr);
-
+
if ($def->type == 'chameleon') {
-
+
$ret .= $this->element('td',
'
Block: ' .
$this->escape($this->listifyTagLookup($def->block->elements)),0,0);
@@ -201,12 +199,12 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->element('td',
'
Inline: ' .
$this->escape($this->listifyTagLookup($def->inline->elements)),0,0);
-
+
} elseif ($def->type == 'custom') {
-
+
$ret .= $this->element('td', '
'.ucfirst($def->type).': ' .
$def->dtd_regex);
-
+
} else {
$ret .= $this->element('td',
'
'.ucfirst($def->type).': ' .
@@ -215,12 +213,12 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
$ret .= $this->end('tr');
return $ret;
}
-
- /**
+
+ /**
* Listifies a tag lookup table.
* @param $array Tag lookup array in form of array('tagname' => true)
*/
- function listifyTagLookup($array) {
+ protected function listifyTagLookup($array) {
ksort($array);
$list = array();
foreach ($array as $name => $discard) {
@@ -229,13 +227,13 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
}
return $this->listify($list);
}
-
+
/**
* Listifies a list of objects by retrieving class names and internal state
* @param $array List of objects
* @todo Also add information about internal state
*/
- function listifyObjectList($array) {
+ protected function listifyObjectList($array) {
ksort($array);
$list = array();
foreach ($array as $discard => $obj) {
@@ -243,12 +241,12 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
}
return $this->listify($list);
}
-
+
/**
* Listifies a hash of attributes to AttrDef classes
* @param $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef)
*/
- function listifyAttr($array) {
+ protected function listifyAttr($array) {
ksort($array);
$list = array();
foreach ($array as $name => $obj) {
@@ -257,17 +255,18 @@ class HTMLPurifier_Printer_HTMLDefinition extends HTMLPurifier_Printer
}
return $this->listify($list);
}
-
+
/**
* Creates a heavy header row
*/
- function heavyHeader($text, $num = 1) {
+ protected function heavyHeader($text, $num = 1) {
$ret = '';
$ret .= $this->start('tr');
$ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy'));
$ret .= $this->end('tr');
return $ret;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/PropertyList.php b/lib/htmlpurifier/HTMLPurifier/PropertyList.php
new file mode 100755
index 00000000000..2b99fb7bc3c
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/PropertyList.php
@@ -0,0 +1,86 @@
+parent = $parent;
+ }
+
+ /**
+ * Recursively retrieves the value for a key
+ */
+ public function get($name) {
+ if ($this->has($name)) return $this->data[$name];
+ // possible performance bottleneck, convert to iterative if necessary
+ if ($this->parent) return $this->parent->get($name);
+ throw new HTMLPurifier_Exception("Key '$name' not found");
+ }
+
+ /**
+ * Sets the value of a key, for this plist
+ */
+ public function set($name, $value) {
+ $this->data[$name] = $value;
+ }
+
+ /**
+ * Returns true if a given key exists
+ */
+ public function has($name) {
+ return array_key_exists($name, $this->data);
+ }
+
+ /**
+ * Resets a value to the value of it's parent, usually the default. If
+ * no value is specified, the entire plist is reset.
+ */
+ public function reset($name = null) {
+ if ($name == null) $this->data = array();
+ else unset($this->data[$name]);
+ }
+
+ /**
+ * Squashes this property list and all of its property lists into a single
+ * array, and returns the array. This value is cached by default.
+ * @param $force If true, ignores the cache and regenerates the array.
+ */
+ public function squash($force = false) {
+ if ($this->cache !== null && !$force) return $this->cache;
+ if ($this->parent) {
+ return $this->cache = array_merge($this->parent->squash($force), $this->data);
+ } else {
+ return $this->cache = $this->data;
+ }
+ }
+
+ /**
+ * Returns the parent plist.
+ */
+ public function getParent() {
+ return $this->parent;
+ }
+
+ /**
+ * Sets the parent plist.
+ */
+ public function setParent($plist) {
+ $this->parent = $plist;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/PropertyListIterator.php b/lib/htmlpurifier/HTMLPurifier/PropertyListIterator.php
new file mode 100755
index 00000000000..8f250443e41
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/PropertyListIterator.php
@@ -0,0 +1,32 @@
+l = strlen($filter);
+ $this->filter = $filter;
+ }
+
+ public function accept() {
+ $key = $this->getInnerIterator()->key();
+ if( strncmp($key, $this->filter, $this->l) !== 0 ) {
+ return false;
+ }
+ return true;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy.php b/lib/htmlpurifier/HTMLPurifier/Strategy.php
index a6ab7e8bcab..2462865210c 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy.php
@@ -2,31 +2,25 @@
/**
* Supertype for classes that define a strategy for modifying/purifying tokens.
- *
- * While HTMLPurifier's core purpose is fixing HTML into something proper,
+ *
+ * While HTMLPurifier's core purpose is fixing HTML into something proper,
* strategies provide plug points for extra configuration or even extra
* features, such as custom tags, custom parsing of text, etc.
*/
-HTMLPurifier_ConfigSchema::define(
- 'Core', 'EscapeInvalidTags', false, 'bool',
- 'When true, invalid tags will be written back to the document as plain '.
- 'text. Otherwise, they are silently dropped.'
-);
-
-class HTMLPurifier_Strategy
+
+abstract class HTMLPurifier_Strategy
{
-
+
/**
* Executes the strategy on the tokens.
- *
+ *
* @param $tokens Array of HTMLPurifier_Token objects to be operated on.
* @param $config Configuration options
* @returns Processed array of token objects.
*/
- function execute($tokens, $config, &$context) {
- trigger_error('Cannot call abstract function', E_USER_ERROR);
- }
-
+ abstract public function execute($tokens, $config, $context);
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php b/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php
index fcd230f4723..816490b7996 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy/Composite.php
@@ -1,29 +1,25 @@
strategies as $strategy) {
$tokens = $strategy->execute($tokens, $config, $context);
}
return $tokens;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php b/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php
index 93d051046a0..d90e1586064 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy/Core.php
@@ -1,24 +1,18 @@
strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements();
$this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed();
$this->strategies[] = new HTMLPurifier_Strategy_FixNesting();
$this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes();
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php b/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php
index 25e9f8acbca..f81802391bf 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php
@@ -1,102 +1,99 @@
getHTMLDefinition();
-
+
// insert implicit "parent" node, will be removed at end.
// DEFINITION CALL
$parent_name = $definition->info_parent;
array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name));
$tokens[] = new HTMLPurifier_Token_End($parent_name);
-
+
// setup the context variable 'IsInline', for chameleon processing
// is 'false' when we are not inline, 'true' when it must always
// be inline, and an integer when it is inline for a certain
// branch of the document tree
$is_inline = $definition->info_parent_def->descendants_are_inline;
$context->register('IsInline', $is_inline);
-
+
// setup error collector
$e =& $context->get('ErrorCollector', true);
-
+
//####################################################################//
// Loop initialization
-
+
// stack that contains the indexes of all parents,
// $stack[count($stack)-1] being the current parent
$stack = array();
-
+
// stack that contains all elements that are excluded
- // it is organized by parent elements, similar to $stack,
+ // it is organized by parent elements, similar to $stack,
// but it is only populated when an element with exclusions is
// processed, i.e. there won't be empty exclusions.
$exclude_stack = array();
-
+
// variable that contains the start token while we are processing
// nodes. This enables error reporting to do its job
$start_token = false;
$context->register('CurrentToken', $start_token);
-
+
//####################################################################//
// Loop
-
+
// iterate through all start nodes. Determining the start node
// is complicated so it has been omitted from the loop construct
for ($i = 0, $size = count($tokens) ; $i < $size; ) {
-
+
//################################################################//
// Gather information on children
-
+
// child token accumulator
$child_tokens = array();
-
+
// scroll to the end of this node, report number, and collect
// all children
for ($j = $i, $depth = 0; ; $j++) {
- if ($tokens[$j]->type == 'start') {
+ if ($tokens[$j] instanceof HTMLPurifier_Token_Start) {
$depth++;
// skip token assignment on first iteration, this is the
// token we currently are on
if ($depth == 1) continue;
- } elseif ($tokens[$j]->type == 'end') {
+ } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) {
$depth--;
// skip token assignment on last iteration, this is the
// end token of the token we're currently on
@@ -104,15 +101,15 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
}
$child_tokens[] = $tokens[$j];
}
-
+
// $i is index of start token
// $j is index of end token
-
+
$start_token = $tokens[$i]; // to make token available via CurrentToken
-
+
//################################################################//
// Gather information on parent
-
+
// calculate parent information
if ($count = count($stack)) {
$parent_index = $stack[$count-1];
@@ -125,11 +122,11 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
} else {
// processing as if the parent were the "root" node
// unknown info, it won't be used anyway, in the future,
- // we may want to enforce one element only (this is
+ // we may want to enforce one element only (this is
// necessary for HTML Purifier to clean entire documents
$parent_index = $parent_name = $parent_def = null;
}
-
+
// calculate context
if ($is_inline === false) {
// check if conditions make it inline
@@ -142,10 +139,10 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
$is_inline = false;
}
}
-
+
//################################################################//
// Determine whether element is explicitly excluded SGML-style
-
+
// determine whether or not element is excluded by checking all
// parent exclusions. The array should not be very large, two
// elements at most.
@@ -159,10 +156,10 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
}
}
}
-
+
//################################################################//
// Perform child validation
-
+
if ($excluded) {
// there is an exclusion, remove the entire node
$result = false;
@@ -174,9 +171,9 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
$def = $definition->info_parent_def;
} else {
$def = $definition->info[$tokens[$i]->name];
-
+
}
-
+
if (!empty($def->child)) {
// have DTD child def validate children
$result = $def->child->validateChildren(
@@ -185,31 +182,31 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
// weird, no child definition, get rid of everything
$result = false;
}
-
+
// determine whether or not this element has any exclusions
$excludes = $def->excludes;
}
-
+
// $result is now a bool or array
-
+
//################################################################//
// Process result by interpreting $result
-
+
if ($result === true || $child_tokens === $result) {
// leave the node as is
-
+
// register start token as a parental node start
$stack[] = $i;
-
+
// register exclusions if there are any
if (!empty($excludes)) $exclude_stack[] = $excludes;
-
+
// move cursor to next possible start node
$i++;
-
+
} elseif($result === false) {
// remove entire node
-
+
if ($e) {
if ($excluded) {
$e->send(E_ERROR, 'Strategy_FixNesting: Node excluded');
@@ -217,20 +214,20 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
$e->send(E_ERROR, 'Strategy_FixNesting: Node removed');
}
}
-
+
// calculate length of inner tokens and current tokens
$length = $j - $i + 1;
-
+
// perform removal
array_splice($tokens, $i, $length);
-
+
// update size
$size -= $length;
-
+
// there is no start token to register,
// current node is now the next possible start node
// unless it turns out that we need to do a double-check
-
+
// this is a rought heuristic that covers 100% of HTML's
// cases and 99% of all other cases. A child definition
// that would be tricked by this would be something like:
@@ -242,16 +239,16 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
$i = $parent_index;
array_pop($stack);
}
-
+
// PROJECTED OPTIMIZATION: Process all children elements before
// reprocessing parent node.
-
+
} else {
// replace node with $result
-
+
// calculate length of inner tokens
$length = $j - $i - 1;
-
+
if ($e) {
if (empty($result) && $length) {
$e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed');
@@ -259,36 +256,36 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
$e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized');
}
}
-
+
// perform replacement
array_splice($tokens, $i + 1, $length, $result);
-
+
// update size
$size -= $length;
$size += count($result);
-
+
// register start token as a parental node start
$stack[] = $i;
-
+
// register exclusions if there are any
if (!empty($excludes)) $exclude_stack[] = $excludes;
-
+
// move cursor to next possible start node
$i++;
-
+
}
-
+
//################################################################//
// Scroll to next start node
-
+
// We assume, at this point, that $i is the index of the token
// that is the first possible new start point for a node.
-
+
// Test if the token indeed is a start tag, if not, move forward
// and test again.
$size = count($tokens);
- while ($i < $size and $tokens[$i]->type != 'start') {
- if ($tokens[$i]->type == 'end') {
+ while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) {
+ if ($tokens[$i] instanceof HTMLPurifier_Token_End) {
// pop a token index off the stack if we ended a node
array_pop($stack);
// pop an exclusion lookup off exclusion stack if
@@ -305,27 +302,27 @@ class HTMLPurifier_Strategy_FixNesting extends HTMLPurifier_Strategy
}
$i++;
}
-
+
}
-
+
//####################################################################//
// Post-processing
-
+
// remove implicit parent tokens at the beginning and end
array_shift($tokens);
array_pop($tokens);
-
+
// remove context variables
$context->destroy('IsInline');
$context->destroy('CurrentToken');
-
+
//####################################################################//
// Return
-
+
return $tokens;
-
+
}
-
+
}
-
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php b/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php
index 30208ba1479..316b2386ac8 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php
@@ -1,316 +1,471 @@
- This directive can be used to add custom auto-format injectors.
- Specify an array of injector names (class name minus the prefix)
- or concrete implementations. Injector class must exist. This directive
- has been available since 2.0.1.
-
-'
-);
-
/**
* Takes tokens makes them well-formed (balance end tags, etc.)
*/
class HTMLPurifier_Strategy_MakeWellFormed extends HTMLPurifier_Strategy
{
-
+
/**
- * Locally shared variable references
- * @private
+ * Array stream of tokens being processed.
*/
- var $inputTokens, $inputIndex, $outputTokens, $currentNesting,
- $currentInjector, $injectors;
-
- function execute($tokens, $config, &$context) {
-
+ protected $tokens;
+
+ /**
+ * Current index in $tokens.
+ */
+ protected $t;
+
+ /**
+ * Current nesting of elements.
+ */
+ protected $stack;
+
+ /**
+ * Injectors active in this stream processing.
+ */
+ protected $injectors;
+
+ /**
+ * Current instance of HTMLPurifier_Config.
+ */
+ protected $config;
+
+ /**
+ * Current instance of HTMLPurifier_Context.
+ */
+ protected $context;
+
+ public function execute($tokens, $config, $context) {
+
$definition = $config->getHTMLDefinition();
-
+
// local variables
- $result = array();
- $generator = new HTMLPurifier_Generator();
- $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags');
- $e =& $context->get('ErrorCollector', true);
-
+ $generator = new HTMLPurifier_Generator($config, $context);
+ $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
+ $e = $context->get('ErrorCollector', true);
+ $t = false; // token index
+ $i = false; // injector index
+ $token = false; // the current token
+ $reprocess = false; // whether or not to reprocess the same token
+ $stack = array();
+
// member variables
- $this->currentNesting = array();
- $this->inputIndex = false;
- $this->inputTokens =& $tokens;
- $this->outputTokens =& $result;
-
+ $this->stack =& $stack;
+ $this->t =& $t;
+ $this->tokens =& $tokens;
+ $this->config = $config;
+ $this->context = $context;
+
// context variables
- $context->register('CurrentNesting', $this->currentNesting);
- $context->register('InputIndex', $this->inputIndex);
- $context->register('InputTokens', $tokens);
-
+ $context->register('CurrentNesting', $stack);
+ $context->register('InputIndex', $t);
+ $context->register('InputTokens', $tokens);
+ $context->register('CurrentToken', $token);
+
// -- begin INJECTOR --
-
+
$this->injectors = array();
-
+
$injectors = $config->getBatch('AutoFormat');
+ $def_injectors = $definition->info_injector;
$custom_injectors = $injectors['Custom'];
unset($injectors['Custom']); // special case
foreach ($injectors as $injector => $b) {
+ // XXX: Fix with a legitimate lookup table of enabled filters
+ if (strpos($injector, '.') !== false) continue;
$injector = "HTMLPurifier_Injector_$injector";
if (!$b) continue;
$this->injectors[] = new $injector;
}
+ foreach ($def_injectors as $injector) {
+ // assumed to be objects
+ $this->injectors[] = $injector;
+ }
foreach ($custom_injectors as $injector) {
+ if (!$injector) continue;
if (is_string($injector)) {
$injector = "HTMLPurifier_Injector_$injector";
$injector = new $injector;
}
$this->injectors[] = $injector;
}
-
- // array index of the injector that resulted in an array
- // substitution. This enables processTokens() to know which
- // injectors are affected by the added tokens and which are
- // not (namely, the ones after the current injector are not
- // affected)
- $this->currentInjector = false;
-
+
// give the injectors references to the definition and context
// variables for performance reasons
- foreach ($this->injectors as $i => $x) {
- $error = $this->injectors[$i]->prepare($config, $context);
+ foreach ($this->injectors as $ix => $injector) {
+ $error = $injector->prepare($config, $context);
if (!$error) continue;
- list($injector) = array_splice($this->injectors, $i, 1);
- $name = $injector->name;
- trigger_error("Cannot enable $name injector because $error is not allowed", E_USER_WARNING);
+ array_splice($this->injectors, $ix, 1); // rm the injector
+ trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING);
}
-
- // warning: most foreach loops follow the convention $i => $x.
- // be sure, for PHP4 compatibility, to only perform write operations
- // directly referencing the object using $i: $x is only safe for reads
-
+
// -- end INJECTOR --
-
- $token = false;
- $context->register('CurrentToken', $token);
-
- for ($this->inputIndex = 0; isset($tokens[$this->inputIndex]); $this->inputIndex++) {
-
- // if all goes well, this token will be passed through unharmed
- $token = $tokens[$this->inputIndex];
-
- //printTokens($tokens, $this->inputIndex);
-
- foreach ($this->injectors as $i => $x) {
- if ($x->skip > 0) $this->injectors[$i]->skip--;
- }
-
- // quick-check: if it's not a tag, no need to process
- if (empty( $token->is_tag )) {
- if ($token->type === 'text') {
- // injector handler code; duplicated for performance reasons
- foreach ($this->injectors as $i => $x) {
- if (!$x->skip) $this->injectors[$i]->handleText($token);
- if (is_array($token)) {
- $this->currentInjector = $i;
- break;
- }
- }
+
+ // a note on punting:
+ // In order to reduce code duplication, whenever some code needs
+ // to make HTML changes in order to make things "correct", the
+ // new HTML gets sent through the purifier, regardless of its
+ // status. This means that if we add a start token, because it
+ // was totally necessary, we don't have to update nesting; we just
+ // punt ($reprocess = true; continue;) and it does that for us.
+
+ // isset is in loop because $tokens size changes during loop exec
+ for (
+ $t = 0;
+ $t == 0 || isset($tokens[$t - 1]);
+ // only increment if we don't need to reprocess
+ $reprocess ? $reprocess = false : $t++
+ ) {
+
+ // check for a rewind
+ if (is_int($i) && $i >= 0) {
+ // possibility: disable rewinding if the current token has a
+ // rewind set on it already. This would offer protection from
+ // infinite loop, but might hinder some advanced rewinding.
+ $rewind_to = $this->injectors[$i]->getRewind();
+ if (is_int($rewind_to) && $rewind_to < $t) {
+ if ($rewind_to < 0) $rewind_to = 0;
+ while ($t > $rewind_to) {
+ $t--;
+ $prev = $tokens[$t];
+ // indicate that other injectors should not process this token,
+ // but we need to reprocess it
+ unset($prev->skip[$i]);
+ $prev->rewind = $i;
+ if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack);
+ elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start;
+ }
}
- $this->processToken($token, $config, $context);
+ $i = false;
+ }
+
+ // handle case of document end
+ if (!isset($tokens[$t])) {
+ // kill processing if stack is empty
+ if (empty($this->stack)) break;
+
+ // peek
+ $top_nesting = array_pop($this->stack);
+ $this->stack[] = $top_nesting;
+
+ // send error
+ if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting);
+ }
+
+ // append, don't splice, since this is the end
+ $tokens[] = new HTMLPurifier_Token_End($top_nesting->name);
+
+ // punt!
+ $reprocess = true;
continue;
}
-
- $info = $definition->info[$token->name]->child;
-
- // quick tag checks: anything that's *not* an end tag
- $ok = false;
- if ($info->type == 'empty' && $token->type == 'start') {
- // test if it claims to be a start tag but is empty
- $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
- $ok = true;
- } elseif ($info->type != 'empty' && $token->type == 'empty' ) {
- // claims to be empty but really is a start tag
- $token = array(
- new HTMLPurifier_Token_Start($token->name, $token->attr),
- new HTMLPurifier_Token_End($token->name)
- );
- $ok = true;
- } elseif ($token->type == 'empty') {
- // real empty token
- $ok = true;
- } elseif ($token->type == 'start') {
- // start tag
-
- // ...unless they also have to close their parent
- if (!empty($this->currentNesting)) {
-
- $parent = array_pop($this->currentNesting);
- $parent_info = $definition->info[$parent->name];
-
- // this can be replaced with a more general algorithm:
- // if the token is not allowed by the parent, auto-close
- // the parent
- if (!isset($parent_info->child->elements[$token->name])) {
- if ($e) $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
- // close the parent, then re-loop to reprocess token
- $result[] = new HTMLPurifier_Token_End($parent->name);
- $this->inputIndex--;
- continue;
- }
-
- $this->currentNesting[] = $parent; // undo the pop
- }
- $ok = true;
- }
-
- // injector handler code; duplicated for performance reasons
- if ($ok) {
- foreach ($this->injectors as $i => $x) {
- if (!$x->skip) $this->injectors[$i]->handleElement($token);
- if (is_array($token)) {
- $this->currentInjector = $i;
+
+ $token = $tokens[$t];
+
+ //echo '
'; printTokens($tokens, $t); printTokens($this->stack);
+
+ // quick-check: if it's not a tag, no need to process
+ if (empty($token->is_tag)) {
+ if ($token instanceof HTMLPurifier_Token_Text) {
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) continue;
+ if ($token->rewind !== null && $token->rewind !== $i) continue;
+ $injector->handleText($token);
+ $this->processToken($token, $i);
+ $reprocess = true;
break;
}
}
- $this->processToken($token, $config, $context);
+ // another possibility is a comment
continue;
}
-
+
+ if (isset($definition->info[$token->name])) {
+ $type = $definition->info[$token->name]->child->type;
+ } else {
+ $type = false; // Type is unknown, treat accordingly
+ }
+
+ // quick tag checks: anything that's *not* an end tag
+ $ok = false;
+ if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) {
+ // claims to be a start tag but is empty
+ $token = new HTMLPurifier_Token_Empty($token->name, $token->attr);
+ $ok = true;
+ } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) {
+ // claims to be empty but really is a start tag
+ $this->swap(new HTMLPurifier_Token_End($token->name));
+ $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr));
+ // punt (since we had to modify the input stream in a non-trivial way)
+ $reprocess = true;
+ continue;
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
+ // real empty token
+ $ok = true;
+ } elseif ($token instanceof HTMLPurifier_Token_Start) {
+ // start tag
+
+ // ...unless they also have to close their parent
+ if (!empty($this->stack)) {
+
+ $parent = array_pop($this->stack);
+ $this->stack[] = $parent;
+
+ if (isset($definition->info[$parent->name])) {
+ $elements = $definition->info[$parent->name]->child->getAllowedElements($config);
+ $autoclose = !isset($elements[$token->name]);
+ } else {
+ $autoclose = false;
+ }
+
+ if ($autoclose && $definition->info[$token->name]->wrap) {
+ // check if this is actually a wrap (mmm wraps!)
+ $wrapname = $definition->info[$token->name]->wrap;
+ $wrapdef = $definition->info[$wrapname];
+ $elements = $wrapdef->child->getAllowedElements($config);
+ if (isset($elements[$token->name])) {
+ $newtoken = new HTMLPurifier_Token_Start($wrapname);
+ $this->insertBefore($newtoken);
+ $reprocess = true;
+ continue;
+ }
+ }
+
+ $carryover = false;
+ if ($autoclose && $definition->info[$parent->name]->formatting) {
+ $carryover = true;
+ }
+
+ if ($autoclose) {
+ // errors need to be updated
+ $new_token = new HTMLPurifier_Token_End($parent->name);
+ $new_token->start = $parent;
+ if ($carryover) {
+ $element = clone $parent;
+ $element->armor['MakeWellFormed_TagClosedError'] = true;
+ $element->carryover = true;
+ $this->processToken(array($new_token, $token, $element));
+ } else {
+ $this->insertBefore($new_token);
+ }
+ if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) {
+ if (!$carryover) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent);
+ } else {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent);
+ }
+ }
+ $reprocess = true;
+ continue;
+ }
+
+ }
+ $ok = true;
+ }
+
+ if ($ok) {
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) continue;
+ if ($token->rewind !== null && $token->rewind !== $i) continue;
+ $injector->handleElement($token);
+ $this->processToken($token, $i);
+ $reprocess = true;
+ break;
+ }
+ if (!$reprocess) {
+ // ah, nothing interesting happened; do normal processing
+ $this->swap($token);
+ if ($token instanceof HTMLPurifier_Token_Start) {
+ $this->stack[] = $token;
+ } elseif ($token instanceof HTMLPurifier_Token_End) {
+ throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed');
+ }
+ }
+ continue;
+ }
+
// sanity check: we should be dealing with a closing tag
- if ($token->type != 'end') continue;
-
+ if (!$token instanceof HTMLPurifier_Token_End) {
+ throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier');
+ }
+
// make sure that we have something open
- if (empty($this->currentNesting)) {
+ if (empty($this->stack)) {
if ($escape_invalid_tags) {
if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text');
- $result[] = new HTMLPurifier_Token_Text(
- $generator->generateFromToken($token, $config, $context)
- );
- } elseif ($e) {
- $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
+ $this->swap(new HTMLPurifier_Token_Text(
+ $generator->generateFromToken($token)
+ ));
+ } else {
+ $this->remove();
+ if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed');
}
+ $reprocess = true;
continue;
}
-
- // first, check for the simplest case: everything closes neatly
- $current_parent = array_pop($this->currentNesting);
+
+ // first, check for the simplest case: everything closes neatly.
+ // Eventually, everything passes through here; if there are problems
+ // we modify the input stream accordingly and then punt, so that
+ // the tokens get processed again.
+ $current_parent = array_pop($this->stack);
if ($current_parent->name == $token->name) {
- $result[] = $token;
- foreach ($this->injectors as $i => $x) {
- $this->injectors[$i]->notifyEnd($token);
+ $token->start = $current_parent;
+ foreach ($this->injectors as $i => $injector) {
+ if (isset($token->skip[$i])) continue;
+ if ($token->rewind !== null && $token->rewind !== $i) continue;
+ $injector->handleEnd($token);
+ $this->processToken($token, $i);
+ $this->stack[] = $current_parent;
+ $reprocess = true;
+ break;
}
continue;
}
-
+
// okay, so we're trying to close the wrong tag
-
+
// undo the pop previous pop
- $this->currentNesting[] = $current_parent;
-
+ $this->stack[] = $current_parent;
+
// scroll back the entire nest, trying to find our tag.
// (feature could be to specify how far you'd like to go)
- $size = count($this->currentNesting);
+ $size = count($this->stack);
// -2 because -1 is the last element, but we already checked that
$skipped_tags = false;
- for ($i = $size - 2; $i >= 0; $i--) {
- if ($this->currentNesting[$i]->name == $token->name) {
- // current nesting is modified
- $skipped_tags = array_splice($this->currentNesting, $i);
+ for ($j = $size - 2; $j >= 0; $j--) {
+ if ($this->stack[$j]->name == $token->name) {
+ $skipped_tags = array_slice($this->stack, $j);
break;
}
}
-
- // we still didn't find the tag, so remove
+
+ // we didn't find the tag, so remove
if ($skipped_tags === false) {
if ($escape_invalid_tags) {
- $result[] = new HTMLPurifier_Token_Text(
- $generator->generateFromToken($token, $config, $context)
- );
+ $this->swap(new HTMLPurifier_Token_Text(
+ $generator->generateFromToken($token)
+ ));
if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text');
- } elseif ($e) {
- $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
+ } else {
+ $this->remove();
+ if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed');
}
+ $reprocess = true;
continue;
}
-
- // okay, we found it, close all the skipped tags
- // note that skipped tags contains the element we need closed
- for ($i = count($skipped_tags) - 1; $i >= 0; $i--) {
- if ($i && $e && !isset($skipped_tags[$i]->armor['MakeWellFormed_TagClosedError'])) {
- $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$i]);
- }
- $result[] = $new_token = new HTMLPurifier_Token_End($skipped_tags[$i]->name);
- foreach ($this->injectors as $j => $x) { // $j, not $i!!!
- $this->injectors[$j]->notifyEnd($new_token);
+
+ // do errors, in REVERSE $j order: a,b,c with
+ $c = count($skipped_tags);
+ if ($e) {
+ for ($j = $c - 1; $j > 0; $j--) {
+ // notice we exclude $j == 0, i.e. the current ending tag, from
+ // the errors...
+ if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) {
+ $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]);
+ }
}
}
-
+
+ // insert tags, in FORWARD $j order: c,b,a with
+ $replace = array($token);
+ for ($j = 1; $j < $c; $j++) {
+ // ...as well as from the insertions
+ $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name);
+ $new_token->start = $skipped_tags[$j];
+ array_unshift($replace, $new_token);
+ if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) {
+ $element = clone $skipped_tags[$j];
+ $element->carryover = true;
+ $element->armor['MakeWellFormed_TagClosedError'] = true;
+ $replace[] = $element;
+ }
+ }
+ $this->processToken($replace);
+ $reprocess = true;
+ continue;
}
-
+
$context->destroy('CurrentNesting');
$context->destroy('InputTokens');
$context->destroy('InputIndex');
$context->destroy('CurrentToken');
-
- // we're at the end now, fix all still unclosed tags (this is
- // duplicated from the end of the loop with some slight modifications)
- // not using $skipped_tags since it would invariably be all of them
- if (!empty($this->currentNesting)) {
- for ($i = count($this->currentNesting) - 1; $i >= 0; $i--) {
- if ($e && !isset($this->currentNesting[$i]->armor['MakeWellFormed_TagClosedError'])) {
- $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $this->currentNesting[$i]);
- }
- $result[] = $new_token = new HTMLPurifier_Token_End($this->currentNesting[$i]->name);
- foreach ($this->injectors as $j => $x) { // $j, not $i!!!
- $this->injectors[$j]->notifyEnd($new_token);
- }
+
+ unset($this->injectors, $this->stack, $this->tokens, $this->t);
+ return $tokens;
+ }
+
+ /**
+ * Processes arbitrary token values for complicated substitution patterns.
+ * In general:
+ *
+ * If $token is an array, it is a list of tokens to substitute for the
+ * current token. These tokens then get individually processed. If there
+ * is a leading integer in the list, that integer determines how many
+ * tokens from the stream should be removed.
+ *
+ * If $token is a regular token, it is swapped with the current token.
+ *
+ * If $token is false, the current token is deleted.
+ *
+ * If $token is an integer, that number of tokens (with the first token
+ * being the current one) will be deleted.
+ *
+ * @param $token Token substitution value
+ * @param $injector Injector that performed the substitution; default is if
+ * this is not an injector related operation.
+ */
+ protected function processToken($token, $injector = -1) {
+
+ // normalize forms of token
+ if (is_object($token)) $token = array(1, $token);
+ if (is_int($token)) $token = array($token);
+ if ($token === false) $token = array(1);
+ if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector');
+ if (!is_int($token[0])) array_unshift($token, 1);
+ if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid');
+
+ // $token is now an array with the following form:
+ // array(number nodes to delete, new node 1, new node 2, ...)
+
+ $delete = array_shift($token);
+ $old = array_splice($this->tokens, $this->t, $delete, $token);
+
+ if ($injector > -1) {
+ // determine appropriate skips
+ $oldskip = isset($old[0]) ? $old[0]->skip : array();
+ foreach ($token as $object) {
+ $object->skip = $oldskip;
+ $object->skip[$injector] = true;
}
}
-
- unset($this->outputTokens, $this->injectors, $this->currentInjector,
- $this->currentNesting, $this->inputTokens, $this->inputIndex);
-
- return $result;
+
}
-
- function processToken($token, $config, &$context) {
- if (is_array($token)) {
- // the original token was overloaded by an injector, time
- // to some fancy acrobatics
-
- // $this->inputIndex is decremented so that the entire set gets
- // re-processed
- array_splice($this->inputTokens, $this->inputIndex--, 1, $token);
-
- // adjust the injector skips based on the array substitution
- if ($this->injectors) {
- $offset = count($token);
- for ($i = 0; $i <= $this->currentInjector; $i++) {
- // because of the skip back, we need to add one more
- // for uninitialized injectors. I'm not exactly
- // sure why this is the case, but I think it has to
- // do with the fact that we're decrementing skips
- // before re-checking text
- if (!$this->injectors[$i]->skip) $this->injectors[$i]->skip++;
- $this->injectors[$i]->skip += $offset;
- }
- }
- } elseif ($token) {
- // regular case
- $this->outputTokens[] = $token;
- if ($token->type == 'start') {
- $this->currentNesting[] = $token;
- } elseif ($token->type == 'end') {
- array_pop($this->currentNesting); // not actually used
- }
- }
+
+ /**
+ * Inserts a token before the current token. Cursor now points to this token
+ */
+ private function insertBefore($token) {
+ array_splice($this->tokens, $this->t, 0, array($token));
}
-
+
+ /**
+ * Removes current token. Cursor now points to new token occupying previously
+ * occupied space.
+ */
+ private function remove() {
+ array_splice($this->tokens, $this->t, 1);
+ }
+
+ /**
+ * Swap current token with new token. Cursor points to new token (no change).
+ */
+ private function swap($token) {
+ $this->tokens[$this->t] = $token;
+ }
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php b/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php
index 5d26e4f5705..cf3a33e406e 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php
@@ -1,52 +1,8 @@
- This directive enables pre-emptive URI checking in
img
- tags, as the attribute validation strategy is not authorized to
- remove elements from the document. This directive has been available
- since 1.3.0, revert to pre-1.3.0 behavior by setting to false.
-
-'
-);
-
-HTMLPurifier_ConfigSchema::define(
- 'Core', 'RemoveScriptContents', null, 'bool/null', '
-
- This directive enables HTML Purifier to remove not only script tags
- but all of their contents. This directive has been deprecated since 2.1.0,
- and when not set the value of %Core.HiddenElements will take
- precedence. This directive has been available since 2.0.0, and can be used to
- revert to pre-2.0.0 behavior by setting it to false.
-
-'
-);
-
-HTMLPurifier_ConfigSchema::define(
- 'Core', 'HiddenElements', array('script' => true, 'style' => true), 'lookup', '
-
- This directive is a lookup array of elements which should have their
- contents removed when they are not allowed by the HTML definition.
- For example, the contents of a script tag are not
- normally shown in a document, so if script tags are to be removed,
- their contents should be removed to. This is opposed to a b
- tag, which defines some presentational changes but does not hide its
- contents.
-
-'
-);
-
/**
* Removes all unrecognized tags from the list of tokens.
- *
+ *
* This strategy iterates through all the tokens and removes unrecognized
* tokens. If a token is not recognized but a TagTransform is defined for
* that element, the element will be transformed accordingly.
@@ -54,41 +10,44 @@ HTMLPurifier_ConfigSchema::define(
class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
{
-
- function execute($tokens, $config, &$context) {
+
+ public function execute($tokens, $config, $context) {
$definition = $config->getHTMLDefinition();
- $generator = new HTMLPurifier_Generator();
+ $generator = new HTMLPurifier_Generator($config, $context);
$result = array();
-
- $escape_invalid_tags = $config->get('Core', 'EscapeInvalidTags');
- $remove_invalid_img = $config->get('Core', 'RemoveInvalidImg');
-
- $remove_script_contents = $config->get('Core', 'RemoveScriptContents');
- $hidden_elements = $config->get('Core', 'HiddenElements');
-
+
+ $escape_invalid_tags = $config->get('Core.EscapeInvalidTags');
+ $remove_invalid_img = $config->get('Core.RemoveInvalidImg');
+
+ // currently only used to determine if comments should be kept
+ $trusted = $config->get('HTML.Trusted');
+
+ $remove_script_contents = $config->get('Core.RemoveScriptContents');
+ $hidden_elements = $config->get('Core.HiddenElements');
+
// remove script contents compatibility
if ($remove_script_contents === true) {
$hidden_elements['script'] = true;
} elseif ($remove_script_contents === false && isset($hidden_elements['script'])) {
unset($hidden_elements['script']);
}
-
+
$attr_validator = new HTMLPurifier_AttrValidator();
-
+
// removes tokens until it reaches a closing tag with its value
$remove_until = false;
-
+
// converts comments into text tokens when this is equal to a tag name
$textify_comments = false;
-
+
$token = false;
$context->register('CurrentToken', $token);
-
+
$e = false;
- if ($config->get('Core', 'CollectErrors')) {
+ if ($config->get('Core.CollectErrors')) {
$e =& $context->get('ErrorCollector');
}
-
+
foreach($tokens as $token) {
if ($remove_until) {
if (empty($token->is_tag) || $token->name !== $remove_until) {
@@ -97,7 +56,7 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
}
if (!empty( $token->is_tag )) {
// DEFINITION CALL
-
+
// before any processing, try to transform the element
if (
isset($definition->info_tag_transform[$token->name])
@@ -110,13 +69,13 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
transform($token, $config, $context);
if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name);
}
-
+
if (isset($definition->info[$token->name])) {
-
+
// mostly everything's good, but
// we need to make sure required attributes are in order
if (
- ($token->type === 'start' || $token->type === 'empty') &&
+ ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) &&
$definition->info[$token->name]->required_attr &&
($token->name != 'img' || $remove_invalid_img) // ensure config option still works
) {
@@ -134,26 +93,26 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
}
$token->armor['ValidateAttributes'] = true;
}
-
- if (isset($hidden_elements[$token->name]) && $token->type == 'start') {
+
+ if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) {
$textify_comments = $token->name;
- } elseif ($token->name === $textify_comments && $token->type == 'end') {
+ } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) {
$textify_comments = false;
}
-
+
} elseif ($escape_invalid_tags) {
// invalid tag, generate HTML representation and insert in
if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text');
$token = new HTMLPurifier_Token_Text(
- $generator->generateFromToken($token, $config, $context)
+ $generator->generateFromToken($token)
);
} else {
// check if we need to destroy all of the tag's children
// CAN BE GENERICIZED
if (isset($hidden_elements[$token->name])) {
- if ($token->type == 'start') {
+ if ($token instanceof HTMLPurifier_Token_Start) {
$remove_until = $token->name;
- } elseif ($token->type == 'empty') {
+ } elseif ($token instanceof HTMLPurifier_Token_Empty) {
// do nothing: we're still looking
} else {
$remove_until = false;
@@ -164,17 +123,34 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
}
continue;
}
- } elseif ($token->type == 'comment') {
+ } elseif ($token instanceof HTMLPurifier_Token_Comment) {
// textify comments in script tags when they are allowed
if ($textify_comments !== false) {
$data = $token->data;
$token = new HTMLPurifier_Token_Text($data);
+ } elseif ($trusted) {
+ // keep, but perform comment cleaning
+ if ($e) {
+ // perform check whether or not there's a trailing hyphen
+ if (substr($token->data, -1) == '-') {
+ $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed');
+ }
+ }
+ $token->data = rtrim($token->data, '-');
+ $found_double_hyphen = false;
+ while (strpos($token->data, '--') !== false) {
+ if ($e && !$found_double_hyphen) {
+ $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed');
+ }
+ $found_double_hyphen = true; // prevent double-erroring
+ $token->data = str_replace('--', '-', $token->data);
+ }
} else {
// strip comments
if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed');
continue;
}
- } elseif ($token->type == 'text') {
+ } elseif ($token instanceof HTMLPurifier_Token_Text) {
} else {
continue;
}
@@ -184,11 +160,12 @@ class HTMLPurifier_Strategy_RemoveForeignElements extends HTMLPurifier_Strategy
// we removed tokens until the end, throw error
$e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until);
}
-
+
$context->destroy('CurrentToken');
-
+
return $result;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php b/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php
index 6debcc336bd..c3328a9d441 100644
--- a/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php
+++ b/lib/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php
@@ -1,44 +1,39 @@
register('CurrentToken', $token);
-
+
foreach ($tokens as $key => $token) {
-
+
// only process tokens that have attributes,
// namely start and empty tags
- if ($token->type !== 'start' && $token->type !== 'empty') continue;
-
+ if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue;
+
// skip tokens that are armored
if (!empty($token->armor['ValidateAttributes'])) continue;
-
+
// note that we have no facilities here for removing tokens
$validator->validateToken($token, $config, $context);
-
+
$tokens[$key] = $token; // for PHP 4
}
$context->destroy('CurrentToken');
-
+
return $tokens;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/StringHash.php b/lib/htmlpurifier/HTMLPurifier/StringHash.php
new file mode 100644
index 00000000000..62085c5c2ff
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/StringHash.php
@@ -0,0 +1,39 @@
+accessed[$index] = true;
+ return parent::offsetGet($index);
+ }
+
+ /**
+ * Returns a lookup array of all array indexes that have been accessed.
+ * @return Array in form array($index => true).
+ */
+ public function getAccessed() {
+ return $this->accessed;
+ }
+
+ /**
+ * Resets the access array.
+ */
+ public function resetAccessed() {
+ $this->accessed = array();
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/StringHashParser.php b/lib/htmlpurifier/HTMLPurifier/StringHashParser.php
new file mode 100644
index 00000000000..f3e70c712f0
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/StringHashParser.php
@@ -0,0 +1,110 @@
+ 'DefaultKeyValue',
+ * 'KEY' => 'Value',
+ * 'KEY2' => 'Value2',
+ * 'MULTILINE-KEY' => "Multiline\nvalue.\n",
+ * )
+ *
+ * We use this as an easy to use file-format for configuration schema
+ * files, but the class itself is usage agnostic.
+ *
+ * You can use ---- to forcibly terminate parsing of a single string-hash;
+ * this marker is used in multi string-hashes to delimit boundaries.
+ */
+class HTMLPurifier_StringHashParser
+{
+
+ public $default = 'ID';
+
+ /**
+ * Parses a file that contains a single string-hash.
+ */
+ public function parseFile($file) {
+ if (!file_exists($file)) return false;
+ $fh = fopen($file, 'r');
+ if (!$fh) return false;
+ $ret = $this->parseHandle($fh);
+ fclose($fh);
+ return $ret;
+ }
+
+ /**
+ * Parses a file that contains multiple string-hashes delimited by '----'
+ */
+ public function parseMultiFile($file) {
+ if (!file_exists($file)) return false;
+ $ret = array();
+ $fh = fopen($file, 'r');
+ if (!$fh) return false;
+ while (!feof($fh)) {
+ $ret[] = $this->parseHandle($fh);
+ }
+ fclose($fh);
+ return $ret;
+ }
+
+ /**
+ * Internal parser that acepts a file handle.
+ * @note While it's possible to simulate in-memory parsing by using
+ * custom stream wrappers, if such a use-case arises we should
+ * factor out the file handle into its own class.
+ * @param $fh File handle with pointer at start of valid string-hash
+ * block.
+ */
+ protected function parseHandle($fh) {
+ $state = false;
+ $single = false;
+ $ret = array();
+ do {
+ $line = fgets($fh);
+ if ($line === false) break;
+ $line = rtrim($line, "\n\r");
+ if (!$state && $line === '') continue;
+ if ($line === '----') break;
+ if (strncmp('--#', $line, 3) === 0) {
+ // Comment
+ continue;
+ } elseif (strncmp('--', $line, 2) === 0) {
+ // Multiline declaration
+ $state = trim($line, '- ');
+ if (!isset($ret[$state])) $ret[$state] = '';
+ continue;
+ } elseif (!$state) {
+ $single = true;
+ if (strpos($line, ':') !== false) {
+ // Single-line declaration
+ list($state, $line) = explode(':', $line, 2);
+ $line = trim($line);
+ } else {
+ // Use default declaration
+ $state = $this->default;
+ }
+ }
+ if ($single) {
+ $ret[$state] = $line;
+ $single = false;
+ $state = false;
+ } else {
+ $ret[$state] .= "$line\n";
+ }
+ } while (!feof($fh));
+ return $ret;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/TagTransform.php b/lib/htmlpurifier/HTMLPurifier/TagTransform.php
index f5de99ce49c..210a4472176 100644
--- a/lib/htmlpurifier/HTMLPurifier/TagTransform.php
+++ b/lib/htmlpurifier/HTMLPurifier/TagTransform.php
@@ -1,29 +1,24 @@
'xx-small',
'1' => 'xx-small',
'2' => 'small',
@@ -35,30 +36,30 @@ class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
'+3' => '200%',
'+4' => '300%'
);
-
- function transform($tag, $config, &$context) {
-
- if ($tag->type == 'end') {
- $new_tag = $tag->copy();
+
+ public function transform($tag, $config, $context) {
+
+ if ($tag instanceof HTMLPurifier_Token_End) {
+ $new_tag = clone $tag;
$new_tag->name = $this->transform_to;
return $new_tag;
}
-
+
$attr = $tag->attr;
$prepend_style = '';
-
+
// handle color transform
if (isset($attr['color'])) {
$prepend_style .= 'color:' . $attr['color'] . ';';
unset($attr['color']);
}
-
+
// handle face transform
if (isset($attr['face'])) {
$prepend_style .= 'font-family:' . $attr['face'] . ';';
unset($attr['face']);
}
-
+
// handle size transform
if (isset($attr['size'])) {
// normalize large numbers
@@ -76,19 +77,20 @@ class HTMLPurifier_TagTransform_Font extends HTMLPurifier_TagTransform
}
unset($attr['size']);
}
-
+
if ($prepend_style) {
$attr['style'] = isset($attr['style']) ?
$prepend_style . $attr['style'] :
$prepend_style;
}
-
- $new_tag = $tag->copy();
+
+ $new_tag = clone $tag;
$new_tag->name = $this->transform_to;
$new_tag->attr = $attr;
-
+
return $new_tag;
-
+
}
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php b/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php
index 0b5a84d480d..0e36130f253 100644
--- a/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php
+++ b/lib/htmlpurifier/HTMLPurifier/TagTransform/Simple.php
@@ -1,7 +1,5 @@
transform_to = $transform_to;
$this->style = $style;
}
-
- function transform($tag, $config, &$context) {
- $new_tag = $tag->copy();
+
+ public function transform($tag, $config, $context) {
+ $new_tag = clone $tag;
$new_tag->name = $this->transform_to;
if (!is_null($this->style) &&
- ($new_tag->type == 'start' || $new_tag->type == 'empty')
+ ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty)
) {
$this->prependCSS($new_tag->attr, $this->style);
}
return $new_tag;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Token.php b/lib/htmlpurifier/HTMLPurifier/Token.php
index bd859d7f470..7900e6cb101 100644
--- a/lib/htmlpurifier/HTMLPurifier/Token.php
+++ b/lib/htmlpurifier/HTMLPurifier/Token.php
@@ -1,163 +1,57 @@
is_a(). @public */
- var $line; /**< Line number node was on in source document. Null if unknown. @public */
-
+ public $line; /**< Line number node was on in source document. Null if unknown. */
+ public $col; /**< Column of line node was on in source document. Null if unknown. */
+
/**
* Lookup array of processing that this token is exempt from.
* Currently, valid values are "ValidateAttributes" and
* "MakeWellFormed_TagClosedError"
*/
- var $armor = array();
-
- /**
- * Copies the tag into a new one (clone substitute).
- * @return Copied token
- */
- function copy() {
- return unserialize(serialize($this));
- }
-}
+ public $armor = array();
-/**
- * Abstract class of a tag token (start, end or empty), and its behavior.
- */
-class HTMLPurifier_Token_Tag extends HTMLPurifier_Token // abstract
-{
/**
- * Static bool marker that indicates the class is a tag.
- *
- * This allows us to check objects with
!empty($obj->is_tag)
- * without having to use a function call
is_a().
- *
- * @public
+ * Used during MakeWellFormed.
*/
- var $is_tag = true;
-
- /**
- * The lower-case name of the tag, like 'a', 'b' or 'blockquote'.
- *
- * @note Strictly speaking, XML tags are case sensitive, so we shouldn't
- * be lower-casing them, but these tokens cater to HTML tags, which are
- * insensitive.
- *
- * @public
- */
- var $name;
-
- /**
- * Associative array of the tag's attributes.
- */
- var $attr = array();
-
- /**
- * Non-overloaded constructor, which lower-cases passed tag name.
- *
- * @param $name String name.
- * @param $attr Associative array of attributes.
- */
- function HTMLPurifier_Token_Tag($name, $attr = array(), $line = null) {
- $this->name = ctype_lower($name) ? $name : strtolower($name);
- foreach ($attr as $key => $value) {
- // normalization only necessary when key is not lowercase
- if (!ctype_lower($key)) {
- $new_key = strtolower($key);
- if (!isset($attr[$new_key])) {
- $attr[$new_key] = $attr[$key];
- }
- if ($new_key !== $key) {
- unset($attr[$key]);
- }
- }
+ public $skip;
+ public $rewind;
+ public $carryover;
+
+ public function __get($n) {
+ if ($n === 'type') {
+ trigger_error('Deprecated type property called; use instanceof', E_USER_NOTICE);
+ switch (get_class($this)) {
+ case 'HTMLPurifier_Token_Start': return 'start';
+ case 'HTMLPurifier_Token_Empty': return 'empty';
+ case 'HTMLPurifier_Token_End': return 'end';
+ case 'HTMLPurifier_Token_Text': return 'text';
+ case 'HTMLPurifier_Token_Comment': return 'comment';
+ default: return null;
}
- $this->attr = $attr;
- $this->line = $line;
+ }
}
-}
-/**
- * Concrete start token class.
- */
-class HTMLPurifier_Token_Start extends HTMLPurifier_Token_Tag
-{
- var $type = 'start';
-}
-
-/**
- * Concrete empty token class.
- */
-class HTMLPurifier_Token_Empty extends HTMLPurifier_Token_Tag
-{
- var $type = 'empty';
-}
-
-/**
- * Concrete end token class.
- *
- * @warning This class accepts attributes even though end tags cannot. This
- * is for optimization reasons, as under normal circumstances, the Lexers
- * do not pass attributes.
- */
-class HTMLPurifier_Token_End extends HTMLPurifier_Token_Tag
-{
- var $type = 'end';
-}
-
-/**
- * Concrete text token class.
- *
- * Text tokens comprise of regular parsed character data (PCDATA) and raw
- * character data (from the CDATA sections). Internally, their
- * data is parsed with all entities expanded. Surprisingly, the text token
- * does have a "tag name" called #PCDATA, which is how the DTD represents it
- * in permissible child nodes.
- */
-class HTMLPurifier_Token_Text extends HTMLPurifier_Token
-{
-
- var $name = '#PCDATA'; /**< PCDATA tag name compatible with DTD. @public */
- var $type = 'text';
- var $data; /**< Parsed character data of text. @public */
- var $is_whitespace; /**< Bool indicating if node is whitespace. @public */
-
/**
- * Constructor, accepts data and determines if it is whitespace.
- *
- * @param $data String parsed character data.
+ * Sets the position of the token in the source document.
*/
- function HTMLPurifier_Token_Text($data, $line = null) {
- $this->data = $data;
- $this->is_whitespace = ctype_space($data);
- $this->line = $line;
+ public function position($l = null, $c = null) {
+ $this->line = $l;
+ $this->col = $c;
}
-
-}
-/**
- * Concrete comment token class. Generally will be ignored.
- */
-class HTMLPurifier_Token_Comment extends HTMLPurifier_Token
-{
- var $data; /**< Character data within comment. @public */
- var $type = 'comment';
/**
- * Transparent constructor.
- *
- * @param $data String comment data.
+ * Convenience function for DirectLex settings line/col position.
*/
- function HTMLPurifier_Token_Comment($data, $line = null) {
- $this->data = $data;
- $this->line = $line;
+ public function rawPosition($l, $c) {
+ if ($c === -1) $l++;
+ $this->line = $l;
+ $this->col = $c;
}
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Token/Comment.php b/lib/htmlpurifier/HTMLPurifier/Token/Comment.php
new file mode 100644
index 00000000000..dc6bdcabb85
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/Token/Comment.php
@@ -0,0 +1,22 @@
+data = $data;
+ $this->line = $line;
+ $this->col = $col;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Token/Empty.php b/lib/htmlpurifier/HTMLPurifier/Token/Empty.php
new file mode 100644
index 00000000000..2a82b47ad12
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/Token/Empty.php
@@ -0,0 +1,11 @@
+!empty($obj->is_tag)
+ * without having to use a function call
is_a().
+ */
+ public $is_tag = true;
+
+ /**
+ * The lower-case name of the tag, like 'a', 'b' or 'blockquote'.
+ *
+ * @note Strictly speaking, XML tags are case sensitive, so we shouldn't
+ * be lower-casing them, but these tokens cater to HTML tags, which are
+ * insensitive.
+ */
+ public $name;
+
+ /**
+ * Associative array of the tag's attributes.
+ */
+ public $attr = array();
+
+ /**
+ * Non-overloaded constructor, which lower-cases passed tag name.
+ *
+ * @param $name String name.
+ * @param $attr Associative array of attributes.
+ */
+ public function __construct($name, $attr = array(), $line = null, $col = null) {
+ $this->name = ctype_lower($name) ? $name : strtolower($name);
+ foreach ($attr as $key => $value) {
+ // normalization only necessary when key is not lowercase
+ if (!ctype_lower($key)) {
+ $new_key = strtolower($key);
+ if (!isset($attr[$new_key])) {
+ $attr[$new_key] = $attr[$key];
+ }
+ if ($new_key !== $key) {
+ unset($attr[$key]);
+ }
+ }
+ }
+ $this->attr = $attr;
+ $this->line = $line;
+ $this->col = $col;
+ }
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/Token/Text.php b/lib/htmlpurifier/HTMLPurifier/Token/Text.php
new file mode 100644
index 00000000000..82efd823d6b
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/Token/Text.php
@@ -0,0 +1,33 @@
+data = $data;
+ $this->is_whitespace = ctype_space($data);
+ $this->line = $line;
+ $this->col = $col;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/TokenFactory.php b/lib/htmlpurifier/HTMLPurifier/TokenFactory.php
index d15ee1a9e12..7cf48fb41c1 100644
--- a/lib/htmlpurifier/HTMLPurifier/TokenFactory.php
+++ b/lib/htmlpurifier/HTMLPurifier/TokenFactory.php
@@ -1,28 +1,26 @@
p_text = new HTMLPurifier_Token_Text('');
$this->p_comment= new HTMLPurifier_Token_Comment('');
}
-
+
/**
* Creates a HTMLPurifier_Token_Start.
* @param $name Tag name
@@ -42,10 +40,10 @@ class HTMLPurifier_TokenFactory
*/
public function createStart($name, $attr = array()) {
$p = clone $this->p_start;
- $p->HTMLPurifier_Token_Tag($name, $attr);
+ $p->__construct($name, $attr);
return $p;
}
-
+
/**
* Creates a HTMLPurifier_Token_End.
* @param $name Tag name
@@ -53,10 +51,10 @@ class HTMLPurifier_TokenFactory
*/
public function createEnd($name) {
$p = clone $this->p_end;
- $p->HTMLPurifier_Token_Tag($name);
+ $p->__construct($name);
return $p;
}
-
+
/**
* Creates a HTMLPurifier_Token_Empty.
* @param $name Tag name
@@ -65,10 +63,10 @@ class HTMLPurifier_TokenFactory
*/
public function createEmpty($name, $attr = array()) {
$p = clone $this->p_empty;
- $p->HTMLPurifier_Token_Tag($name, $attr);
+ $p->__construct($name, $attr);
return $p;
}
-
+
/**
* Creates a HTMLPurifier_Token_Text.
* @param $data Data of text token
@@ -76,10 +74,10 @@ class HTMLPurifier_TokenFactory
*/
public function createText($data) {
$p = clone $this->p_text;
- $p->HTMLPurifier_Token_Text($data);
+ $p->__construct($data);
return $p;
}
-
+
/**
* Creates a HTMLPurifier_Token_Comment.
* @param $data Data of comment token
@@ -87,9 +85,10 @@ class HTMLPurifier_TokenFactory
*/
public function createComment($data) {
$p = clone $this->p_comment;
- $p->HTMLPurifier_Token_Comment($data);
+ $p->__construct($data);
return $p;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URI.php b/lib/htmlpurifier/HTMLPurifier/URI.php
index be42ccb8690..8b50d0d18d8 100644
--- a/lib/htmlpurifier/HTMLPurifier/URI.php
+++ b/lib/htmlpurifier/HTMLPurifier/URI.php
@@ -1,8 +1,5 @@
scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme);
$this->userinfo = $userinfo;
$this->host = $host;
@@ -28,15 +25,15 @@ class HTMLPurifier_URI
$this->query = $query;
$this->fragment = $fragment;
}
-
+
/**
* Retrieves a scheme object corresponding to the URI's scheme/default
* @param $config Instance of HTMLPurifier_Config
* @param $context Instance of HTMLPurifier_Context
* @return Scheme object appropriate for validating this URI
*/
- function getSchemeObj($config, &$context) {
- $registry =& HTMLPurifier_URISchemeRegistry::instance();
+ public function getSchemeObj($config, $context) {
+ $registry = HTMLPurifier_URISchemeRegistry::instance();
if ($this->scheme !== null) {
$scheme_obj = $registry->getScheme($this->scheme, $config, $context);
if (!$scheme_obj) return false; // invalid scheme, clean it out
@@ -55,7 +52,7 @@ class HTMLPurifier_URI
}
return $scheme_obj;
}
-
+
/**
* Generic validation method applicable for all schemes. May modify
* this URI in order to get it into a compliant form.
@@ -63,13 +60,13 @@ class HTMLPurifier_URI
* @param $context Instance of HTMLPurifier_Context
* @return True if validation/filtering succeeds, false if failure
*/
- function validate($config, &$context) {
-
+ public function validate($config, $context) {
+
// ABNF definitions from RFC 3986
$chars_sub_delims = '!$&\'()*+,;=';
$chars_gen_delims = ':/?#[]@';
$chars_pchar = $chars_sub_delims . ':@';
-
+
// validate scheme (MUST BE FIRST!)
if (!is_null($this->scheme) && is_null($this->host)) {
$def = $config->getDefinition('URI');
@@ -77,25 +74,25 @@ class HTMLPurifier_URI
$this->scheme = null;
}
}
-
+
// validate host
if (!is_null($this->host)) {
$host_def = new HTMLPurifier_AttrDef_URI_Host();
$this->host = $host_def->validate($this->host, $config, $context);
if ($this->host === false) $this->host = null;
}
-
+
// validate username
if (!is_null($this->userinfo)) {
$encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':');
$this->userinfo = $encoder->encode($this->userinfo);
}
-
+
// validate port
if (!is_null($this->port)) {
if ($this->port < 1 || $this->port > 65535) $this->port = null;
}
-
+
// validate path
$path_parts = array();
$segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/');
@@ -120,7 +117,7 @@ class HTMLPurifier_URI
$segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@');
$c = strpos($this->path, '/');
if ($c !== false) {
- $this->path =
+ $this->path =
$segment_nc_encoder->encode(substr($this->path, 0, $c)) .
$segments_encoder->encode(substr($this->path, $c));
} else {
@@ -130,27 +127,27 @@ class HTMLPurifier_URI
// path-empty (hier and relative)
$this->path = ''; // just to be safe
}
-
+
// qf = query and fragment
$qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?');
-
+
if (!is_null($this->query)) {
$this->query = $qf_encoder->encode($this->query);
}
-
+
if (!is_null($this->fragment)) {
$this->fragment = $qf_encoder->encode($this->fragment);
}
-
+
return true;
-
+
}
-
+
/**
* Convert URI back to string
* @return String URI appropriate for output
*/
- function toString() {
+ public function toString() {
// reconstruct authority
$authority = null;
if (!is_null($this->host)) {
@@ -159,7 +156,7 @@ class HTMLPurifier_URI
$authority .= $this->host;
if(!is_null($this->port)) $authority .= ':' . $this->port;
}
-
+
// reconstruct the result
$result = '';
if (!is_null($this->scheme)) $result .= $this->scheme . ':';
@@ -167,16 +164,10 @@ class HTMLPurifier_URI
$result .= $this->path;
if (!is_null($this->query)) $result .= '?' . $this->query;
if (!is_null($this->fragment)) $result .= '#' . $this->fragment;
-
+
return $result;
}
-
- /**
- * Returns a copy of the URI object
- */
- function copy() {
- return unserialize(serialize($this));
- }
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIDefinition.php b/lib/htmlpurifier/HTMLPurifier/URIDefinition.php
index 45c505edb03..ea2b8fe2458 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIDefinition.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIDefinition.php
@@ -1,145 +1,93 @@
- Unique identifier for a custom-built URI definition. If you want
- to add custom URIFilters, you must specify this value.
- This directive has been available since 2.1.0.
-
-');
-
-HTMLPurifier_ConfigSchema::define(
- 'URI', 'DefinitionRev', 1, 'int', '
-
- Revision identifier for your custom definition. See
- %HTML.DefinitionRev for details. This directive has been available
- since 2.1.0.
-
-');
-
-// informative URI directives
-
-HTMLPurifier_ConfigSchema::define(
- 'URI', 'DefaultScheme', 'http', 'string', '
-
- Defines through what scheme the output will be served, in order to
- select the proper object validator when no scheme information is present.
-
-');
-
-HTMLPurifier_ConfigSchema::define(
- 'URI', 'Host', null, 'string/null', '
-
- Defines the domain name of the server, so we can determine whether or
- an absolute URI is from your website or not. Not strictly necessary,
- as users should be using relative URIs to reference resources on your
- website. It will, however, let you use absolute URIs to link to
- subdomains of the domain you post here: i.e. example.com will allow
- sub.example.com. However, higher up domains will still be excluded:
- if you set %URI.Host to sub.example.com, example.com will be blocked.
- Note: This directive overrides %URI.Base because
- a given page may be on a sub-domain, but you wish HTML Purifier to be
- more relaxed and allow some of the parent domains too.
- This directive has been available since 1.2.0.
-
-');
-
-HTMLPurifier_ConfigSchema::define(
- 'URI', 'Base', null, 'string/null', '
-
- The base URI is the URI of the document this purified HTML will be
- inserted into. This information is important if HTML Purifier needs
- to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute
- is on. You may use a non-absolute URI for this value, but behavior
- may vary (%URI.MakeAbsolute deals nicely with both absolute and
- relative paths, but forwards-compatibility is not guaranteed).
- Warning: If set, the scheme on this URI
- overrides the one specified by %URI.DefaultScheme. This directive has
- been available since 2.1.0.
-
-');
-
class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
{
-
- var $type = 'URI';
- var $filters = array();
- var $registeredFilters = array();
-
+
+ public $type = 'URI';
+ protected $filters = array();
+ protected $postFilters = array();
+ protected $registeredFilters = array();
+
/**
* HTMLPurifier_URI object of the base specified at %URI.Base
*/
- var $base;
-
+ public $base;
+
/**
- * String host to consider "home" base
+ * String host to consider "home" base, derived off of $base
*/
- var $host;
-
+ public $host;
+
/**
* Name of default scheme based on %URI.DefaultScheme and %URI.Base
*/
- var $defaultScheme;
-
- function HTMLPurifier_URIDefinition() {
+ public $defaultScheme;
+
+ public function __construct() {
$this->registerFilter(new HTMLPurifier_URIFilter_DisableExternal());
$this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
$this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
$this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
+ $this->registerFilter(new HTMLPurifier_URIFilter_Munge());
}
-
- function registerFilter($filter) {
+
+ public function registerFilter($filter) {
$this->registeredFilters[$filter->name] = $filter;
}
-
- function addFilter($filter, $config) {
- $filter->prepare($config);
- $this->filters[$filter->name] = $filter;
+
+ public function addFilter($filter, $config) {
+ $r = $filter->prepare($config);
+ if ($r === false) return; // null is ok, for backwards compat
+ if ($filter->post) {
+ $this->postFilters[$filter->name] = $filter;
+ } else {
+ $this->filters[$filter->name] = $filter;
+ }
}
-
- function doSetup($config) {
+
+ protected function doSetup($config) {
$this->setupMemberVariables($config);
$this->setupFilters($config);
}
-
- function setupFilters($config) {
+
+ protected function setupFilters($config) {
foreach ($this->registeredFilters as $name => $filter) {
- $conf = $config->get('URI', $name);
+ $conf = $config->get('URI.' . $name);
if ($conf !== false && $conf !== null) {
$this->addFilter($filter, $config);
}
}
unset($this->registeredFilters);
}
-
- function setupMemberVariables($config) {
- $this->host = $config->get('URI', 'Host');
- $base_uri = $config->get('URI', 'Base');
+
+ protected function setupMemberVariables($config) {
+ $this->host = $config->get('URI.Host');
+ $base_uri = $config->get('URI.Base');
if (!is_null($base_uri)) {
$parser = new HTMLPurifier_URIParser();
$this->base = $parser->parse($base_uri);
$this->defaultScheme = $this->base->scheme;
if (is_null($this->host)) $this->host = $this->base->host;
}
- if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI', 'DefaultScheme');
+ if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme');
}
-
- function filter(&$uri, $config, &$context) {
- foreach ($this->filters as $name => $x) {
- $result = $this->filters[$name]->filter($uri, $config, $context);
+
+ public function filter(&$uri, $config, $context) {
+ foreach ($this->filters as $name => $f) {
+ $result = $f->filter($uri, $config, $context);
if (!$result) return false;
}
return true;
}
-
+
+ public function postFilter(&$uri, $config, $context) {
+ foreach ($this->postFilters as $name => $f) {
+ $result = $f->filter($uri, $config, $context);
+ if (!$result) return false;
+ }
+ return true;
+ }
+
}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter.php b/lib/htmlpurifier/HTMLPurifier/URIFilter.php
index ca000ea5a2a..c116f93dffc 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIFilter.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIFilter.php
@@ -2,39 +2,44 @@
/**
* Chainable filters for custom URI processing.
- *
+ *
* These filters can perform custom actions on a URI filter object,
* including transformation or blacklisting.
- *
+ *
* @warning This filter is called before scheme object validation occurs.
* Make sure, if you require a specific scheme object, you
* you check that it exists. This allows filters to convert
* proprietary URI schemes into regular ones.
*/
-class HTMLPurifier_URIFilter
+abstract class HTMLPurifier_URIFilter
{
-
+
/**
* Unique identifier of filter
*/
- var $name;
-
+ public $name;
+
+ /**
+ * True if this filter should be run after scheme validation.
+ */
+ public $post = false;
+
/**
* Performs initialization for the filter
*/
- function prepare($config) {}
-
+ public function prepare($config) {return true;}
+
/**
* Filter a URI object
- * @param &$uri Reference to URI object
+ * @param $uri Reference to URI object variable
* @param $config Instance of HTMLPurifier_Config
- * @param &$context Instance of HTMLPurifier_Context
+ * @param $context Instance of HTMLPurifier_Context
* @return bool Whether or not to continue processing: false indicates
* URL is no good, true indicates continue processing. Note that
* all changes are committed directly on the URI object
*/
- function filter(&$uri, $config, &$context) {
- trigger_error('Cannot call abstract function', E_USER_ERROR);
- }
-
+ abstract public function filter(&$uri, $config, $context);
+
}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php
index 4e6dc187c8e..d8a39a50111 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternal.php
@@ -1,26 +1,14 @@
get('URI', 'Host');
+ public $name = 'DisableExternal';
+ protected $ourHostParts = false;
+ public function prepare($config) {
+ $our_host = $config->getDefinition('URI')->host;
if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host));
}
- function filter(&$uri, $config, &$context) {
+ public function filter(&$uri, $config, $context) {
if (is_null($uri->host)) return true;
if ($this->ourHostParts === false) return false;
$host_parts = array_reverse(explode('.', $uri->host));
@@ -32,3 +20,4 @@ class HTMLPurifier_URIFilter_DisableExternal extends HTMLPurifier_URIFilter
}
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php
index dc00e741105..881abc43cfa 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php
@@ -1,26 +1,12 @@
get('EmbeddedURI', true)) return true;
return parent::filter($uri, $config, $context);
}
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php
index d3429d5cbf9..045aa0992c6 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php
@@ -1,23 +1,14 @@
moo.com will catch
moo.com.example.com. '.
- 'This directive has been available since 1.3.0.'
-);
-
class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
{
- var $name = 'HostBlacklist';
- var $blacklist = array();
- function prepare($config) {
- $this->blacklist = $config->get('URI', 'HostBlacklist');
+ public $name = 'HostBlacklist';
+ protected $blacklist = array();
+ public function prepare($config) {
+ $this->blacklist = $config->get('URI.HostBlacklist');
+ return true;
}
- function filter(&$uri, $config, &$context) {
+ public function filter(&$uri, $config, $context) {
foreach($this->blacklist as $blacklisted_host_fragment) {
if (strpos($uri->host, $blacklisted_host_fragment) !== false) {
return false;
@@ -26,3 +17,5 @@ class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
return true;
}
}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php
index 8fe4f73e616..f46ab2630d4 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php
@@ -2,45 +2,33 @@
// does not support network paths
-require_once 'HTMLPurifier/URIFilter.php';
-
-HTMLPurifier_ConfigSchema::define(
- 'URI', 'MakeAbsolute', false, 'bool', '
-
- Converts all URIs into absolute forms. This is useful when the HTML
- being filtered assumes a specific base path, but will actually be
- viewed in a different context (and setting an alternate base URI is
- not possible). %URI.Base must be set for this directive to work.
- This directive has been available since 2.1.0.
-
-');
-
class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
{
- var $name = 'MakeAbsolute';
- var $base;
- var $basePathStack = array();
- function prepare($config) {
+ public $name = 'MakeAbsolute';
+ protected $base;
+ protected $basePathStack = array();
+ public function prepare($config) {
$def = $config->getDefinition('URI');
$this->base = $def->base;
if (is_null($this->base)) {
- trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_ERROR);
- return;
+ trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING);
+ return false;
}
$this->base->fragment = null; // fragment is invalid for base URI
$stack = explode('/', $this->base->path);
array_pop($stack); // discard last segment
$stack = $this->_collapseStack($stack); // do pre-parsing
$this->basePathStack = $stack;
+ return true;
}
- function filter(&$uri, $config, &$context) {
+ public function filter(&$uri, $config, $context) {
if (is_null($this->base)) return true; // abort early
if (
$uri->path === '' && is_null($uri->scheme) &&
is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment)
) {
// reference to current document
- $uri = $this->base->copy();
+ $uri = clone $this->base;
return true;
}
if (!is_null($uri->scheme)) {
@@ -63,12 +51,18 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
}
if ($uri->path === '') {
$uri->path = $this->base->path;
- }elseif ($uri->path[0] !== '/') {
+ } elseif ($uri->path[0] !== '/') {
// relative path, needs more complicated processing
$stack = explode('/', $uri->path);
$new_stack = array_merge($this->basePathStack, $stack);
+ if ($new_stack[0] !== '' && !is_null($this->base->host)) {
+ array_unshift($new_stack, '');
+ }
$new_stack = $this->_collapseStack($new_stack);
$uri->path = implode('/', $new_stack);
+ } else {
+ // absolute path, but still we should collapse
+ $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path)));
}
// re-combine
$uri->scheme = $this->base->scheme;
@@ -77,13 +71,13 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
if (is_null($uri->port)) $uri->port = $this->base->port;
return true;
}
-
+
/**
* Resolve dots and double-dots in a path stack
- * @private
*/
- function _collapseStack($stack) {
+ private function _collapseStack($stack) {
$result = array();
+ $is_folder = false;
for ($i = 0; isset($stack[$i]); $i++) {
$is_folder = false;
// absorb an internally duplicated slash
@@ -117,3 +111,4 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
}
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php b/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php
new file mode 100644
index 00000000000..efa10a6458a
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/URIFilter/Munge.php
@@ -0,0 +1,58 @@
+target = $config->get('URI.' . $this->name);
+ $this->parser = new HTMLPurifier_URIParser();
+ $this->doEmbed = $config->get('URI.MungeResources');
+ $this->secretKey = $config->get('URI.MungeSecretKey');
+ return true;
+ }
+ public function filter(&$uri, $config, $context) {
+ if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true;
+
+ $scheme_obj = $uri->getSchemeObj($config, $context);
+ if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it
+ if (is_null($uri->host) || empty($scheme_obj->browsable)) {
+ return true;
+ }
+ // don't redirect if target host is our host
+ if ($uri->host === $config->getDefinition('URI')->host) {
+ return true;
+ }
+
+ $this->makeReplace($uri, $config, $context);
+ $this->replace = array_map('rawurlencode', $this->replace);
+
+ $new_uri = strtr($this->target, $this->replace);
+ $new_uri = $this->parser->parse($new_uri);
+ // don't redirect if the target host is the same as the
+ // starting host
+ if ($uri->host === $new_uri->host) return true;
+ $uri = $new_uri; // overwrite
+ return true;
+ }
+
+ protected function makeReplace($uri, $config, $context) {
+ $string = $uri->toString();
+ // always available
+ $this->replace['%s'] = $string;
+ $this->replace['%r'] = $context->get('EmbeddedURI', true);
+ $token = $context->get('CurrentToken', true);
+ $this->replace['%n'] = $token ? $token->name : null;
+ $this->replace['%m'] = $context->get('CurrentAttr', true);
+ $this->replace['%p'] = $context->get('CurrentCSSProperty', true);
+ // not always available
+ if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIParser.php b/lib/htmlpurifier/HTMLPurifier/URIParser.php
index 8ba485cf351..7179e4ab899 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIParser.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIParser.php
@@ -1,35 +1,33 @@
percentEncoder = new HTMLPurifier_PercentEncoder();
}
-
+
/**
* Parses a URI.
* @param $uri string URI to parse
* @return HTMLPurifier_URI representation of URI. This representation has
* not been validated yet and may not conform to RFC.
*/
- function parse($uri) {
-
+ public function parse($uri) {
+
$uri = $this->percentEncoder->normalize($uri);
-
+
// Regexp is as per Appendix B.
- // Note that ["<>] are an addition to the RFC's recommended
+ // Note that ["<>] are an addition to the RFC's recommended
// characters, because they represent external delimeters.
$r_URI = '!'.
'(([^:/?#"<>]+):)?'. // 2. Scheme
@@ -38,19 +36,19 @@ class HTMLPurifier_URIParser
'(\?([^#"<>]*))?'. // 7. Query
'(#([^"<>]*))?'. // 8. Fragment
'!';
-
+
$matches = array();
$result = preg_match($r_URI, $uri, $matches);
-
+
if (!$result) return false; // *really* invalid URI
-
+
// seperate out parts
$scheme = !empty($matches[1]) ? $matches[2] : null;
$authority = !empty($matches[3]) ? $matches[4] : null;
$path = $matches[5]; // always present, can be empty
$query = !empty($matches[6]) ? $matches[7] : null;
$fragment = !empty($matches[8]) ? $matches[9] : null;
-
+
// further parse authority
if ($authority !== null) {
$r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/";
@@ -62,10 +60,11 @@ class HTMLPurifier_URIParser
} else {
$port = $host = $userinfo = null;
}
-
+
return new HTMLPurifier_URI(
$scheme, $userinfo, $host, $port, $path, $query, $fragment);
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme.php b/lib/htmlpurifier/HTMLPurifier/URIScheme.php
index 41c02f70d2d..039710fd158 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIScheme.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme.php
@@ -5,26 +5,24 @@
*/
class HTMLPurifier_URIScheme
{
-
+
/**
* Scheme's default port (integer)
- * @public
*/
- var $default_port = null;
-
+ public $default_port = null;
+
/**
* Whether or not URIs of this schem are locatable by a browser
* http and ftp are accessible, while mailto and news are not.
- * @public
*/
- var $browsable = false;
-
+ public $browsable = false;
+
/**
* Whether or not the URI always uses
, resolves edge cases
* with making relative URIs absolute
*/
- var $hierarchical = false;
-
+ public $hierarchical = false;
+
/**
* Validates the components of a URI
* @note This implementation should be called by children if they define
@@ -34,10 +32,11 @@ class HTMLPurifier_URIScheme
* @param $context HTMLPurifier_Context object
* @return Bool success or failure
*/
- function validate(&$uri, $config, &$context) {
+ public function validate(&$uri, $config, $context) {
if ($this->default_port == $uri->port) $uri->port = null;
return true;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/data.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/data.php
new file mode 100644
index 00000000000..b7f1989cbf7
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/data.php
@@ -0,0 +1,93 @@
+ true,
+ 'image/gif' => true,
+ 'image/png' => true,
+ );
+
+ public function validate(&$uri, $config, $context) {
+ $result = explode(',', $uri->path, 2);
+ $is_base64 = false;
+ $charset = null;
+ $content_type = null;
+ if (count($result) == 2) {
+ list($metadata, $data) = $result;
+ // do some legwork on the metadata
+ $metas = explode(';', $metadata);
+ while(!empty($metas)) {
+ $cur = array_shift($metas);
+ if ($cur == 'base64') {
+ $is_base64 = true;
+ break;
+ }
+ if (substr($cur, 0, 8) == 'charset=') {
+ // doesn't match if there are arbitrary spaces, but
+ // whatever dude
+ if ($charset !== null) continue; // garbage
+ $charset = substr($cur, 8); // not used
+ } else {
+ if ($content_type !== null) continue; // garbage
+ $content_type = $cur;
+ }
+ }
+ } else {
+ $data = $result[0];
+ }
+ if ($content_type !== null && empty($this->allowed_types[$content_type])) {
+ return false;
+ }
+ if ($charset !== null) {
+ // error; we don't allow plaintext stuff
+ $charset = null;
+ }
+ $data = rawurldecode($data);
+ if ($is_base64) {
+ $raw_data = base64_decode($data);
+ } else {
+ $raw_data = $data;
+ }
+ // XXX probably want to refactor this into a general mechanism
+ // for filtering arbitrary content types
+ $file = tempnam("/tmp", "");
+ file_put_contents($file, $raw_data);
+ if (function_exists('exif_imagetype')) {
+ $image_code = exif_imagetype($file);
+ } elseif (function_exists('getimagesize')) {
+ set_error_handler(array($this, 'muteErrorHandler'));
+ $info = getimagesize($file);
+ restore_error_handler();
+ if ($info == false) return false;
+ $image_code = $info[2];
+ } else {
+ trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR);
+ }
+ $real_content_type = image_type_to_mime_type($image_code);
+ if ($real_content_type != $content_type) {
+ // we're nice guys; if the content type is something else we
+ // support, change it over
+ if (empty($this->allowed_types[$real_content_type])) return false;
+ $content_type = $real_content_type;
+ }
+ // ok, it's kosher, rewrite what we need
+ $uri->userinfo = null;
+ $uri->host = null;
+ $uri->port = null;
+ $uri->fragment = null;
+ $uri->query = null;
+ $uri->path = "$content_type;base64," . base64_encode($raw_data);
+ return true;
+ }
+
+ public function muteErrorHandler($errno, $errstr) {}
+
+}
+
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php
index 5555ef33a17..5849bf7ff05 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/ftp.php
@@ -1,20 +1,18 @@
query = null;
-
+
// typecode check
$semicolon_pos = strrpos($uri->path, ';'); // reverse
if ($semicolon_pos !== false) {
@@ -36,9 +34,10 @@ class HTMLPurifier_URIScheme_ftp extends HTMLPurifier_URIScheme {
$uri->path = str_replace(';', '%3B', $uri->path);
$uri->path .= $type_ret;
}
-
+
return true;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php
index 7abc6680fdc..b097a31d6a3 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/http.php
@@ -1,21 +1,20 @@
userinfo = null;
return true;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php
index bbd69b9c42f..29e380919f0 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/https.php
@@ -1,13 +1,12 @@
userinfo = null;
$uri->host = null;
@@ -23,6 +21,7 @@ class HTMLPurifier_URIScheme_mailto extends HTMLPurifier_URIScheme {
// we need to validate path against RFC 2368's addr-spec
return true;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php
index 87bda63c7f6..f5f54f4f56f 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/news.php
@@ -1,15 +1,13 @@
userinfo = null;
$uri->host = null;
@@ -18,6 +16,7 @@ class HTMLPurifier_URIScheme_news extends HTMLPurifier_URIScheme {
// typecode check needed on path
return true;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php b/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php
index caa85b260fb..5bf93ea7849 100644
--- a/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php
+++ b/lib/htmlpurifier/HTMLPurifier/URIScheme/nntp.php
@@ -1,21 +1,20 @@
userinfo = null;
$uri->query = null;
return true;
}
-
+
}
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php b/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php
index 6837fadaafc..576bf7b6d1a 100644
--- a/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php
+++ b/lib/htmlpurifier/HTMLPurifier/URISchemeRegistry.php
@@ -1,50 +1,19 @@
true, // "Hypertext Transfer Protocol", nuf' said
- 'https' => true, // HTTP over SSL (Secure Socket Layer)
- // quite useful, but not necessary
- 'mailto' => true,// Email
- 'ftp' => true, // "File Transfer Protocol"
- // for Usenet, these two are similar, but distinct
- 'nntp' => true, // individual Netnews articles
- 'news' => true // newsgroup or individual Netnews articles
- ), 'lookup',
- 'Whitelist that defines the schemes that a URI is allowed to have. This '.
- 'prevents XSS attacks from using pseudo-schemes like javascript or mocha.'
-);
-
-HTMLPurifier_ConfigSchema::define(
- 'URI', 'OverrideAllowedSchemes', true, 'bool',
- 'If this is set to true (which it is by default), you can override '.
- '%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme '.
- 'to the registry. If false, you will also have to update that directive '.
- 'in order to add more schemes.'
-);
-
/**
* Registry for retrieving specific URI scheme validator objects.
*/
class HTMLPurifier_URISchemeRegistry
{
-
+
/**
* Retrieve sole instance of the registry.
- * @static
* @param $prototype Optional prototype to overload sole instance with,
* or bool true to reset to default registry.
* @note Pass a registry object $prototype with a compatible interface and
* the function will copy it and return it all further times.
*/
- function &instance($prototype = null) {
+ public static function instance($prototype = null) {
static $instance = null;
if ($prototype !== null) {
$instance = $prototype;
@@ -53,49 +22,47 @@ class HTMLPurifier_URISchemeRegistry
}
return $instance;
}
-
+
/**
* Cache of retrieved schemes.
- * @protected
*/
- var $schemes = array();
-
+ protected $schemes = array();
+
/**
* Retrieves a scheme validator object
* @param $scheme String scheme name like http or mailto
* @param $config HTMLPurifier_Config object
* @param $config HTMLPurifier_Context object
*/
- function &getScheme($scheme, $config, &$context) {
+ public function getScheme($scheme, $config, $context) {
if (!$config) $config = HTMLPurifier_Config::createDefault();
- $null = null; // for the sake of passing by reference
-
+
// important, otherwise attacker could include arbitrary file
- $allowed_schemes = $config->get('URI', 'AllowedSchemes');
- if (!$config->get('URI', 'OverrideAllowedSchemes') &&
+ $allowed_schemes = $config->get('URI.AllowedSchemes');
+ if (!$config->get('URI.OverrideAllowedSchemes') &&
!isset($allowed_schemes[$scheme])
) {
- return $null;
+ return;
}
-
+
if (isset($this->schemes[$scheme])) return $this->schemes[$scheme];
- if (!isset($allowed_schemes[$scheme])) return $null;
-
+ if (!isset($allowed_schemes[$scheme])) return;
+
$class = 'HTMLPurifier_URIScheme_' . $scheme;
- if (!class_exists($class)) return $null;
+ if (!class_exists($class)) return;
$this->schemes[$scheme] = new $class();
return $this->schemes[$scheme];
}
-
+
/**
* Registers a custom scheme to the cache, bypassing reflection.
* @param $scheme Scheme name
* @param $scheme_obj HTMLPurifier_URIScheme object
*/
- function register($scheme, &$scheme_obj) {
- $this->schemes[$scheme] =& $scheme_obj;
+ public function register($scheme, $scheme_obj) {
+ $this->schemes[$scheme] = $scheme_obj;
}
-
+
}
-
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/UnitConverter.php b/lib/htmlpurifier/HTMLPurifier/UnitConverter.php
index d8791ccaa8d..545d4262206 100644
--- a/lib/htmlpurifier/HTMLPurifier/UnitConverter.php
+++ b/lib/htmlpurifier/HTMLPurifier/UnitConverter.php
@@ -6,28 +6,56 @@
*/
class HTMLPurifier_UnitConverter
{
-
+
+ const ENGLISH = 1;
+ const METRIC = 2;
+ const DIGITAL = 3;
+
+ /**
+ * Units information array. Units are grouped into measuring systems
+ * (English, Metric), and are assigned an integer representing
+ * the conversion factor between that unit and the smallest unit in
+ * the system. Numeric indexes are actually magical constants that
+ * encode conversion data from one system to the next, with a O(n^2)
+ * constraint on memory (this is generally not a problem, since
+ * the number of measuring systems is small.)
+ */
+ protected static $units = array(
+ self::ENGLISH => array(
+ 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
+ 'pt' => 4,
+ 'pc' => 48,
+ 'in' => 288,
+ self::METRIC => array('pt', '0.352777778', 'mm'),
+ ),
+ self::METRIC => array(
+ 'mm' => 1,
+ 'cm' => 10,
+ self::ENGLISH => array('mm', '2.83464567', 'pt'),
+ ),
+ );
+
/**
* Minimum bcmath precision for output.
*/
- var $outputPrecision;
-
+ protected $outputPrecision;
+
/**
* Bcmath precision for internal calculations.
*/
- var $internalPrecision;
-
+ protected $internalPrecision;
+
/**
* Whether or not BCMath is available
*/
- var $bcmath;
-
- function HTMLPurifier_UnitConverter($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) {
+ private $bcmath;
+
+ public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) {
$this->outputPrecision = $output_precision;
$this->internalPrecision = $internal_precision;
$this->bcmath = !$force_no_bcmath && function_exists('bcmul');
}
-
+
/**
* Converts a length object of one unit into another unit.
* @param HTMLPurifier_Length $length
@@ -46,132 +74,103 @@ class HTMLPurifier_UnitConverter
* and this causes some decimals to be excluded, those
* decimals will be added on.
*/
- function convert($length, $to_unit) {
-
- /**
- * Units information array. Units are grouped into measuring systems
- * (English, Metric), and are assigned an integer representing
- * the conversion factor between that unit and the smallest unit in
- * the system. Numeric indexes are actually magical constants that
- * encode conversion data from one system to the next, with a O(n^2)
- * constraint on memory (this is generally not a problem, since
- * the number of measuring systems is small.)
- */
- static $units = array(
- 1 => array(
- 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary
- 'pt' => 4,
- 'pc' => 48,
- 'in' => 288,
- 2 => array('pt', '0.352777778', 'mm'),
- ),
- 2 => array(
- 'mm' => 1,
- 'cm' => 10,
- 1 => array('mm', '2.83464567', 'pt'),
- ),
- );
-
+ public function convert($length, $to_unit) {
+
if (!$length->isValid()) return false;
-
+
$n = $length->getN();
$unit = $length->getUnit();
-
+
if ($n === '0' || $unit === false) {
return new HTMLPurifier_Length('0', false);
}
-
+
$state = $dest_state = false;
- foreach ($units as $k => $x) {
+ foreach (self::$units as $k => $x) {
if (isset($x[$unit])) $state = $k;
if (isset($x[$to_unit])) $dest_state = $k;
}
if (!$state || !$dest_state) return false;
-
+
// Some calculations about the initial precision of the number;
// this will be useful when we need to do final rounding.
$sigfigs = $this->getSigFigs($n);
if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision;
-
- // Cleanup $n for PHP 4.3.9 and 4.3.10. See http://bugs.php.net/bug.php?id=30726
- if (strncmp($n, '-.', 2) === 0) {
- $n = '-0.' . substr($n, 2);
- }
-
+
// BCMath's internal precision deals only with decimals. Use
// our default if the initial number has no decimals, or increase
// it by how ever many decimals, thus, the number of guard digits
// will always be greater than or equal to internalPrecision.
$log = (int) floor(log(abs($n), 10));
$cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision
-
+
for ($i = 0; $i < 2; $i++) {
-
+
// Determine what unit IN THIS SYSTEM we need to convert to
if ($dest_state === $state) {
// Simple conversion
$dest_unit = $to_unit;
} else {
// Convert to the smallest unit, pending a system shift
- $dest_unit = $units[$state][$dest_state][0];
+ $dest_unit = self::$units[$state][$dest_state][0];
}
-
+
// Do the conversion if necessary
if ($dest_unit !== $unit) {
- $factor = $this->div($units[$state][$unit], $units[$state][$dest_unit], $cp);
+ $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp);
$n = $this->mul($n, $factor, $cp);
$unit = $dest_unit;
}
-
+
// Output was zero, so bail out early. Shouldn't ever happen.
if ($n === '') {
$n = '0';
$unit = $to_unit;
break;
}
-
+
// It was a simple conversion, so bail out
if ($dest_state === $state) {
break;
}
-
+
if ($i !== 0) {
// Conversion failed! Apparently, the system we forwarded
// to didn't have this unit. This should never happen!
return false;
}
-
+
// Pre-condition: $i == 0
-
+
// Perform conversion to next system of units
- $n = $this->mul($n, $units[$state][$dest_state][1], $cp);
- $unit = $units[$state][$dest_state][2];
+ $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp);
+ $unit = self::$units[$state][$dest_state][2];
$state = $dest_state;
-
+
// One more loop around to convert the unit in the new system.
-
+
}
-
+
// Post-condition: $unit == $to_unit
if ($unit !== $to_unit) return false;
-
+
// Useful for debugging:
//echo "n";
//echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n\n";
-
+
$n = $this->round($n, $sigfigs);
if (strpos($n, '.') !== false) $n = rtrim($n, '0');
$n = rtrim($n, '.');
-
+
return new HTMLPurifier_Length($n, $unit);
}
-
+
/**
* Returns the number of significant figures in a string number.
* @param string $n Decimal number
* @return int number of sigfigs
*/
- function getSigFigs($n) {
+ public function getSigFigs($n) {
$n = ltrim($n, '0+-');
$dp = strpos($n, '.'); // decimal position
if ($dp === false) {
@@ -182,36 +181,36 @@ class HTMLPurifier_UnitConverter
}
return $sigfigs;
}
-
+
/**
* Adds two numbers, using arbitrary precision when available.
*/
- function add($s1, $s2, $scale) {
+ private function add($s1, $s2, $scale) {
if ($this->bcmath) return bcadd($s1, $s2, $scale);
else return $this->scale($s1 + $s2, $scale);
}
-
+
/**
* Multiples two numbers, using arbitrary precision when available.
*/
- function mul($s1, $s2, $scale) {
+ private function mul($s1, $s2, $scale) {
if ($this->bcmath) return bcmul($s1, $s2, $scale);
else return $this->scale($s1 * $s2, $scale);
}
-
+
/**
* Divides two numbers, using arbitrary precision when available.
*/
- function div($s1, $s2, $scale) {
+ private function div($s1, $s2, $scale) {
if ($this->bcmath) return bcdiv($s1, $s2, $scale);
else return $this->scale($s1 / $s2, $scale);
}
-
+
/**
* Rounds a number according to the number of sigfigs it should have,
* using arbitrary precision when available.
*/
- function round($n, $sigfigs) {
+ private function round($n, $sigfigs) {
$new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1
$rp = $sigfigs - $new_log - 1; // Number of decimal places needed
$neg = $n < 0 ? '-' : ''; // Negative sign
@@ -230,12 +229,26 @@ class HTMLPurifier_UnitConverter
return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1);
}
}
-
+
/**
* Scales a float to $scale digits right of decimal point, like BCMath.
*/
- function scale($r, $scale) {
+ private function scale($r, $scale) {
+ if ($scale < 0) {
+ // The f sprintf type doesn't support negative numbers, so we
+ // need to cludge things manually. First get the string.
+ $r = sprintf('%.0f', (float) $r);
+ // Due to floating point precision loss, $r will more than likely
+ // look something like 4652999999999.9234. We grab one more digit
+ // than we need to precise from $r and then use that to round
+ // appropriately.
+ $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1);
+ // Now we return it, truncating the zero that was rounded off.
+ return substr($precise, 0, -1) . str_repeat('0', -$scale + 1);
+ }
return sprintf('%.' . $scale . 'f', (float) $r);
}
-
+
}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser.php b/lib/htmlpurifier/HTMLPurifier/VarParser.php
new file mode 100644
index 00000000000..68e72ae8695
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/VarParser.php
@@ -0,0 +1,154 @@
+ self::STRING,
+ 'istring' => self::ISTRING,
+ 'text' => self::TEXT,
+ 'itext' => self::ITEXT,
+ 'int' => self::INT,
+ 'float' => self::FLOAT,
+ 'bool' => self::BOOL,
+ 'lookup' => self::LOOKUP,
+ 'list' => self::ALIST,
+ 'hash' => self::HASH,
+ 'mixed' => self::MIXED
+ );
+
+ /**
+ * Lookup table of types that are string, and can have aliases or
+ * allowed value lists.
+ */
+ static public $stringTypes = array(
+ self::STRING => true,
+ self::ISTRING => true,
+ self::TEXT => true,
+ self::ITEXT => true,
+ );
+
+ /**
+ * Validate a variable according to type. Throws
+ * HTMLPurifier_VarParserException if invalid.
+ * It may return NULL as a valid type if $allow_null is true.
+ *
+ * @param $var Variable to validate
+ * @param $type Type of variable, see HTMLPurifier_VarParser->types
+ * @param $allow_null Whether or not to permit null as a value
+ * @return Validated and type-coerced variable
+ */
+ final public function parse($var, $type, $allow_null = false) {
+ if (is_string($type)) {
+ if (!isset(HTMLPurifier_VarParser::$types[$type])) {
+ throw new HTMLPurifier_VarParserException("Invalid type '$type'");
+ } else {
+ $type = HTMLPurifier_VarParser::$types[$type];
+ }
+ }
+ $var = $this->parseImplementation($var, $type, $allow_null);
+ if ($allow_null && $var === null) return null;
+ // These are basic checks, to make sure nothing horribly wrong
+ // happened in our implementations.
+ switch ($type) {
+ case (self::STRING):
+ case (self::ISTRING):
+ case (self::TEXT):
+ case (self::ITEXT):
+ if (!is_string($var)) break;
+ if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var);
+ return $var;
+ case (self::INT):
+ if (!is_int($var)) break;
+ return $var;
+ case (self::FLOAT):
+ if (!is_float($var)) break;
+ return $var;
+ case (self::BOOL):
+ if (!is_bool($var)) break;
+ return $var;
+ case (self::LOOKUP):
+ case (self::ALIST):
+ case (self::HASH):
+ if (!is_array($var)) break;
+ if ($type === self::LOOKUP) {
+ foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true');
+ } elseif ($type === self::ALIST) {
+ $keys = array_keys($var);
+ if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform');
+ }
+ return $var;
+ case (self::MIXED):
+ return $var;
+ default:
+ $this->errorInconsistent(get_class($this), $type);
+ }
+ $this->errorGeneric($var, $type);
+ }
+
+ /**
+ * Actually implements the parsing. Base implementation is to not
+ * do anything to $var. Subclasses should overload this!
+ */
+ protected function parseImplementation($var, $type, $allow_null) {
+ return $var;
+ }
+
+ /**
+ * Throws an exception.
+ */
+ protected function error($msg) {
+ throw new HTMLPurifier_VarParserException($msg);
+ }
+
+ /**
+ * Throws an inconsistency exception.
+ * @note This should not ever be called. It would be called if we
+ * extend the allowed values of HTMLPurifier_VarParser without
+ * updating subclasses.
+ */
+ protected function errorInconsistent($class, $type) {
+ throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented");
+ }
+
+ /**
+ * Generic error for if a type didn't work.
+ */
+ protected function errorGeneric($var, $type) {
+ $vtype = gettype($var);
+ $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype");
+ }
+
+ static public function getTypeName($type) {
+ static $lookup;
+ if (!$lookup) {
+ // Lazy load the alternative lookup table
+ $lookup = array_flip(HTMLPurifier_VarParser::$types);
+ }
+ if (!isset($lookup[$type])) return 'unknown';
+ return $lookup[$type];
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php b/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php
new file mode 100644
index 00000000000..c954250e9f0
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/VarParser/Flexible.php
@@ -0,0 +1,96 @@
+ $j) $var[$i] = trim($j);
+ if ($type === self::HASH) {
+ // key:value,key2:value2
+ $nvar = array();
+ foreach ($var as $keypair) {
+ $c = explode(':', $keypair, 2);
+ if (!isset($c[1])) continue;
+ $nvar[$c[0]] = $c[1];
+ }
+ $var = $nvar;
+ }
+ }
+ if (!is_array($var)) break;
+ $keys = array_keys($var);
+ if ($keys === array_keys($keys)) {
+ if ($type == self::ALIST) return $var;
+ elseif ($type == self::LOOKUP) {
+ $new = array();
+ foreach ($var as $key) {
+ $new[$key] = true;
+ }
+ return $new;
+ } else break;
+ }
+ if ($type === self::LOOKUP) {
+ foreach ($var as $key => $value) {
+ $var[$key] = true;
+ }
+ }
+ return $var;
+ default:
+ $this->errorInconsistent(__CLASS__, $type);
+ }
+ $this->errorGeneric($var, $type);
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/VarParser/Native.php b/lib/htmlpurifier/HTMLPurifier/VarParser/Native.php
new file mode 100644
index 00000000000..b02a6de54ca
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/VarParser/Native.php
@@ -0,0 +1,26 @@
+evalExpression($var);
+ }
+
+ protected function evalExpression($expr) {
+ $var = null;
+ $result = eval("\$var = $expr;");
+ if ($result === false) {
+ throw new HTMLPurifier_VarParserException("Fatal error in evaluated code");
+ }
+ return $var;
+ }
+
+}
+
+// vim: et sw=4 sts=4
diff --git a/lib/htmlpurifier/HTMLPurifier/VarParserException.php b/lib/htmlpurifier/HTMLPurifier/VarParserException.php
new file mode 100644
index 00000000000..5df3414959b
--- /dev/null
+++ b/lib/htmlpurifier/HTMLPurifier/VarParserException.php
@@ -0,0 +1,11 @@
+, , and tags
@@ -6,5 +6,3 @@ Changes:
* AttrDef/Lang.php - relax lang check - needed for multilang
skodak
-
-$Id$
diff --git a/lib/setup.php b/lib/setup.php
index 4dee4a24af2..9a71075c2c5 100644
--- a/lib/setup.php
+++ b/lib/setup.php
@@ -92,6 +92,10 @@ global $HTTPSPAGEREQUIRED;
/// sometimes default PHP settings are borked on shared hosting servers, I wonder why they have to do that??
@ini_set('precision', 14); // needed for upgrades and gradebook
+/// New versions of HTML Purifier are not compatible with PHP 4
+ if (version_compare(phpversion(), "5.0.5") < 0) {
+ $CFG->enablehtmlpurifier = 0;
+ }
/// store settings from config.php in array in $CFG - we can use it later to detect problems and overrides
$CFG->config_php_settings = (array)$CFG;
@@ -256,7 +260,6 @@ global $HTTPSPAGEREQUIRED;
$originaldatabasedebug = -1;
}
-
/// For now, only needed under apache (and probably unstable in other contexts)
if (function_exists('register_shutdown_function')) {
register_shutdown_function('moodle_request_shutdown');
diff --git a/lib/weblib.php b/lib/weblib.php
index 6ec0acf7889..16ff9cb432d 100644
--- a/lib/weblib.php
+++ b/lib/weblib.php
@@ -2019,6 +2019,7 @@ function clean_text($text, $format=FORMAT_MOODLE) {
default:
if (!empty($CFG->enablehtmlpurifier)) {
+ //this is PHP5 only, the lib/setup.php contains a disabler for PHP4
$text = purify_html($text);
} else {
/// Fix non standard entity notations
@@ -2046,24 +2047,28 @@ function clean_text($text, $format=FORMAT_MOODLE) {
/**
* KSES replacement cleaning function - uses HTML Purifier.
+ *
+ * @global object
+ * @param string $text The (X)HTML string to purify
*/
function purify_html($text) {
global $CFG;
// this can not be done only once because we sometimes need to reset the cache
- $cachedir = $CFG->dataroot.'/cache/htmlpurifier/';
+ $cachedir = $CFG->dataroot.'/cache/htmlpurifier';
$status = check_dir_exists($cachedir, true, true);
static $purifier = false;
+ static $config;
if ($purifier === false) {
- require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.auto.php';
+ require_once $CFG->libdir.'/htmlpurifier/HTMLPurifier.safe-includes.php';
$config = HTMLPurifier_Config::createDefault();
- $config->set('Core', 'AcceptFullDocuments', false);
- $config->set('Core', 'Encoding', 'UTF-8');
- $config->set('HTML', 'Doctype', 'XHTML 1.0 Transitional');
- $config->set('Cache', 'SerializerPath', $cachedir);
- $config->set('URI', 'AllowedSchemes', array('http'=>1, 'https'=>1, 'ftp'=>1, 'irc'=>1, 'nntp'=>1, 'news'=>1, 'rtsp'=>1, 'teamspeak'=>1, 'gopher'=>1, 'mms'=>1));
- $config->set('Attr', 'AllowedFrameTargets', array('_blank'));
+ $config->set('Core.ConvertDocumentToFragment', true);
+ $config->set('Core.Encoding', 'UTF-8');
+ $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
+ $config->set('Cache.SerializerPath', $cachedir);
+ $config->set('URI.AllowedSchemes', array('http'=>1, 'https'=>1, 'ftp'=>1, 'irc'=>1, 'nntp'=>1, 'news'=>1, 'rtsp'=>1, 'teamspeak'=>1, 'gopher'=>1, 'mms'=>1));
+ $config->set('Attr.AllowedFrameTargets', array('_blank'));
$purifier = new HTMLPurifier($config);
}
return $purifier->purify($text);