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
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
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
52 // as many of these as you want
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
) {
74 } elseif ($token instanceof HTMLPurifier_Token_End
) {
78 // handle node collection
81 // okay, let's stash the tokens away
82 // first token tells us the type of the collection
83 switch ($collection[$tag_index]->name
) {
88 $content[] = $collection;
91 if ($caption !== false) break;
92 $caption = $collection;
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) {
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;
124 $cols[] = $collection;
127 $collection = array();
128 $is_collecting = false;
131 // add the node to the collection
132 $collection[] = $token;
137 if ($token === false) break;
140 // determine what we're dealing with
141 if ($token->name
== 'col') {
142 // the only empty tag in the possie, we can handle it
144 $cols[] = array_merge($collection, array($token));
145 $collection = array();
146 $is_collecting = false;
150 switch($token->name
) {
157 $is_collecting = true;
158 $collection[] = $token;
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
166 if (!empty($token->is_whitespace
)) {
167 $collection[] = $token;
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.
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);
189 // a little tricky, since the start of the collection may be
191 $inside_tbody = false;
192 foreach ($content as $token_array) {
193 // find the starting token
194 // INVARIANT: token_array is not empty
196 foreach ($token_array as $t) {
197 if ($t->name
=== 'tr' ||
$t->name
=== 'tbody') {
200 } // iterator variable carries over
201 if ($t->name
=== 'tr') {
203 $ret = array_merge($ret, $token_array);
205 $ret[] = new HTMLPurifier_Token_Start('tbody');
206 $ret = array_merge($ret, $token_array);
207 $inside_tbody = true;
209 } elseif ($t->name
=== 'tbody') {
211 $ret[] = new HTMLPurifier_Token_End('tbody');
212 $inside_tbody = false;
213 $ret = array_merge($ret, $token_array);
215 $ret = array_merge($ret, $token_array);
218 trigger_error("tr/tbody in content invariant failed in Table ChildDef", E_USER_ERROR
);
222 $ret[] = new HTMLPurifier_Token_End('tbody');
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