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
 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 / 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\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 array<Token|string> $chunk
97     * @param array $options
98     * @return array<Token|string>
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' => 'peg-tokens-to-expanded-tokens',
120            'pipelineOpts' => [
121                'expandTemplates' => $options['expandTemplates'],
122                'inTemplate' => $options['inTemplate'],
123                'attrExpansion' => $options['attrExpansion'] ?? false
124            ],
125            'sol' => true,
126            'srcOffsets' => $options['srcOffsets'] ?? null,
127            'tplArgs' => [ 'name' => null, 'title' => null, 'attribs' => [] ]
128        ];
129
130        $tokens = PipelineUtils::processContentInPipeline( $this->env, $this, $content, $opts );
131        TokenUtils::stripEOFTkfromTokens( $tokens );
132        return $tokens;
133    }
134
135    /**
136     * Check if expanding a template would lead to a loop, or would exceed the
137     * maximum expansion depth.
138     *
139     * @param Title $title
140     * @param int $maxDepth
141     * @param bool $ignoreLoop
142     * @return ?string null => no error; non-null => error message
143     */
144    public function loopAndDepthCheck( Title $title, int $maxDepth, bool $ignoreLoop ): ?string {
145        if ( $this->depth > $maxDepth ) {
146            // Too deep
147            return "Template recursion depth limit exceeded ($maxDepth): ";
148        }
149
150        if ( $ignoreLoop ) {
151            return null;
152        }
153
154        $frame = $this;
155        do {
156            if ( $title->equals( $frame->title ) ) {
157                // Loop detected
158                return 'Template loop detected: ';
159            }
160            $frame = $frame->parentFrame;
161        } while ( $frame );
162
163        // No loop detected.
164        return null;
165    }
166
167    /**
168     * @param mixed $arg
169     * @param SourceRange $srcOffsets
170     * @return array
171     */
172    private function expandArg( $arg, SourceRange $srcOffsets ): array {
173        if ( is_string( $arg ) ) {
174            return [ $arg ];
175        } else {
176            return $this->expand( $arg, [
177                'expandTemplates' => true,
178                'inTemplate' => true,
179                'srcOffsets' => $srcOffsets,
180            ] );
181        }
182    }
183
184    /**
185     * @param Token $tplArgToken
186     * @return array tokens representing the arg value
187     */
188    public function expandTemplateArg( Token $tplArgToken ): array {
189        $args = $this->args->named();
190        $attribs = $tplArgToken->attribs;
191
192        $expandedKeyToks = $this->expandArg(
193            $attribs[0]->k,
194            $attribs[0]->srcOffsets->key
195        );
196
197        $argName = trim( TokenUtils::tokensToString( $expandedKeyToks ) );
198        $res = $args['dict'][$argName] ?? null;
199
200        if ( $res !== null ) {
201            $res = isset( $args['namedArgs'][$argName] ) ?
202                TokenUtils::tokenTrim( $res ) : $res;
203            return is_string( $res ) ? [ $res ] : $res;
204        } elseif ( count( $attribs ) > 1 ) {
205            return $this->expandArg(
206                $attribs[1]->v,
207                $attribs[1]->srcOffsets->value
208            );
209        } else {
210            return array_merge( [ '{{{' ], $expandedKeyToks, [ '}}}' ] );
211        }
212    }
213}