Source for file PH5P.php

Documentation is available at PH5P.php

  1. <?php
  2.  
  3. /**
  4.  * Experimental HTML5-based parser using Jeroen van der Meer's PH5P library.
  5.  * Occupies space in the HTML5 pseudo-namespace, which may cause conflicts.
  6.  * 
  7.  * @note
  8.  *     Recent changes to PHP's DOM extension have resulted in some fatal
  9.  *     error conditions with the original version of PH5P. Pending changes,
  10.  *     this lexer will punt to DirectLex if DOM throughs an exception.
  11.  */
  12.  
  13.     
  14.     public function tokenizeHTML($html$config$context{
  15.         $new_html $this->normalize($html$config$context);
  16.         $new_html $this->wrapHTML($new_html$config$context);
  17.         try {
  18.             $parser new HTML5($new_html);
  19.             $doc $parser->save();
  20.         catch (DOMException $e{
  21.             // Uh oh, it failed. Punt to DirectLex.
  22.             $lexer new HTMLPurifier_Lexer_DirectLex();
  23.             $context->register('PH5PError'$e)// save the error, so we can detect it
  24.             return $lexer->tokenizeHTML($html$config$context)// use original HTML
  25.         }
  26.         $tokens array();
  27.         $this->tokenizeDOM(
  28.             $doc->getElementsByTagName('html')->item(0)-> // <html>
  29.                   getElementsByTagName('body')->item(0)-> //   <body>
  30.                   getElementsByTagName('div')->item(0)    //     <div>
  31.             $tokens);
  32.         return $tokens;
  33.     }
  34.     
  35. }
  36.  
  37. /*
  38.  
  39. Copyright 2007 Jeroen van der Meer <http://jero.net/> 
  40.  
  41. Permission is hereby granted, free of charge, to any person obtaining a 
  42. copy of this software and associated documentation files (the 
  43. "Software"), to deal in the Software without restriction, including 
  44. without limitation the rights to use, copy, modify, merge, publish, 
  45. distribute, sublicense, and/or sell copies of the Software, and to 
  46. permit persons to whom the Software is furnished to do so, subject to 
  47. the following conditions: 
  48.  
  49. The above copyright notice and this permission notice shall be included 
  50. in all copies or substantial portions of the Software. 
  51.  
  52. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
  53. OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
  54. MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
  55. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
  56. CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
  57. TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
  58. SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
  59.  
  60. */
  61.  
  62. class HTML5 {
  63.     private $data;
  64.     private $char;
  65.     private $EOF;
  66.     private $state;
  67.     private $tree;
  68.     private $token;
  69.     private $content_model;
  70.     private $escape false;
  71.     private $entities array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute',
  72.     'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;',
  73.     'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;',
  74.     'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;',
  75.     'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;',
  76.     'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;',
  77.     'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;',
  78.     'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;',
  79.     'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;',
  80.     'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN',
  81.     'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;',
  82.     'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;',
  83.     'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig',
  84.     'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;',
  85.     'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;',
  86.     'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil',
  87.     'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;',
  88.     'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;',
  89.     'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;',
  90.     'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth',
  91.     'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12',
  92.     'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt',
  93.     'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc',
  94.     'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;',
  95.     'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;',
  96.     'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;',
  97.     'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro',
  98.     'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;',
  99.     'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;',
  100.     'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;',
  101.     'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash',
  102.     'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;',
  103.     'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;',
  104.     'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;',
  105.     'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;',
  106.     'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;',
  107.     'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;',
  108.     'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;',
  109.     'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;',
  110.     'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc',
  111.     'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;',
  112.     'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;');
  113.  
  114.     const PCDATA    = 0;
  115.     const RCDATA    = 1;
  116.     const CDATA     = 2;
  117.     const PLAINTEXT = 3;
  118.  
  119.     const DOCTYPE  = 0;
  120.     const STARTTAG = 1;
  121.     const ENDTAG   = 2;
  122.     const COMMENT  = 3;
  123.     const CHARACTR = 4;
  124.     const EOF      = 5;
  125.  
  126.     public function __construct($data{
  127.         $data str_replace("\r\n""\n"$data);
  128.         $data str_replace("\r"null$data);
  129.  
  130.         $this->data $data;
  131.         $this->char = -1;
  132.         $this->EOF  strlen($data);
  133.         $this->tree new HTML5TreeConstructer;
  134.         $this->content_model self::PCDATA;
  135.  
  136.         $this->state 'data';
  137.  
  138.         while($this->state !== null{
  139.             $this->{$this->state.'State'}();
  140.         }
  141.     }
  142.  
  143.     public function save({
  144.         return $this->tree->save();
  145.     }
  146.  
  147.     private function char({
  148.         return ($this->char $this->EOF)
  149.             ? $this->data[$this->char]
  150.             : false;
  151.     }
  152.  
  153.     private function character($s$l 0{
  154.         if($s $l $this->EOF{
  155.             if($l === 0{
  156.                 return $this->data[$s];
  157.             else {
  158.                 return substr($this->data$s$l);
  159.             }
  160.         }
  161.     }
  162.  
  163.     private function characters($char_class$start{
  164.         return preg_replace('#^(['.$char_class.']+).*#s''\\1'substr($this->data$start));
  165.     }
  166.  
  167.     private function dataState({
  168.         // Consume the next input character
  169.         $this->char++;
  170.         $char $this->char();
  171.  
  172.         if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) {
  173.             /* U+0026 AMPERSAND (&)
  174.             When the content model flag is set to one of the PCDATA or RCDATA
  175.             states: switch to the entity data state. Otherwise: treat it as per
  176.             the "anything else"    entry below. */
  177.             $this->state 'entityData';
  178.  
  179.         elseif($char === '-'{
  180.             /* If the content model flag is set to either the RCDATA state or
  181.             the CDATA state, and the escape flag is false, and there are at
  182.             least three characters before this one in the input stream, and the
  183.             last four characters in the input stream, including this one, are
  184.             U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS,
  185.             and U+002D HYPHEN-MINUS ("<!--"), then set the escape flag to true. */
  186.             if(($this->content_model === self::RCDATA || $this->content_model ===
  187.             self::CDATA&& $this->escape === false &&
  188.             $this->char >= && $this->character($this->char 44=== '<!--'{
  189.                 $this->escape true;
  190.             }
  191.  
  192.             /* In any case, emit the input character as a character token. Stay
  193.             in the data state. */
  194.             $this->emitToken(array(
  195.                 'type' => self::CHARACTR,
  196.                 'data' => $char
  197.             ));
  198.  
  199.         /* U+003C LESS-THAN SIGN (<) */
  200.         elseif($char === '<' && ($this->content_model === self::PCDATA ||
  201.         (($this->content_model === self::RCDATA ||
  202.         $this->content_model === self::CDATA&& $this->escape === false))) {
  203.             /* When the content model flag is set to the PCDATA state: switch
  204.             to the tag open state.
  205.  
  206.             When the content model flag is set to either the RCDATA state or
  207.             the CDATA state and the escape flag is false: switch to the tag
  208.             open state.
  209.  
  210.             Otherwise: treat it as per the "anything else" entry below. */
  211.             $this->state 'tagOpen';
  212.  
  213.         /* U+003E GREATER-THAN SIGN (>) */
  214.         elseif($char === '>'{
  215.             /* If the content model flag is set to either the RCDATA state or
  216.             the CDATA state, and the escape flag is true, and the last three
  217.             characters in the input stream including this one are U+002D
  218.             HYPHEN-MINUS, U+002D HYPHEN-MINUS, U+003E GREATER-THAN SIGN ("-->"),
  219.             set the escape flag to false. */
  220.             if(($this->content_model === self::RCDATA ||
  221.             $this->content_model === self::CDATA&& $this->escape === true &&
  222.             $this->character($this->char3=== '-->'{
  223.                 $this->escape false;
  224.             }
  225.  
  226.             /* In any case, emit the input character as a character token.
  227.             Stay in the data state. */
  228.             $this->emitToken(array(
  229.                 'type' => self::CHARACTR,
  230.                 'data' => $char
  231.             ));
  232.  
  233.         elseif($this->char === $this->EOF{
  234.             /* EOF
  235.             Emit an end-of-file token. */
  236.             $this->EOF();
  237.  
  238.         elseif($this->content_model === self::PLAINTEXT{
  239.             /* When the content model flag is set to the PLAINTEXT state
  240.             THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of
  241.             the text and emit it as a character token. */
  242.             $this->emitToken(array(
  243.                 'type' => self::CHARACTR,
  244.                 'data' => substr($this->data$this->char)
  245.             ));
  246.  
  247.             $this->EOF();
  248.  
  249.         else {
  250.             /* Anything else
  251.             THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that
  252.             otherwise would also be treated as a character token and emit it
  253.             as a single character token. Stay in the data state. */
  254.             $len  strcspn($this->data'<&'$this->char);
  255.             $char substr($this->data$this->char$len);
  256.             $this->char += $len 1;
  257.  
  258.             $this->emitToken(array(
  259.                 'type' => self::CHARACTR,
  260.                 'data' => $char
  261.             ));
  262.  
  263.             $this->state 'data';
  264.         }
  265.     }
  266.  
  267.     private function entityDataState({
  268.         // Attempt to consume an entity.
  269.         $entity $this->entity();
  270.  
  271.         // If nothing is returned, emit a U+0026 AMPERSAND character token.
  272.         // Otherwise, emit the character token that was returned.
  273.         $char (!$entity'&' $entity;
  274.         $this->emitToken(array(
  275.             'type' => self::CHARACTR,
  276.             'data' => $char
  277.         ));
  278.  
  279.         // Finally, switch to the data state.
  280.         $this->state 'data';
  281.     }
  282.  
  283.     private function tagOpenState({
  284.         switch($this->content_model{
  285.             case self::RCDATA:
  286.             case self::CDATA:
  287.                 /* If the next input character is a U+002F SOLIDUS (/) character,
  288.                 consume it and switch to the close tag open state. If the next
  289.                 input character is not a U+002F SOLIDUS (/) character, emit a
  290.                 U+003C LESS-THAN SIGN character token and switch to the data
  291.                 state to process the next input character. */
  292.                 if($this->character($this->char 1=== '/'{
  293.                     $this->char++;
  294.                     $this->state 'closeTagOpen';
  295.  
  296.                 else {
  297.                     $this->emitToken(array(
  298.                         'type' => self::CHARACTR,
  299.                         'data' => '<'
  300.                     ));
  301.  
  302.                     $this->state 'data';
  303.                 }
  304.             break;
  305.  
  306.             case self::PCDATA:
  307.                 // If the content model flag is set to the PCDATA state
  308.                 // Consume the next input character:
  309.                 $this->char++;
  310.                 $char $this->char();
  311.  
  312.                 if($char === '!'{
  313.                     /* U+0021 EXCLAMATION MARK (!)
  314.                     Switch to the markup declaration open state. */
  315.                     $this->state 'markupDeclarationOpen';
  316.  
  317.                 elseif($char === '/'{
  318.                     /* U+002F SOLIDUS (/)
  319.                     Switch to the close tag open state. */
  320.                     $this->state 'closeTagOpen';
  321.  
  322.                 elseif(preg_match('/^[A-Za-z]$/'$char)) {
  323.                     /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
  324.                     Create a new start tag token, set its tag name to the lowercase
  325.                     version of the input character (add 0x0020 to the character's code
  326.                     point), then switch to the tag name state. (Don't emit the token
  327.                     yet; further details will be filled in before it is emitted.) */
  328.                     $this->token array(
  329.                         'name'  => strtolower($char),
  330.                         'type'  => self::STARTTAG,
  331.                         'attr'  => array()
  332.                     );
  333.  
  334.                     $this->state 'tagName';
  335.  
  336.                 elseif($char === '>'{
  337.                     /* U+003E GREATER-THAN SIGN (>)
  338.                     Parse error. Emit a U+003C LESS-THAN SIGN character token and a
  339.                     U+003E GREATER-THAN SIGN character token. Switch to the data state. */
  340.                     $this->emitToken(array(
  341.                         'type' => self::CHARACTR,
  342.                         'data' => '<>'
  343.                     ));
  344.  
  345.                     $this->state 'data';
  346.  
  347.                 elseif($char === '?'{
  348.                     /* U+003F QUESTION MARK (?)
  349.                     Parse error. Switch to the bogus comment state. */
  350.                     $this->state 'bogusComment';
  351.  
  352.                 else {
  353.                     /* Anything else
  354.                     Parse error. Emit a U+003C LESS-THAN SIGN character token and
  355.                     reconsume the current input character in the data state. */
  356.                     $this->emitToken(array(
  357.                         'type' => self::CHARACTR,
  358.                         'data' => '<'
  359.                     ));
  360.  
  361.                     $this->char--;
  362.                     $this->state 'data';
  363.                 }
  364.             break;
  365.         }
  366.     }
  367.  
  368.     private function closeTagOpenState({
  369.         $next_node strtolower($this->characters('A-Za-z'$this->char 1));
  370.         $the_same count($this->tree->stack&& $next_node === end($this->tree->stack)->nodeName;
  371.  
  372.         if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA&&
  373.         (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/',
  374.         $this->character($this->char strlen($next_node))) || $this->EOF === $this->char)))) {
  375.             /* If the content model flag is set to the RCDATA or CDATA states then
  376.             examine the next few characters. If they do not match the tag name of
  377.             the last start tag token emitted (case insensitively), or if they do but
  378.             they are not immediately followed by one of the following characters:
  379.                 * U+0009 CHARACTER TABULATION
  380.                 * U+000A LINE FEED (LF)
  381.                 * U+000B LINE TABULATION
  382.                 * U+000C FORM FEED (FF)
  383.                 * U+0020 SPACE
  384.                 * U+003E GREATER-THAN SIGN (>)
  385.                 * U+002F SOLIDUS (/)
  386.                 * EOF
  387.             ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character
  388.             token, a U+002F SOLIDUS character token, and switch to the data state
  389.             to process the next input character. */
  390.             $this->emitToken(array(
  391.                 'type' => self::CHARACTR,
  392.                 'data' => '</'
  393.             ));
  394.  
  395.             $this->state 'data';
  396.  
  397.         else {
  398.             /* Otherwise, if the content model flag is set to the PCDATA state,
  399.             or if the next few characters do match that tag name, consume the
  400.             next input character: */
  401.             $this->char++;
  402.             $char $this->char();
  403.  
  404.             if(preg_match('/^[A-Za-z]$/'$char)) {
  405.                 /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z
  406.                 Create a new end tag token, set its tag name to the lowercase version
  407.                 of the input character (add 0x0020 to the character's code point), then
  408.                 switch to the tag name state. (Don't emit the token yet; further details
  409.                 will be filled in before it is emitted.) */
  410.                 $this->token array(
  411.                     'name'  => strtolower($char),
  412.                     'type'  => self::ENDTAG
  413.                 );
  414.  
  415.                 $this->state 'tagName';
  416.  
  417.             elseif($char === '>'{
  418.                 /* U+003E GREATER-THAN SIGN (>)
  419.                 Parse error. Switch to the data state. */
  420.                 $this->state 'data';
  421.  
  422.             elseif($this->char === $this->EOF{
  423.                 /* EOF
  424.                 Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F
  425.                 SOLIDUS character token. Reconsume the EOF character in the data state. */
  426.                 $this->emitToken(array(
  427.      &