Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
121 / 121
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
InTable
100.00% covered (success)
100.00%
121 / 121
100.00% covered (success)
100.00%
4 / 4
41
100.00% covered (success)
100.00%
1 / 1
 characters
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 startTag
100.00% covered (success)
100.00%
74 / 74
100.00% covered (success)
100.00%
1 / 1
22
 endTag
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
16
 endDocument
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace Wikimedia\RemexHtml\TreeBuilder;
4
5use Wikimedia\RemexHtml\Tokenizer\Attributes;
6use Wikimedia\RemexHtml\Tokenizer\PlainAttributes;
7
8/**
9 * The "in table" insertion mode
10 */
11class InTable extends InsertionMode {
12    /**
13     * The tag names that are cleared when we "clear the stack back to a table context"
14     * @var array<string,bool>
15     */
16    private static $tableContext = [
17        'table' => true,
18        'template' => true,
19        'html' => true
20    ];
21
22    public function characters( $text, $start, $length, $sourceStart, $sourceLength ) {
23        $allowed = [
24            'table' => true,
25            'tbody' => true,
26            'tfoot' => true,
27            'thead' => true,
28            'tr' => true
29        ];
30        if ( isset( $allowed[$this->builder->stack->current->htmlName] ) ) {
31            $this->builder->pendingTableCharacters = [];
32            $this->dispatcher->switchAndSave( Dispatcher::IN_TABLE_TEXT )
33                ->characters( $text, $start, $length, $sourceStart, $sourceLength );
34        } else {
35            $this->builder->error( 'unexpected text in table, fostering', $sourceStart );
36            $this->builder->fosterParenting = true;
37            $this->dispatcher->inBody->characters(
38                $text, $start, $length, $sourceStart, $sourceLength );
39            $this->builder->fosterParenting = false;
40        }
41    }
42
43    public function startTag( $name, Attributes $attrs, $selfClose, $sourceStart, $sourceLength ) {
44        $builder = $this->builder;
45        $dispatcher = $this->dispatcher;
46        $stack = $builder->stack;
47
48        switch ( $name ) {
49            case 'caption':
50                $builder->clearStackBack( self::$tableContext, $sourceStart );
51                $builder->afe->insertMarker();
52                $dispatcher->switchMode( Dispatcher::IN_CAPTION );
53                $builder->insertElement( $name, $attrs, false,
54                    $sourceStart, $sourceLength );
55                break;
56
57            case 'colgroup':
58                $builder->clearStackBack( self::$tableContext, $sourceStart );
59                $dispatcher->switchMode( Dispatcher::IN_COLUMN_GROUP );
60                $builder->insertElement( $name, $attrs, false,
61                    $sourceStart, $sourceLength );
62                break;
63
64            case 'col':
65                $builder->clearStackBack( self::$tableContext, $sourceStart );
66                $builder->insertElement( 'colgroup', new PlainAttributes, false,
67                    $sourceStart, 0 );
68                $dispatcher->switchMode( Dispatcher::IN_COLUMN_GROUP )
69                    ->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
70                break;
71
72            case 'tbody':
73            case 'tfoot':
74            case 'thead':
75                $builder->clearStackBack( self::$tableContext, $sourceStart );
76                $builder->insertElement( $name, $attrs, false,
77                    $sourceStart, $sourceLength );
78                $dispatcher->switchMode( Dispatcher::IN_TABLE_BODY );
79                break;
80
81            case 'td':
82            case 'th':
83            case 'tr':
84                $builder->clearStackBack( self::$tableContext, $sourceStart );
85                $builder->insertElement( 'tbody', new PlainAttributes, false,
86                    $sourceStart, $sourceLength );
87                $dispatcher->switchMode( Dispatcher::IN_TABLE_BODY )
88                    ->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
89                break;
90
91            case 'table':
92                $builder->error( 'unexpected <table> in table', $sourceStart );
93                if ( !$stack->isInTableScope( 'table' ) ) {
94                    // Ignore
95                    break;
96                }
97                $builder->popAllUpToName( 'table', $sourceStart, 0 );
98                $dispatcher->reset()
99                ->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
100                break;
101
102            case 'style':
103            case 'script':
104            case 'template':
105                $dispatcher->inHead->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
106                break;
107
108            case 'form':
109                if ( $stack->hasTemplate() || $builder->formElement !== null ) {
110                    $builder->error( 'invalid form in table, ignoring', $sourceStart );
111                    // Ignore
112                    break;
113                }
114                $builder->error( 'invalid form in table, inserting void element', $sourceStart );
115                $elt = $builder->insertElement( 'form', $attrs, true,
116                    $sourceStart, $sourceLength );
117                $builder->formElement = $elt;
118                break;
119
120            case 'input':
121                if ( isset( $attrs['type'] ) && strcasecmp( $attrs['type'], 'hidden' ) === 0 ) {
122                    $builder->error( 'begrudgingly accepting a hidden input in table mode',
123                        $sourceStart );
124                    $dispatcher->ack = true;
125                    $builder->insertElement( $name, $attrs, true, $sourceStart, $sourceLength );
126                    break;
127                }
128                // Fall through
129
130            default:
131                $builder->error( 'invalid start tag in table, fostering', $sourceStart );
132                $builder->fosterParenting = true;
133                $dispatcher->inBody->startTag( $name, $attrs, $selfClose, $sourceStart, $sourceLength );
134                $builder->fosterParenting = false;
135                break;
136        }
137    }
138
139    public function endTag( $name, $sourceStart, $sourceLength ) {
140        $builder = $this->builder;
141        $stack = $builder->stack;
142        $dispatcher = $this->dispatcher;
143
144        switch ( $name ) {
145            case 'table':
146                if ( !$stack->isInTableScope( 'table' ) ) {
147                    $builder->error( '</table> found but no table element in scope, ignoring', $sourceStart );
148                    // Ignore
149                    break;
150                }
151                $builder->popAllUpToName( 'table', $sourceStart, $sourceLength );
152                $dispatcher->reset();
153                break;
154
155            case 'body':
156            case 'caption':
157            case 'col':
158            case 'colgroup':
159            case 'html':
160            case 'tbody':
161            case 'td':
162            case 'tfoot':
163            case 'th':
164            case 'thead':
165            case 'tr':
166                $builder->error( 'ignoring invalid end tag inside table', $sourceStart );
167                break;
168
169            case 'template':
170                $dispatcher->inHead->endTag( $name, $sourceStart, $sourceLength );
171                break;
172
173            default:
174                $builder->error( 'unexpected end tag in table, fostering', $sourceStart );
175                $builder->fosterParenting = true;
176                $dispatcher->inBody->endTag( $name, $sourceStart, $sourceLength );
177                $builder->fosterParenting = false;
178        }
179    }
180
181    public function endDocument( $pos ) {
182        $this->dispatcher->inBody->endDocument( $pos );
183    }
184}