Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TokenCollector
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 8
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 type
n/a
0 / 0
n/a
0 / 0
0
 name
n/a
0 / 0
n/a
0 / 0
0
 toEnd
n/a
0 / 0
n/a
0 / 0
0
 ackEnd
n/a
0 / 0
n/a
0 / 0
0
 transformation
n/a
0 / 0
n/a
0 / 0
0
 onDelimiterToken
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
182
 onAnyToken
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 buildMetaToken
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 buildStrippedMetaToken
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 onTag
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 onEnd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 onAny
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\TT;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\Parsoid\NodeData\DataParsoid;
8use Wikimedia\Parsoid\Tokens\EndTagTk;
9use Wikimedia\Parsoid\Tokens\EOFTk;
10use Wikimedia\Parsoid\Tokens\KV;
11use Wikimedia\Parsoid\Tokens\SelfclosingTagTk;
12use Wikimedia\Parsoid\Tokens\SourceRange;
13use Wikimedia\Parsoid\Tokens\TagTk;
14use Wikimedia\Parsoid\Tokens\Token;
15use Wikimedia\Parsoid\Utils\PHPUtils;
16use Wikimedia\Parsoid\Wt2Html\TokenTransformManager;
17
18/**
19 * Small utility class that encapsulates the common 'collect all tokens
20 * starting from a token of type x until token of type y or (optionally) the
21 * end-of-input'.
22 */
23abstract class TokenCollector extends TokenHandler {
24    protected $scopeStack;
25
26    /**
27     * @param TokenTransformManager $manager manager enviroment
28     * @param array $options various configuration options
29     */
30    public function __construct( TokenTransformManager $manager, array $options ) {
31        parent::__construct( $manager, $options );
32        $this->onAnyEnabled = false;
33        $this->scopeStack = [];
34    }
35
36    /**
37     * Token type to register for ('tag', 'text' etc)
38     * @return string
39     */
40    abstract protected function type(): string;
41
42    /**
43     * (optional, only for token type 'tag'): tag name.
44     * @return string
45     */
46    abstract protected function name(): string;
47
48    /**
49     * Match the 'end' tokens as closing tag as well (accept unclosed sections).
50     * @return bool
51     */
52    abstract protected function toEnd(): bool;
53
54    /**
55     * Whether to transform unmatched end tags. If this returns true,
56     * unbalanced end tags will be passed to transform(). If it returns false,
57     * they will be left in the token stream unmodified.
58     *
59     * @return bool
60     */
61    abstract protected function ackEnd(): bool;
62
63    /**
64     * When an end delimiter is found, this function is called with the
65     * collected token array including the start and end delimiters. The
66     * subclass should transform it and return the result.
67     *
68     * @param array $array
69     * @return TokenHandlerResult
70     */
71    abstract protected function transformation( array $array ): TokenHandlerResult;
72
73    /**
74     * Handle the delimiter token.
75     * XXX: Adjust to sync phase callback when that is modified!
76     * @param Token $token
77     * @return TokenHandlerResult|null
78     */
79    private function onDelimiterToken( Token $token ): ?TokenHandlerResult {
80        $haveOpenTag = count( $this->scopeStack ) > 0;
81        if ( $token instanceof TagTk ) {
82            if ( count( $this->scopeStack ) === 0 ) {
83                $this->onAnyEnabled = true;
84                // Set up transforms
85                $this->env->log( 'debug', 'starting collection on ', $token );
86            }
87
88            // Push a new scope
89            $newScope = [];
90            $this->scopeStack[] = &$newScope;
91            $newScope[] = $token;
92
93            return new TokenHandlerResult( [] );
94        } elseif ( $token instanceof SelfclosingTagTk ) {
95            // We need to handle <ref /> for example, so call the handler.
96            return $this->transformation( [ $token, $token ] );
97        } elseif ( $haveOpenTag ) {
98            // EOFTk or EndTagTk
99            $this->env->log( 'debug', 'finishing collection on ', $token );
100
101            // Pop top scope and push token onto it
102            $activeTokens = array_pop( $this->scopeStack );
103            $activeTokens[] = $token;
104
105            if ( $token instanceof EndTagTk ) {
106                // Transformation receives all collected tokens instead of a single token.
107                $res = $this->transformation( $activeTokens );
108
109                if ( count( $this->scopeStack ) === 0 ) {
110                    $this->onAnyEnabled = false;
111                    return $res;
112                } else {
113                    // Merge tokens onto parent scope and return [].
114                    // Only when we hit the bottom of the stack,
115                    // we will return the collapsed token stream.
116                    $topScope = array_pop( $this->scopeStack );
117                    array_push( $this->scopeStack, array_merge( $topScope, $res->tokens ) );
118                    return new TokenHandlerResult( [] );
119                }
120            } else {
121                // EOF -- collapse stack!
122                $allToks = [];
123                for ( $i = 0,  $n = count( $this->scopeStack );  $i < $n;  $i++ ) {
124                    PHPUtils::pushArray( $allToks, $this->scopeStack[$i] );
125                }
126                PHPUtils::pushArray( $allToks, $activeTokens );
127
128                $res = $this->toEnd() ? $this->transformation( $allToks ) : new TokenHandlerResult( $allToks );
129                if ( $res->tokens !== null
130                    && count( $res->tokens )
131                    && !( PHPUtils::lastItem( $res->tokens ) instanceof EOFTk )
132                ) {
133                    $this->env->log( 'error', $this::name(), 'handler dropped the EOFTk!' );
134
135                    // preserve the EOFTk
136                    $res->tokens[] = $token;
137                }
138
139                $this->scopeStack = [];
140                $this->onAnyEnabled = false;
141                return $res;
142            }
143        } else {
144            // EndTagTk should be the only one that can reach here.
145            Assert::invariant( $token instanceof EndTagTk, 'Expected an end tag.' );
146            if ( $this->ackEnd() ) {
147                return $this->transformation( [ $token ] );
148            } else {
149                // An unbalanced end tag. Ignore it.
150                return null;
151            }
152        }
153    }
154
155    /**
156     * Handle 'any' token in between delimiter tokens. Activated when
157     * encountering the delimiter token, and collects all tokens until the end
158     * token is reached.
159     * @param Token|string $token
160     * @return TokenHandlerResult
161     */
162    private function onAnyToken( $token ): TokenHandlerResult {
163        // Simply collect anything ordinary in between
164        $this->scopeStack[count( $this->scopeStack ) - 1][] = $token;
165        return new TokenHandlerResult( [] );
166    }
167
168    /**
169     * This helper function will build a meta token in the right way for these tags.
170     * @param TokenTransformManager $manager
171     * @param string $tokenName
172     * @param bool $isEnd
173     * @param SourceRange $tsr
174     * @param ?string $src
175     * @return SelfclosingTagTk
176     */
177    public static function buildMetaToken(
178        TokenTransformManager $manager, string $tokenName, bool $isEnd,
179        SourceRange $tsr, ?string $src
180    ): SelfclosingTagTk {
181        if ( $isEnd ) {
182            $tokenName .= '/End';
183        }
184
185        $srcText = $manager->getFrame()->getSrcText();
186        $newSrc = $tsr->substr( $srcText );
187        $dp = new DataParsoid;
188        $dp->tsr = $tsr;
189        $dp->src = $newSrc;
190
191        return new SelfclosingTagTk( 'meta',
192            [ new KV( 'typeof', $tokenName ) ],
193            $dp
194        );
195    }
196
197    /**
198     * @param TokenTransformManager $manager
199     * @param string $tokenName
200     * @param Token $startDelim
201     * @param ?Token $endDelim
202     * @return SelfclosingTagTk
203     */
204    protected static function buildStrippedMetaToken(
205        TokenTransformManager $manager, string $tokenName, Token $startDelim,
206        ?Token $endDelim
207    ): SelfclosingTagTk {
208        $da = $startDelim->dataParsoid;
209        $tsr0 = $da ? $da->tsr : null;
210        $t0 = $tsr0 ? $tsr0->start : null;
211        $t1 = null;
212
213        if ( $endDelim !== null ) {
214            $da = $endDelim->dataParsoid ?? null;
215            $tsr1 = $da ? $da->tsr : null;
216            $t1 = $tsr1 ? $tsr1->end : null;
217        } else {
218            $t1 = strlen( $manager->getFrame()->getSrcText() );
219        }
220
221        return self::buildMetaToken( $manager, $tokenName, false, new SourceRange( $t0, $t1 ), '' );
222    }
223
224    /**
225     * @inheritDoc
226     */
227    public function onTag( Token $token ): ?TokenHandlerResult {
228        return $token->getName() === $this->name() ? $this->onDelimiterToken( $token ) : null;
229    }
230
231    /**
232     * @inheritDoc
233     */
234    public function onEnd( EOFTk $token ): ?TokenHandlerResult {
235        return $this->onAnyEnabled ? $this->onDelimiterToken( $token ) : null;
236    }
237
238    /**
239     * @inheritDoc
240     */
241    public function onAny( $token ): ?TokenHandlerResult {
242        return $this->onAnyToken( $token );
243    }
244}