Rewrite FixNesting implementation to be tree-based.
[htmlpurifier.git] / library / HTMLPurifier / ChildDef / Table.php
blob3e4a0f21824fadf752c06ec6a0fa90be393c90bb
1 <?php
3 /**
4 * Definition for tables. The general idea is to extract out all of the
5 * essential bits, and then reconstruct it later.
7 * This is a bit confusing, because the DTDs and the W3C
8 * validators seem to disagree on the appropriate definition. The
9 * DTD claims:
11 * (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+)
13 * But actually, the HTML4 spec then has this to say:
15 * The TBODY start tag is always required except when the table
16 * contains only one table body and no table head or foot sections.
17 * The TBODY end tag may always be safely omitted.
19 * So the DTD is kind of wrong. The validator is, unfortunately, kind
20 * of on crack.
22 * The definition changed again in XHTML1.1; and in my opinion, this
23 * formulation makes the most sense.
25 * caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ ))
27 * Essentially, we have two modes: thead/tfoot/tbody mode, and tr mode.
28 * If we encounter a thead, tfoot or tbody, we are placed in the former
29 * mode, and we *must* wrap any stray tr segments with a tbody. But if
30 * we don't run into any of them, just have tr tags is OK.
32 class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
34 /**
35 * @type bool
37 public $allow_empty = false;
39 /**
40 * @type string
42 public $type = 'table';
44 /**
45 * @type array
47 public $elements = array(
48 'tr' => true,
49 'tbody' => true,
50 'thead' => true,
51 'tfoot' => true,
52 'caption' => true,
53 'colgroup' => true,
54 'col' => true
57 public function __construct()
61 /**
62 * @param array $children
63 * @param HTMLPurifier_Config $config
64 * @param HTMLPurifier_Context $context
65 * @return array
67 public function validateChildren($children, $config, $context)
69 if (empty($children)) {
70 return false;
73 // only one of these elements is allowed in a table
74 $caption = false;
75 $thead = false;
76 $tfoot = false;
78 // whitespace
79 $initial_ws = array();
80 $after_caption_ws = array();
81 $after_thead_ws = array();
82 $after_tfoot_ws = array();
84 // as many of these as you want
85 $cols = array();
86 $content = array();
88 $tbody_mode = false; // if true, then we need to wrap any stray
89 // <tr>s with a <tbody>.
91 $ws_accum =& $initial_ws;
93 foreach ($children as $node) {
94 if ($node instanceof HTMLPurifier_Node_Comment) {
95 $ws_accum[] = $node;
96 continue;
98 switch ($node->name) {
99 case 'tbody':
100 $tbody_mode = true;
101 // fall through
102 case 'tr':
103 $content[] = $node;
104 $ws_accum =& $content;
105 break;
106 case 'caption':
107 // there can only be one caption!
108 if ($caption !== false) break;
109 $caption = $node;
110 $ws_accum =& $after_caption_ws;
111 break;
112 case 'thead':
113 $tbody_mode = true;
114 // XXX This breaks rendering properties with
115 // Firefox, which never floats a <thead> to
116 // the top. Ever. (Our scheme will float the
117 // first <thead> to the top.) So maybe
118 // <thead>s that are not first should be
119 // turned into <tbody>? Very tricky, indeed.
120 if ($thead === false) {
121 $thead = $node;
122 $ws_accum =& $after_thead_ws;
123 } else {
124 // Oops, there's a second one! What
125 // should we do? Current behavior is to
126 // transmutate the first and last entries into
127 // tbody tags, and then put into content.
128 // Maybe a better idea is to *attach
129 // it* to the existing thead or tfoot?
130 // We don't do this, because Firefox
131 // doesn't float an extra tfoot to the
132 // bottom like it does for the first one.
133 $node->name = 'tbody';
134 $content[] = $node;
135 $ws_accum =& $content;
137 break;
138 case 'tfoot':
139 // see above for some aveats
140 $tbody_mode = true;
141 if ($tfoot === false) {
142 $tfoot = $node;
143 $ws_accum =& $after_tfoot_ws;
144 } else {
145 $node->name = 'tbody';
146 $content[] = $node;
147 $ws_accum =& $content;
149 break;
150 case 'colgroup':
151 case 'col':
152 $cols[] = $node;
153 $ws_accum =& $cols;
154 break;
155 case '#PCDATA':
156 // How is whitespace handled? We treat is as sticky to
157 // the *end* of the previous element. So all of the
158 // nonsense we have worked on is to keep things
159 // together.
160 if (!empty($node->is_whitespace)) {
161 $ws_accum[] = $node;
163 break;
167 if (empty($content)) {
168 return false;
171 $ret = $initial_ws;
172 if ($caption !== false) {
173 $ret[] = $caption;
174 $ret = array_merge($ret, $after_caption_ws);
176 if ($cols !== false) {
177 $ret = array_merge($ret, $cols);
179 if ($thead !== false) {
180 $ret[] = $thead;
181 $ret = array_merge($ret, $after_thead_ws);
183 if ($tfoot !== false) {
184 $ret[] = $tfoot;
185 $ret = array_merge($ret, $after_tfoot_ws);
188 if ($tbody_mode) {
189 // we have to shuffle tr into tbody
190 $current_tr_tbody = null;
192 foreach($content as $node) {
193 switch ($node->name) {
194 case 'tbody':
195 $current_tr_tbody = null;
196 $ret[] = $node;
197 break;
198 case 'tr':
199 if ($current_tr_tbody === null) {
200 $current_tr_tbody = new HTMLPurifier_Node_Element('tbody');
201 $ret[] = $current_tr_tbody;
203 $current_tr_tbody->children[] = $node;
204 break;
205 case '#PCDATA':
206 assert($node->is_whitespace);
207 if ($current_tr_tbody === null) {
208 $ret[] = $node;
209 } else {
210 $current_tr_tbody->children[] = $node;
212 break;
215 } else {
216 $ret = array_merge($ret, $content);
219 return $ret;
224 // vim: et sw=4 sts=4