225 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			PHP
		
	
	
		
		
			
		
	
	
			225 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			PHP
		
	
	
|  | <?php | ||
|  | 
 | ||
|  | /** | ||
|  |  * Definition for tables.  The general idea is to extract out all of the | ||
|  |  * essential bits, and then reconstruct it later. | ||
|  |  * | ||
|  |  * This is a bit confusing, because the DTDs and the W3C | ||
|  |  * validators seem to disagree on the appropriate definition. The | ||
|  |  * DTD claims: | ||
|  |  * | ||
|  |  *      (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+) | ||
|  |  * | ||
|  |  * But actually, the HTML4 spec then has this to say: | ||
|  |  * | ||
|  |  *      The TBODY start tag is always required except when the table | ||
|  |  *      contains only one table body and no table head or foot sections. | ||
|  |  *      The TBODY end tag may always be safely omitted. | ||
|  |  * | ||
|  |  * So the DTD is kind of wrong.  The validator is, unfortunately, kind | ||
|  |  * of on crack. | ||
|  |  * | ||
|  |  * The definition changed again in XHTML1.1; and in my opinion, this | ||
|  |  * formulation makes the most sense. | ||
|  |  * | ||
|  |  *      caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ )) | ||
|  |  * | ||
|  |  * Essentially, we have two modes: thead/tfoot/tbody mode, and tr mode. | ||
|  |  * If we encounter a thead, tfoot or tbody, we are placed in the former | ||
|  |  * mode, and we *must* wrap any stray tr segments with a tbody. But if | ||
|  |  * we don't run into any of them, just have tr tags is OK. | ||
|  |  */ | ||
|  | class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef | ||
|  | { | ||
|  |     /** | ||
|  |      * @type bool | ||
|  |      */ | ||
|  |     public $allow_empty = false; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * @type string | ||
|  |      */ | ||
|  |     public $type = 'table'; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * @type array | ||
|  |      */ | ||
|  |     public $elements = array( | ||
|  |         'tr' => true, | ||
|  |         'tbody' => true, | ||
|  |         'thead' => true, | ||
|  |         'tfoot' => true, | ||
|  |         'caption' => true, | ||
|  |         'colgroup' => true, | ||
|  |         'col' => true | ||
|  |     ); | ||
|  | 
 | ||
|  |     public function __construct() | ||
|  |     { | ||
|  |     } | ||
|  | 
 | ||
|  |     /** | ||
|  |      * @param array $children | ||
|  |      * @param HTMLPurifier_Config $config | ||
|  |      * @param HTMLPurifier_Context $context | ||
|  |      * @return array | ||
|  |      */ | ||
|  |     public function validateChildren($children, $config, $context) | ||
|  |     { | ||
|  |         if (empty($children)) { | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         // only one of these elements is allowed in a table
 | ||
|  |         $caption = false; | ||
|  |         $thead = false; | ||
|  |         $tfoot = false; | ||
|  | 
 | ||
|  |         // whitespace
 | ||
|  |         $initial_ws = array(); | ||
|  |         $after_caption_ws = array(); | ||
|  |         $after_thead_ws = array(); | ||
|  |         $after_tfoot_ws = array(); | ||
|  | 
 | ||
|  |         // as many of these as you want
 | ||
|  |         $cols = array(); | ||
|  |         $content = array(); | ||
|  | 
 | ||
|  |         $tbody_mode = false; // if true, then we need to wrap any stray
 | ||
|  |                              // <tr>s with a <tbody>.
 | ||
|  | 
 | ||
|  |         $ws_accum =& $initial_ws; | ||
|  | 
 | ||
|  |         foreach ($children as $node) { | ||
|  |             if ($node instanceof HTMLPurifier_Node_Comment) { | ||
|  |                 $ws_accum[] = $node; | ||
|  |                 continue; | ||
|  |             } | ||
|  |             switch ($node->name) { | ||
|  |             case 'tbody': | ||
|  |                 $tbody_mode = true; | ||
|  |                 // fall through
 | ||
|  |             case 'tr': | ||
|  |                 $content[] = $node; | ||
|  |                 $ws_accum =& $content; | ||
|  |                 break; | ||
|  |             case 'caption': | ||
|  |                 // there can only be one caption!
 | ||
|  |                 if ($caption !== false)  break; | ||
|  |                 $caption = $node; | ||
|  |                 $ws_accum =& $after_caption_ws; | ||
|  |                 break; | ||
|  |             case 'thead': | ||
|  |                 $tbody_mode = true; | ||
|  |                 // XXX This breaks rendering properties with
 | ||
|  |                 // Firefox, which never floats a <thead> to
 | ||
|  |                 // the top. Ever. (Our scheme will float the
 | ||
|  |                 // first <thead> to the top.)  So maybe
 | ||
|  |                 // <thead>s that are not first should be
 | ||
|  |                 // turned into <tbody>? Very tricky, indeed.
 | ||
|  |                 if ($thead === false) { | ||
|  |                     $thead = $node; | ||
|  |                     $ws_accum =& $after_thead_ws; | ||
|  |                 } else { | ||
|  |                     // Oops, there's a second one! What
 | ||
|  |                     // should we do?  Current behavior is to
 | ||
|  |                     // transmutate the first and last entries into
 | ||
|  |                     // tbody tags, and then put into content.
 | ||
|  |                     // Maybe a better idea is to *attach
 | ||
|  |                     // it* to the existing thead or tfoot?
 | ||
|  |                     // We don't do this, because Firefox
 | ||
|  |                     // doesn't float an extra tfoot to the
 | ||
|  |                     // bottom like it does for the first one.
 | ||
|  |                     $node->name = 'tbody'; | ||
|  |                     $content[] = $node; | ||
|  |                     $ws_accum =& $content; | ||
|  |                 } | ||
|  |                 break; | ||
|  |             case 'tfoot': | ||
|  |                 // see above for some aveats
 | ||
|  |                 $tbody_mode = true; | ||
|  |                 if ($tfoot === false) { | ||
|  |                     $tfoot = $node; | ||
|  |                     $ws_accum =& $after_tfoot_ws; | ||
|  |                 } else { | ||
|  |                     $node->name = 'tbody'; | ||
|  |                     $content[] = $node; | ||
|  |                     $ws_accum =& $content; | ||
|  |                 } | ||
|  |                 break; | ||
|  |             case 'colgroup': | ||
|  |             case 'col': | ||
|  |                 $cols[] = $node; | ||
|  |                 $ws_accum =& $cols; | ||
|  |                 break; | ||
|  |             case '#PCDATA': | ||
|  |                 // How is whitespace handled? We treat is as sticky to
 | ||
|  |                 // the *end* of the previous element. So all of the
 | ||
|  |                 // nonsense we have worked on is to keep things
 | ||
|  |                 // together.
 | ||
|  |                 if (!empty($node->is_whitespace)) { | ||
|  |                     $ws_accum[] = $node; | ||
|  |                 } | ||
|  |                 break; | ||
|  |             } | ||
|  |         } | ||
|  | 
 | ||
|  |         if (empty($content) && $thead === false && $tfoot === false) { | ||
|  |             return false; | ||
|  |         } | ||
|  | 
 | ||
|  |         $ret = $initial_ws; | ||
|  |         if ($caption !== false) { | ||
|  |             $ret[] = $caption; | ||
|  |             $ret = array_merge($ret, $after_caption_ws); | ||
|  |         } | ||
|  |         if ($cols !== false) { | ||
|  |             $ret = array_merge($ret, $cols); | ||
|  |         } | ||
|  |         if ($thead !== false) { | ||
|  |             $ret[] = $thead; | ||
|  |             $ret = array_merge($ret, $after_thead_ws); | ||
|  |         } | ||
|  |         if ($tfoot !== false) { | ||
|  |             $ret[] = $tfoot; | ||
|  |             $ret = array_merge($ret, $after_tfoot_ws); | ||
|  |         } | ||
|  | 
 | ||
|  |         if ($tbody_mode) { | ||
|  |             // we have to shuffle tr into tbody
 | ||
|  |             $current_tr_tbody = null; | ||
|  | 
 | ||
|  |             foreach($content as $node) { | ||
|  |                 switch ($node->name) { | ||
|  |                 case 'tbody': | ||
|  |                     $current_tr_tbody = null; | ||
|  |                     $ret[] = $node; | ||
|  |                     break; | ||
|  |                 case 'tr': | ||
|  |                     if ($current_tr_tbody === null) { | ||
|  |                         $current_tr_tbody = new HTMLPurifier_Node_Element('tbody'); | ||
|  |                         $ret[] = $current_tr_tbody; | ||
|  |                     } | ||
|  |                     $current_tr_tbody->children[] = $node; | ||
|  |                     break; | ||
|  |                 case '#PCDATA': | ||
|  |                     //assert($node->is_whitespace);
 | ||
|  |                     if ($current_tr_tbody === null) { | ||
|  |                         $ret[] = $node; | ||
|  |                     } else { | ||
|  |                         $current_tr_tbody->children[] = $node; | ||
|  |                     } | ||
|  |                     break; | ||
|  |                 } | ||
|  |             } | ||
|  |         } else { | ||
|  |             $ret = array_merge($ret, $content); | ||
|  |         } | ||
|  | 
 | ||
|  |         return $ret; | ||
|  | 
 | ||
|  |     } | ||
|  | } | ||
|  | 
 | ||
|  | // vim: et sw=4 sts=4
 |