1<?php
2
3/**
4 * Definition for tables.  The general idea is to extract out all of the
5 * essential bits, and then reconstruct it later.
6 *
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:
10 *
11 *      (CAPTION?, (COL*|COLGROUP*), THEAD?, TFOOT?, TBODY+)
12 *
13 * But actually, the HTML4 spec then has this to say:
14 *
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.
18 *
19 * So the DTD is kind of wrong.  The validator is, unfortunately, kind
20 * of on crack.
21 *
22 * The definition changed again in XHTML1.1; and in my opinion, this
23 * formulation makes the most sense.
24 *
25 *      caption?, ( col* | colgroup* ), (( thead?, tfoot?, tbody+ ) | ( tr+ ))
26 *
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.
31 */
32class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef
33{
34    /**
35     * @type bool
36     */
37    public $allow_empty = false;
38
39    /**
40     * @type string
41     */
42    public $type = 'table';
43
44    /**
45     * @type array
46     */
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
55    );
56
57    public function __construct()
58    {
59    }
60
61    /**
62     * @param array $children
63     * @param HTMLPurifier_Config $config
64     * @param HTMLPurifier_Context $context
65     * @return array
66     */
67    public function validateChildren($children, $config, $context)
68    {
69        if (empty($children)) {
70            return false;
71        }
72
73        // only one of these elements is allowed in a table
74        $caption = false;
75        $thead = false;
76        $tfoot = false;
77
78        // whitespace
79        $initial_ws = array();
80        $after_caption_ws = array();
81        $after_thead_ws = array();
82        $after_tfoot_ws = array();
83
84        // as many of these as you want
85        $cols = array();
86        $content = array();
87
88        $tbody_mode = false; // if true, then we need to wrap any stray
89                             // <tr>s with a <tbody>.
90
91        $ws_accum =& $initial_ws;
92
93        foreach ($children as $node) {
94            if ($node instanceof HTMLPurifier_Node_Comment) {
95                $ws_accum[] = $node;
96                continue;
97            }
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;
136                }
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;
148                }
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;
162                }
163                break;
164            }
165        }
166
167        if (empty($content)) {
168            return false;
169        }
170
171        $ret = $initial_ws;
172        if ($caption !== false) {
173            $ret[] = $caption;
174            $ret = array_merge($ret, $after_caption_ws);
175        }
176        if ($cols !== false) {
177            $ret = array_merge($ret, $cols);
178        }
179        if ($thead !== false) {
180            $ret[] = $thead;
181            $ret = array_merge($ret, $after_thead_ws);
182        }
183        if ($tfoot !== false) {
184            $ret[] = $tfoot;
185            $ret = array_merge($ret, $after_tfoot_ws);
186        }
187
188        if ($tbody_mode) {
189            // we have to shuffle tr into tbody
190            $current_tr_tbody = null;
191
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;
202                    }
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;
211                    }
212                    break;
213                }
214            }
215        } else {
216            $ret = array_merge($ret, $content);
217        }
218
219        return $ret;
220
221    }
222}
223
224// vim: et sw=4 sts=4
225