Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateInfo
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 5
992
0.00% covered (danger)
0.00%
0 / 1
 __clone
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 newFromJsonArray
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
110
 renumberParamInfos
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 jsonClassHintFor
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 toJsonArray
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\NodeData;
5
6use stdClass;
7use Wikimedia\Assert\Assert;
8use Wikimedia\JsonCodec\Hint;
9use Wikimedia\JsonCodec\JsonCodecable;
10use Wikimedia\JsonCodec\JsonCodecableTrait;
11use Wikimedia\Parsoid\Utils\Utils;
12
13class TemplateInfo implements JsonCodecable {
14    use JsonCodecableTrait;
15
16    /**
17     * The target wikitext
18     */
19    public ?string $targetWt = null;
20
21    /**
22     * The parser function name
23     */
24    public ?string $func = null;
25
26    /**
27     * The URL of the target
28     */
29    public ?string $href = null;
30
31    /**
32     * Template arguments as an ordered list
33     * @var list<ParamInfo>
34     */
35    public array $paramInfos = [];
36
37    /**
38     * The type of template (template, templatearg, parserfunction).
39     * @note For backward-compatibility reasons, this property is
40     * not serialized/deserialized.
41     * @var 'template'|'templatearg'|'parserfunction'|'old-parserfunction'|null
42     * @see https://www.mediawiki.org/wiki/Parsoid/MediaWiki_DOM_spec/Parser_Functions
43     */
44    public ?string $type = null;
45
46    /**
47     * The index into data-parsoid.pi
48     */
49    public ?int $i = null;
50
51    public function __clone() {
52        foreach ( $this->paramInfos as &$pi ) {
53            $pi = clone $pi;
54        }
55    }
56
57    /** @inheritDoc */
58    public static function newFromJsonArray( array $json ): TemplateInfo {
59        $ti = new TemplateInfo;
60        $ti->targetWt = $json['target']['wt'] ?? null;
61        $ti->href = $json['target']['href'] ?? null;
62        $ti->paramInfos = [];
63        $params = (array)( $json['params'] ?? null );
64        $oldPF = false;
65
66        if ( isset( $json['target']['key'] ) ) {
67            $ti->func = $json['target']['key'];
68        } elseif ( isset( $json['target']['function'] ) ) {
69            $ti->func = $json['target']['function'];
70            $oldPF = true;
71        }
72
73        $count = 1;
74        $paramList = [];
75        foreach ( $params as $k => $v ) {
76            // Converting $params to an array can turn the keys into ints,
77            // so we need to explicitly cast them back to string.
78            // We also insert `=` prefixes on duplicate keys; strip those
79            // out.
80            $k = preg_replace( '/^=\d+=/', '', (string)$k );
81            $info = new ParamInfo( $k );
82            $info->valueWt = $v->wt ?? null;
83            $info->html = $v->html ?? null;
84            $info->keyWt = $v->key->wt ?? null;
85            // Somewhat complicated defaults here for conciseness:
86            // If the key is a numeric string, the order defaults to the
87            // numeric value and 'eq' defaults to true.
88            // Order defaults to 'JSON order' (aka $count) but this isn't
89            // guaranteed so we should always emit an 'order' parameter for
90            // non-numeric keys.
91            $info->named = $v->eq ?? !$info->isNumericKey();
92            $order = $v->order ?? ( $info->isNumericKey() ? (int)$k : $count );
93            $count++;
94            $paramList[] = [ $order, $info ];
95        }
96        // Regardless of JSON order (which is not guaranteed), ensure that our
97        // params are sorted consistently with 'order'
98        usort( $paramList, static function ( $a, $b ) {
99            [ $orderA, $infoA ] = $a;
100            [ $orderB, $infoB ] = $b;
101            return $orderA - $orderB;
102        } );
103        // Strip out the order, we don't need it after sorting.
104        $ti->paramInfos = array_map( static fn ( $entry )=>$entry[1], $paramList );
105        $ti->i = $json['i'] ?? null;
106        // BACKWARD COMPATIBILITY: for 'old' parser function serialization
107        // split first arg from function name.
108        if ( $oldPF && str_contains( $ti->targetWt, ':' ) ) {
109            // For old PF we're guaranteed that parameters are all positional
110            // (T204307/T400080)
111            [ $name, $arg0 ] = explode( ':', $ti->targetWt, 2 );
112            $ti->targetWt = $name;
113            $param0 = new ParamInfo( "1", null );
114            $param0->valueWt = $arg0;
115            array_unshift( $ti->paramInfos, $param0 );
116            // BACKWARD COMPATIBILITY: T410826 if there are named parameters
117            // here, convert them to numeric.
118            foreach ( $ti->paramInfos as $param ) {
119                if ( $param->named ) {
120                    if ( $param->srcOffsets !== null ) {
121                        $param->srcOffsets = $param->srcOffsets->span()->expandTsrV();
122                    }
123                    $param->valueWt = ( $param->keyWt ?? $param->k ) . '=' . ( $param->valueWt ?? '' );
124                    $param->named = false;
125                    $param->keyWt = null;
126                }
127            }
128            // Renumber all params (again, all positional with $keyWt=null)
129            self::renumberParamInfos( $ti->paramInfos );
130        }
131        return $ti;
132    }
133
134    /** @param list<ParamInfo> $paramInfos */
135    private static function renumberParamInfos( array $paramInfos ): void {
136        // All args should be positional.  MUTATES PARAMS.
137        $count = 1;
138        foreach ( $paramInfos as $param ) {
139            Assert::invariant( $param->keyWt === null && !$param->named,
140                              "Parameter $count should be positional!" );
141            if ( $param->srcOffsets ) {
142                Assert::invariant( $param->srcOffsets->key->length() === 0,
143                                  "Key should be synthetic" );
144            }
145            $param->k = (string)( $count++ );
146        }
147    }
148
149    /** @inheritDoc */
150    public static function jsonClassHintFor( string $keyname ) {
151        static $hints = null;
152        if ( $hints === null ) {
153            $hints = [
154                // The most deeply nested stdClass structure is "wt" inside
155                // "key" inside a parameter:
156                //     "params":{"1":{"key":{"wt":"..."}}}
157                'params' => Hint::build(
158                    stdClass::class, Hint::ALLOW_OBJECT,
159                    Hint::STDCLASS, Hint::ALLOW_OBJECT,
160                    Hint::STDCLASS, Hint::ALLOW_OBJECT
161                ),
162            ];
163        }
164        return $hints[$keyname] ?? null;
165    }
166
167    /** @inheritDoc */
168    public function toJsonArray(): array {
169        // This is a complicated serialization, but necessary for
170        // backward compatibility with existing data-mw
171        // https://www.mediawiki.org/wiki/Parsoid/MediaWiki_DOM_spec/Parser_Functions
172        // and T404772 has more details.
173
174        $paramInfoList = $this->paramInfos;
175        $target = [ 'wt' => $this->targetWt ];
176        if ( $this->func !== null ) {
177            if ( $this->type === 'parserfunction' ) {
178                $target['key'] = $this->func;
179            } else {
180                // $this->type === 'old-parserfunction'
181                $target['function'] = $this->func;
182                // For back-compat, attach the first parser function argument
183                // to the key.
184                if ( count( $paramInfoList ) > 0 ) {
185                    $paramInfoList = Utils::cloneArray( $paramInfoList );
186                    $firstArg = array_shift( $paramInfoList );
187                    $target['wt'] .= ':' . $firstArg->valueWt;
188                    // All args are positional for old-parserfunction
189                    self::renumberParamInfos( $paramInfoList );
190                }
191            }
192        }
193        if ( $this->href !== null ) {
194            $target['href'] = $this->href;
195        }
196        $params = [];
197        foreach ( $paramInfoList as $idx => $info ) {
198            // Non-standard serialization of ParamInfo, alas. (T404772)
199            $param = [
200                'wt' => $info->valueWt,
201            ];
202            if ( $info->html !== null ) {
203                $param['html'] = $info->html;
204            }
205            if ( $info->keyWt !== null ) {
206                $param['key'] = (object)[
207                    'wt' => $info->keyWt,
208                ];
209            }
210            $key = $info->k;
211            if ( $this->type === 'parserfunction' ) {
212                // Add 'eq' and 'order' keys, but use defaults to avoid
213                // need to explicitly encode these in the common cases.
214                $isNumeric = $info->isNumericKey();
215                $defaultEq = !$isNumeric;
216                if ( $defaultEq !== $info->named ) {
217                    $param['eq'] = $info->named;
218                }
219                $defaultOrder = $isNumeric ? (int)$info->k : null;
220                $order = $idx + 1;
221                if ( $defaultOrder !== $order ) {
222                    $param['order'] = $order;
223                }
224                // For duplicate keys, insert leading `=` to disambiguate.
225                // (key can never legitimately contain leading =)
226                if ( isset( $params[$key] ) ) {
227                    $key = "=" . count( $params ) . "=$key";
228                }
229            }
230            $params[$key] = (object)$param;
231        }
232
233        return [
234            'target' => $target,
235            'params' => (object)$params,
236            'i' => $this->i,
237        ];
238    }
239}