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