Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Frame
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 11
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getEnv
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArgs
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSrcText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newChild
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expand
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
 loopAndDepthCheck
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 expandArg
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 lookupArg
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 expandTemplateArg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html;
5
6use Wikimedia\Parsoid\Config\Env;
7use Wikimedia\Parsoid\Tokens\EOFTk;
8use Wikimedia\Parsoid\Tokens\KV;
9use Wikimedia\Parsoid\Tokens\SourceRange;
10use Wikimedia\Parsoid\Tokens\Token;
11use Wikimedia\Parsoid\Utils\PHPUtils;
12use Wikimedia\Parsoid\Utils\PipelineUtils;
13use Wikimedia\Parsoid\Utils\Title;
14use Wikimedia\Parsoid\Utils\TokenUtils;
15
16/**
17 * A frame represents a template expansion scope including parameters passed
18 * to the template (args). It provides a generic 'expand' method which
19 * expands / converts individual parameter values in its scope.  It also
20 * provides methods to check if another expansion would lead to loops or
21 * exceed the maximum expansion depth.
22 */
23class Frame {
24    /** @var ?Frame */
25    private $parentFrame;
26
27    /** @var Env */
28    private $env;
29
30    /** @var Title */
31    private $title;
32
33    /** @var Params */
34    private $args;
35
36    /** @var string */
37    private $srcText;
38
39    /** @var int */
40    private $depth;
41
42    /**
43     * @param Title $title
44     * @param Env $env
45     * @param KV[] $args
46     * @param string $srcText
47     * @param ?Frame $parentFrame
48     */
49    public function __construct(
50        Title $title, Env $env, array $args, string $srcText,
51        ?Frame $parentFrame = null
52    ) {
53        $this->title = $title;
54        $this->env = $env;
55        $this->args = new Params( $args );
56        $this->srcText = $srcText;
57
58        if ( $parentFrame ) {
59            $this->parentFrame = $parentFrame;
60            $this->depth = $parentFrame->depth + 1;
61        } else {
62            $this->parentFrame = null;
63            $this->depth = 0;
64        }
65    }
66
67    public function getEnv(): Env {
68        return $this->env;
69    }
70
71    public function getTitle(): Title {
72        return $this->title;
73    }
74
75    public function getArgs(): Params {
76        return $this->args;
77    }
78
79    public function getSrcText(): string {
80        return $this->srcText;
81    }
82
83    /**
84     * Create a new child frame.
85     * @param Title $title
86     * @param KV[] $args
87     * @param string $srcText
88     * @return Frame
89     */
90    public function newChild( Title $title, array $args, string $srcText ): Frame {
91        return new Frame( $title, $this->env, $args, $srcText, $this );
92    }
93
94    /**
95     * Expand / convert a thunk (a chunk of tokens not yet fully expanded).
96     * @param Token[] $chunk
97     * @param array $options
98     * @return Token[]
99     */
100    public function expand( array $chunk, array $options ): array {
101        $this->env->log( 'debug', 'Frame.expand', $chunk );
102
103        if ( !$chunk ) {
104            return $chunk;
105        }
106
107        // Add an EOFTk if it isn't present
108        $content = $chunk;
109        if ( !( PHPUtils::lastItem( $chunk ) instanceof EOFTk ) ) {
110            $content[] = new EOFTk();
111        }
112
113        // Downstream template uses should be tracked and wrapped only if:
114        // - not in a nested template        Ex: {{Templ:Foo}} and we are processing Foo
115        // - not in a template use context   Ex: {{ .. | {{ here }} | .. }}
116        // - the attribute use is wrappable  Ex: [[ ... | {{ .. link text }} ]]
117
118        $opts = [
119            'pipelineType' => 'tokens/x-mediawiki',
120            'pipelineOpts' => [
121                'isInclude' => $this->depth > 0,
122                'expandTemplates' => $options['expandTemplates'],
123                'inTemplate' => $options['inTemplate'],
124                'attrExpansion' => $options['attrExpansion'] ?? false
125            ],
126            'sol' => true,
127            'srcOffsets' => $options['srcOffsets'] ?? null,
128            'tplArgs' => [ 'name' => null, 'title' => null, 'attribs' => [] ]
129        ];
130
131        $tokens = PipelineUtils::processContentInPipeline( $this->env, $this, $content, $opts );
132        TokenUtils::stripEOFTkfromTokens( $tokens );
133        return $tokens;
134    }
135
136    /**
137     * Check if expanding a template would lead to a loop, or would exceed the
138     * maximum expansion depth.
139     *
140     * @param Title $title
141     * @param int $maxDepth
142     * @param bool $ignoreLoop
143     * @return ?string null => no error; non-null => error message
144     */
145    public function loopAndDepthCheck( Title $title, int $maxDepth, bool $ignoreLoop ): ?string {
146        if ( $this->depth > $maxDepth ) {
147            // Too deep
148            return "Template recursion depth limit exceeded ($maxDepth): ";
149        }
150
151        if ( $ignoreLoop ) {
152            return null;
153        }
154
155        $frame = $this;
156        do {
157            if ( $title->equals( $frame->title ) ) {
158                // Loop detected
159                return 'Template loop detected: ';
160            }
161            $frame = $frame->parentFrame;
162        } while ( $frame );
163
164        // No loop detected.
165        return null;
166    }
167
168    /**
169     * @param mixed $arg
170     * @param SourceRange $srcOffsets
171     * @return array
172     */
173    private function expandArg( $arg, SourceRange $srcOffsets ): array {
174        if ( is_string( $arg ) ) {
175            return [ $arg ];
176        } else {
177            return $this->expand( $arg, [
178                'expandTemplates' => true,
179                'inTemplate' => true,
180                'srcOffsets' => $srcOffsets,
181            ] );
182        }
183    }
184
185    /**
186     * @param array $args
187     * @param KV[] $attribs
188     * @param array $toks
189     * @return array
190     */
191    private function lookupArg( array $args, array $attribs, array $toks ): array {
192        $argName = trim( TokenUtils::tokensToString( $toks ) );
193        $res = $args['dict'][$argName] ?? null;
194
195        if ( $res !== null ) {
196            $res = isset( $args['namedArgs'][$argName] ) ? TokenUtils::tokenTrim( $res ) : $res;
197            return is_string( $res ) ? [ $res ] : $res;
198        } elseif ( count( $attribs ) > 1 ) {
199            return $this->expandArg( $attribs[1]->v, $attribs[1]->srcOffsets->value );
200        } else {
201            return [ '{{{' . $argName . '}}}' ];
202        }
203    }
204
205    /**
206     * @param Token $tplArgToken
207     * @return array tokens representing the arg value
208     */
209    public function expandTemplateArg( Token $tplArgToken ): array {
210        $attribs = $tplArgToken->attribs;
211        $toks = $this->expandArg( $attribs[0]->k, $attribs[0]->srcOffsets->key );
212        return $this->lookupArg( $this->args->named(), $attribs, $toks );
213    }
214}