Tighten up invariants.
[htmlpurifier.git] / library / HTMLPurifier / ChildDef / Table.php
blob26d184d2e5dbc9e684937cafd1089e47c2c00ab4
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 public $allow_empty = false;
35 public $type = 'table';
36 public $elements = array('tr' => true, 'tbody' => true, 'thead' => true,
37 'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true);
38 public function __construct() {}
39 public function validateChildren($tokens_of_children, $config, $context) {
40 if (empty($tokens_of_children)) return false;
42 // this ensures that the loop gets run one last time before closing
43 // up. It's a little bit of a hack, but it works! Just make sure you
44 // get rid of the token later.
45 $tokens_of_children[] = false;
47 // only one of these elements is allowed in a table
48 $caption = false;
49 $thead = false;
50 $tfoot = false;
52 // as many of these as you want
53 $cols = array();
54 $content = array();
56 $nesting = 0; // current depth so we can determine nodes
57 $is_collecting = false; // are we globbing together tokens to package
58 // into one of the collectors?
59 $collection = array(); // collected nodes
60 // INVARIANT: if $is_collecting, then !empty($collection)
61 // The converse does NOT hold, see [WHITESPACE]
62 $tag_index = 0; // the first node might be whitespace,
63 // so this tells us where the start tag is
64 $tbody_mode = false; // if true, then we need to wrap any stray
65 // <tr>s with a <tbody>.
67 foreach ($tokens_of_children as $token) {
68 $is_child = ($nesting == 0);
70 if ($token === false) {
71 // terminating sequence started
72 } elseif ($token instanceof HTMLPurifier_Token_Start) {
73 $nesting++;
74 } elseif ($token instanceof HTMLPurifier_Token_End) {
75 $nesting--;
78 // handle node collection
79 if ($is_collecting) {
80 if ($is_child) {
81 // okay, let's stash the tokens away
82 // first token tells us the type of the collection
83 switch ($collection[$tag_index]->name) {
84 case 'tbody':
85 $tbody_mode = true;
86 // fall through
87 case 'tr':
88 $content[] = $collection;
89 break;
90 case 'caption':
91 if ($caption !== false) break;
92 $caption = $collection;
93 break;
94 case 'thead':
95 case 'tfoot':
96 $tbody_mode = true;
97 // XXX This breaks rendering properties with
98 // Firefox, which never floats a <thead> to
99 // the top. Ever. (Our scheme will float the
100 // first <thead> to the top.) So maybe
101 // <thead>s that are not first should be
102 // turned into <tbody>? Very tricky, indeed.
104 // access the appropriate variable, $thead or $tfoot
105 $var = $collection[$tag_index]->name;
106 if ($$var === false) {
107 $$var = $collection;
108 } else {
109 // Oops, there's a second one! What
110 // should we do? Current behavior is to
111 // transmutate the first and last entries into
112 // tbody tags, and then put into content.
113 // Maybe a better idea is to *attach
114 // it* to the existing thead or tfoot?
115 // We don't do this, because Firefox
116 // doesn't float an extra tfoot to the
117 // bottom like it does for the first one.
118 $collection[$tag_index]->name = 'tbody';
119 $collection[count($collection)-1]->name = 'tbody';
120 $content[] = $collection;
122 break;
123 case 'colgroup':
124 $cols[] = $collection;
125 break;
127 $collection = array();
128 $is_collecting = false;
129 $tag_index = 0;
130 } else {
131 // add the node to the collection
132 $collection[] = $token;
136 // terminate
137 if ($token === false) break;
139 if ($is_child) {
140 // determine what we're dealing with
141 if ($token->name == 'col') {
142 // the only empty tag in the possie, we can handle it
143 // immediately
144 $cols[] = array_merge($collection, array($token));
145 $collection = array();
146 $is_collecting = false;
147 $tag_index = 0;
148 continue;
150 switch($token->name) {
151 case 'caption':
152 case 'colgroup':
153 case 'thead':
154 case 'tfoot':
155 case 'tbody':
156 case 'tr':
157 $is_collecting = true;
158 $collection[] = $token;
159 continue;
160 default:
161 // [WHITESPACE] Whitespace is added to the
162 // collection without triggering collection
163 // mode. This is a hack to make whitespace
164 // 'sticky' (that is to say, we ought /not/ to
165 // drop whitespace.)
166 if (!empty($token->is_whitespace)) {
167 $collection[] = $token;
168 $tag_index++;
170 continue;
175 if (empty($content)) return false;
176 // INVARIANT: all members of content are non-empty. This can
177 // be shown by observing when things are pushed onto content:
178 // they are only ever pushed when is_collecting is true, and
179 // collection is the only thing ever pushed; but it is known
180 // that collections are non-empty when is_collecting is true.
182 $ret = array();
183 if ($caption !== false) $ret = array_merge($ret, $caption);
184 if ($cols !== false) foreach ($cols as $token_array) $ret = array_merge($ret, $token_array);
185 if ($thead !== false) $ret = array_merge($ret, $thead);
186 if ($tfoot !== false) $ret = array_merge($ret, $tfoot);
188 if ($tbody_mode) {
189 // a little tricky, since the start of the collection may be
190 // whitespace
191 $inside_tbody = false;
192 foreach ($content as $token_array) {
193 // find the starting token
194 // INVARIANT: token_array is not empty
195 $t = NULL;
196 foreach ($token_array as $t) {
197 if ($t->name === 'tr' || $t->name === 'tbody') {
198 break;
200 } // iterator variable carries over
201 if ($t->name === 'tr') {
202 if ($inside_tbody) {
203 $ret = array_merge($ret, $token_array);
204 } else {
205 $ret[] = new HTMLPurifier_Token_Start('tbody');
206 $ret = array_merge($ret, $token_array);
207 $inside_tbody = true;
209 } elseif ($t->name === 'tbody') {
210 if ($inside_tbody) {
211 $ret[] = new HTMLPurifier_Token_End('tbody');
212 $inside_tbody = false;
213 $ret = array_merge($ret, $token_array);
214 } else {
215 $ret = array_merge($ret, $token_array);
217 } else {
218 trigger_error("tr/tbody in content invariant failed in Table ChildDef", E_USER_ERROR);
221 if ($inside_tbody) {
222 $ret[] = new HTMLPurifier_Token_End('tbody');
224 } else {
225 foreach ($content as $token_array) {
226 // invariant: everything in here is <tr>s
227 $ret = array_merge($ret, $token_array);
231 if (!empty($collection) && $is_collecting == false){
232 // grab the trailing space
233 $ret = array_merge($ret, $collection);
236 array_pop($tokens_of_children); // remove phantom token
238 return ($ret === $tokens_of_children) ? true : $ret;
243 // vim: et sw=4 sts=4