Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.88% covered (danger)
28.88%
80 / 277
4.55% covered (danger)
4.55%
1 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
PPFrame_Hash
28.99% covered (danger)
28.99%
80 / 276
4.55% covered (danger)
4.55%
1 / 22
4285.23
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 newChild
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
132
 cachedExpand
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 expand
42.48% covered (danger)
42.48%
65 / 153
0.00% covered (danger)
0.00%
0 / 1
566.49
 implodeWithFlags
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 implode
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 virtualImplode
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 virtualBracketedImplode
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 __toString
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPDBK
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getArguments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNumberedArguments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNamedArguments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEmpty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getArgument
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loopCheck
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isTemplate
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
 setVolatile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isVolatile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTTL
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 getTTL
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @ingroup Parser
6 */
7
8namespace MediaWiki\Parser;
9
10use InvalidArgumentException;
11use MediaWiki\MainConfigNames;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Message\Message;
14use MediaWiki\Parser\Parsoid\Config\DataAccess;
15use MediaWiki\Title\Title;
16use RuntimeException;
17use Stringable;
18use Wikimedia\Parsoid\Fragments\HeadingPFragment;
19
20/**
21 * An expansion frame, used as a context to expand the result of preprocessToObj()
22 * @ingroup Parser
23 */
24// phpcs:ignore Squiz.Classes.ValidClassName.NotCamelCaps
25class PPFrame_Hash implements Stringable, PPFrame {
26
27    /**
28     * @var Parser
29     */
30    public $parser;
31
32    /**
33     * @var Preprocessor
34     */
35    public $preprocessor;
36
37    /**
38     * @var Title
39     */
40    public $title;
41
42    /**
43     * @var (string|false)[]
44     */
45    public $titleCache;
46
47    /**
48     * Hashtable listing templates which are disallowed for expansion in this frame,
49     * having been encountered previously in parent frames.
50     * @var true[]
51     */
52    public $loopCheckHash;
53
54    /**
55     * Recursion depth of this frame, top = 0
56     * Note that this is NOT the same as expansion depth in expand()
57     * @var int
58     */
59    public $depth;
60
61    /** @var bool */
62    private $volatile = false;
63    /** @var int|null */
64    private $ttl = null;
65
66    /**
67     * @var array
68     */
69    protected $childExpansionCache;
70    /**
71     * @var int
72     */
73    private $maxPPNodeCount;
74    /**
75     * @var int
76     */
77    private $maxPPExpandDepth;
78
79    private bool $useHeadingPFragments;
80
81    /**
82     * @param Preprocessor $preprocessor The parent preprocessor
83     */
84    public function __construct( $preprocessor ) {
85        $this->preprocessor = $preprocessor;
86        $this->parser = $preprocessor->parser;
87        $this->title = $this->parser->getTitle();
88        $this->maxPPNodeCount = $this->parser->getOptions()->getMaxPPNodeCount();
89        $this->maxPPExpandDepth = $this->parser->getOptions()->getMaxPPExpandDepth();
90        $this->titleCache = [ $this->title ? $this->title->getPrefixedDBkey() : false ];
91        $this->loopCheckHash = [];
92        $this->depth = 0;
93        $this->childExpansionCache = [];
94
95        $config = MediaWikiServices::getInstance()->getMainConfig();
96        $this->useHeadingPFragments = in_array(
97            'HeadingPFragment',
98            $config->get( MainConfigNames::ReturnExperimentalPFragmentTypes ),
99            true
100        );
101    }
102
103    /**
104     * Create a new child frame
105     * $args is optionally a multi-root PPNode or array containing the template arguments
106     *
107     * @param PPNode[]|false|PPNode_Hash_Array $args
108     * @param Title|false $title
109     * @param int $indexOffset
110     * @return PPTemplateFrame_Hash
111     */
112    public function newChild( $args = false, $title = false, $indexOffset = 0 ) {
113        $namedArgs = [];
114        $numberedArgs = [];
115        if ( $title === false ) {
116            $title = $this->title;
117        }
118        if ( $args !== false ) {
119            if ( $args instanceof PPNode_Hash_Array ) {
120                $args = $args->value;
121            } elseif ( !is_array( $args ) ) {
122                throw new InvalidArgumentException( __METHOD__ . ': $args must be array or PPNode_Hash_Array' );
123            }
124            foreach ( $args as $arg ) {
125                $bits = $arg->splitArg();
126                if ( $bits['index'] !== '' ) {
127                    // Numbered parameter
128                    $index = $bits['index'] - $indexOffset;
129                    if ( isset( $namedArgs[$index] ) || isset( $numberedArgs[$index] ) ) {
130                        $this->parser->getOutput()->addWarningMsg(
131                            'duplicate-args-warning',
132                            wfEscapeWikiText( (string)$this->title ),
133                            wfEscapeWikiText( (string)$title ),
134                            Message::numParam( $index )
135                        );
136                        $this->parser->addTrackingCategory( 'duplicate-args-category' );
137                    }
138                    $numberedArgs[$index] = $bits['value'];
139                    unset( $namedArgs[$index] );
140                } else {
141                    // Named parameter
142                    $name = trim( $this->expand( $bits['name'], PPFrame::STRIP_COMMENTS ) );
143                    if ( isset( $namedArgs[$name] ) || isset( $numberedArgs[$name] ) ) {
144                        $this->parser->getOutput()->addWarningMsg(
145                            'duplicate-args-warning',
146                            wfEscapeWikiText( (string)$this->title ),
147                            wfEscapeWikiText( (string)$title ),
148                            // @phan-suppress-next-line SecurityCheck-DoubleEscaped
149                            wfEscapeWikiText( $name )
150                        );
151                        $this->parser->addTrackingCategory( 'duplicate-args-category' );
152                    }
153                    $namedArgs[$name] = $bits['value'];
154                    unset( $numberedArgs[$name] );
155                }
156            }
157        }
158        return new PPTemplateFrame_Hash( $this->preprocessor, $this, $numberedArgs, $namedArgs, $title );
159    }
160
161    /**
162     * @param string|int $key
163     * @param string|PPNode $root
164     * @param int $flags
165     * @return string
166     */
167    public function cachedExpand( $key, $root, $flags = 0 ) {
168        // we don't have a parent, so we don't have a cache
169        return $this->expand( $root, $flags );
170    }
171
172    /**
173     * @param string|PPNode $root
174     * @param int $flags
175     * @return string
176     */
177    public function expand( $root, $flags = 0 ) {
178        static $expansionDepth = 0;
179        if ( is_string( $root ) ) {
180            return $root;
181        }
182
183        if ( ++$this->parser->mPPNodeCount > $this->maxPPNodeCount ) {
184            $this->parser->limitationWarn( 'node-count-exceeded',
185                    $this->parser->mPPNodeCount,
186                    $this->maxPPNodeCount
187            );
188            return '<span class="error">Node-count limit exceeded</span>';
189        }
190        if ( $expansionDepth > $this->maxPPExpandDepth ) {
191            $this->parser->limitationWarn( 'expansion-depth-exceeded',
192                    $expansionDepth,
193                    $this->maxPPExpandDepth
194            );
195            return '<span class="error">Expansion depth limit exceeded</span>';
196        }
197        ++$expansionDepth;
198        if ( $expansionDepth > $this->parser->mHighestExpansionDepth ) {
199            $this->parser->mHighestExpansionDepth = $expansionDepth;
200        }
201
202        $outStack = [ '', '' ];
203        $iteratorStack = [ false, $root ];
204        $indexStack = [ 0, 0 ];
205
206        while ( count( $iteratorStack ) > 1 ) {
207            $level = count( $outStack ) - 1;
208            $iteratorNode =& $iteratorStack[$level];
209            $out =& $outStack[$level];
210            $index =& $indexStack[$level];
211
212            if ( is_array( $iteratorNode ) ) {
213                if ( $index >= count( $iteratorNode ) ) {
214                    // All done with this iterator
215                    $iteratorStack[$level] = false;
216                    $contextNode = false;
217                } else {
218                    $contextNode = $iteratorNode[$index];
219                    $index++;
220                }
221            } elseif ( $iteratorNode instanceof PPNode_Hash_Array ) {
222                if ( $index >= $iteratorNode->getLength() ) {
223                    // All done with this iterator
224                    $iteratorStack[$level] = false;
225                    $contextNode = false;
226                } else {
227                    $contextNode = $iteratorNode->item( $index );
228                    $index++;
229                }
230            } else {
231                // Copy to $contextNode and then delete from iterator stack,
232                // because this is not an iterator but we do have to execute it once
233                $contextNode = $iteratorStack[$level];
234                $iteratorStack[$level] = false;
235            }
236
237            $newIterator = false;
238            $contextName = false;
239            $contextChildren = false;
240
241            if ( $contextNode === false ) {
242                // nothing to do
243            } elseif ( is_string( $contextNode ) ) {
244                $out .= $contextNode;
245            } elseif ( $contextNode instanceof PPNode_Hash_Array ) {
246                $newIterator = $contextNode;
247            } elseif ( $contextNode instanceof PPNode_Hash_Attr ) {
248                // No output
249            } elseif ( $contextNode instanceof PPNode_Hash_Text ) {
250                $out .= $contextNode->value;
251            } elseif ( $contextNode instanceof PPNode_Hash_Tree ) {
252                $contextName = $contextNode->name;
253                $contextChildren = $contextNode->getRawChildren();
254            } elseif ( is_array( $contextNode ) ) {
255                // Node descriptor array
256                if ( count( $contextNode ) !== 2 ) {
257                    throw new RuntimeException( __METHOD__ .
258                        ': found an array where a node descriptor should be' );
259                }
260                [ $contextName, $contextChildren ] = $contextNode;
261            } else {
262                throw new RuntimeException( __METHOD__ . ': Invalid parameter type' );
263            }
264
265            // Handle node descriptor array or tree object
266            if ( $contextName === false ) {
267                // Not a node, already handled above
268            } elseif ( $contextName[0] === '@' ) {
269                // Attribute: no output
270            } elseif ( $contextName === 'template' ) {
271                # Double-brace expansion
272                $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
273                if ( $flags & PPFrame::NO_TEMPLATES ) {
274                    $newIterator = $this->virtualBracketedImplode(
275                        '{{', '|', '}}',
276                        $bits['title'],
277                        $bits['parts']
278                    );
279                } else {
280                    $ret = $this->parser->braceSubstitution( $bits, $this );
281                    if ( isset( $ret['object'] ) ) {
282                        $newIterator = $ret['object'];
283                    } else {
284                        $out .= $ret['text'];
285                    }
286                }
287            } elseif ( $contextName === 'tplarg' ) {
288                # Triple-brace expansion
289                $bits = PPNode_Hash_Tree::splitRawTemplate( $contextChildren );
290                if ( $flags & PPFrame::NO_ARGS ) {
291                    $newIterator = $this->virtualBracketedImplode(
292                        '{{{', '|', '}}}',
293                        $bits['title'],
294                        $bits['parts']
295                    );
296                } else {
297                    $ret = $this->parser->argSubstitution( $bits, $this );
298                    if ( isset( $ret['object'] ) ) {
299                        $newIterator = $ret['object'];
300                    } else {
301                        $out .= $ret['text'];
302                    }
303                }
304            } elseif ( $contextName === 'comment' ) {
305                # HTML-style comment
306                # Remove it in HTML, pre+remove and STRIP_COMMENTS modes
307                # Not in RECOVER_COMMENTS mode (msgnw) though.
308                if ( ( $this->parser->getOutputType() === Parser::OT_HTML
309                    || ( $this->parser->getOutputType() === Parser::OT_PREPROCESS &&
310                        $this->parser->getOptions()->getRemoveComments() )
311                    || ( $flags & PPFrame::STRIP_COMMENTS )
312                    ) && !( $flags & PPFrame::RECOVER_COMMENTS )
313                ) {
314                    $out .= '';
315                } elseif (
316                    $this->parser->getOutputType() === Parser::OT_WIKI &&
317                    !( $flags & PPFrame::RECOVER_COMMENTS )
318                ) {
319                    # Add a strip marker in PST mode so that pstPass2() can
320                    # run some old-fashioned regexes on the result.
321                    # Not in RECOVER_COMMENTS mode (extractSections) though.
322                    $out .= $this->parser->insertStripItem( $contextChildren[0] );
323                } else {
324                    # Recover the literal comment in RECOVER_COMMENTS and pre+no-remove
325                    $out .= $contextChildren[0];
326                }
327            } elseif ( $contextName === 'ignore' ) {
328                # Output suppression used by <includeonly> etc.
329                # OT_WIKI will only respect <ignore> in substed templates.
330                # The other output types respect it unless NO_IGNORE is set.
331                # extractSections() sets NO_IGNORE and so never respects it.
332                if ( ( !isset( $this->parent ) && $this->parser->getOutputType() === Parser::OT_WIKI )
333                    || ( $flags & PPFrame::NO_IGNORE )
334                ) {
335                    $out .= $contextChildren[0];
336                } else {
337                    // $out .= '';
338                }
339            } elseif ( $contextName === 'ext' ) {
340                # Extension tag
341                $bits = PPNode_Hash_Tree::splitRawExt( $contextChildren ) +
342                    [ 'attr' => null, 'inner' => null, 'close' => null ];
343                if ( $flags & PPFrame::NO_TAGS ) {
344                    $s = '<' . $bits['name']->getFirstChild()->value;
345                    if ( $bits['attr'] ) {
346                        $s .= $bits['attr']->getFirstChild()->value;
347                    }
348                    if ( $bits['inner'] ) {
349                        $s .= '>' . $bits['inner']->getFirstChild()->value;
350                        if ( $bits['close'] ) {
351                            $s .= $bits['close']->getFirstChild()->value;
352                        }
353                    } else {
354                        $s .= '/>';
355                    }
356                    $out .= $s;
357                } else {
358                    $out .= $this->parser->extensionSubstitution( $bits, $this );
359                }
360            } elseif ( $contextName === 'h' ) {
361                # Heading
362                if ( $this->parser->getOutputType() === Parser::OT_HTML ) {
363                    # Expand immediately and insert heading index marker
364                    $s = $this->expand( $contextChildren, $flags );
365                    $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
366                    $titleText = $this->title->getPrefixedDBkey();
367                    $this->parser->mHeadings[] = [ $titleText, $bits['i'] ];
368                    $serial = count( $this->parser->mHeadings ) - 1;
369                    $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
370                    $s = substr( $s, 0, $bits['level'] ) . $marker . substr( $s, $bits['level'] );
371                    $this->parser->getStripState()->addGeneral( $marker, '' );
372                    $out .= $s;
373                } elseif (
374                    $this->useHeadingPFragments &&
375                    $this->parser->useParsoidFragments() &&
376                    $this->parser->getOutputType() === Parser::OT_PREPROCESS
377                ) {
378                    $s = $this->expand( $contextChildren, $flags );
379                    $fragment = DataAccess::unstripForParsoid( $s, $this->parser );
380                    $bits = PPNode_Hash_Tree::splitRawHeading( $contextChildren );
381                    $pFragment = new HeadingPFragment( $fragment, $this->title, $bits['i'] );
382
383                    // Just generate a unique marker
384                    $this->parser->mHeadings[] = [];
385                    $serial = count( $this->parser->mHeadings ) - 1;
386                    $marker = Parser::MARKER_PREFIX . "-h-$serial-" . Parser::MARKER_SUFFIX;
387
388                    $this->parser->getStripState()->addParsoidOpaque( $marker, $pFragment );
389                    $out .= $marker;
390                } else {
391                    # Expand in virtual stack
392                    $newIterator = $contextChildren;
393                }
394            } else {
395                # Generic recursive expansion
396                $newIterator = $contextChildren;
397            }
398
399            if ( $newIterator !== false ) {
400                $outStack[] = '';
401                $iteratorStack[] = $newIterator;
402                $indexStack[] = 0;
403            } elseif ( $iteratorStack[$level] === false ) {
404                // Return accumulated value to parent
405                // With tail recursion
406                while ( $iteratorStack[$level] === false && $level > 0 ) {
407                    $outStack[$level - 1] .= $out;
408                    array_pop( $outStack );
409                    array_pop( $iteratorStack );
410                    array_pop( $indexStack );
411                    $level--;
412                }
413            }
414        }
415        --$expansionDepth;
416        return $outStack[0];
417    }
418
419    /**
420     * @param string $sep
421     * @param int $flags
422     * @param string|PPNode ...$args
423     * @return string
424     */
425    public function implodeWithFlags( $sep, $flags, ...$args ) {
426        $first = true;
427        $s = '';
428        foreach ( $args as $root ) {
429            if ( $root instanceof PPNode_Hash_Array ) {
430                $root = $root->value;
431            }
432            if ( !is_array( $root ) ) {
433                $root = [ $root ];
434            }
435            foreach ( $root as $node ) {
436                if ( $first ) {
437                    $first = false;
438                } else {
439                    $s .= $sep;
440                }
441                $s .= $this->expand( $node, $flags );
442            }
443        }
444        return $s;
445    }
446
447    /**
448     * Implode with no flags specified
449     * This previously called implodeWithFlags but has now been inlined to reduce stack depth
450     * @param string $sep
451     * @param string|PPNode ...$args
452     * @return string
453     */
454    public function implode( $sep, ...$args ) {
455        $first = true;
456        $s = '';
457        foreach ( $args as $root ) {
458            if ( $root instanceof PPNode_Hash_Array ) {
459                $root = $root->value;
460            }
461            if ( !is_array( $root ) ) {
462                $root = [ $root ];
463            }
464            foreach ( $root as $node ) {
465                if ( $first ) {
466                    $first = false;
467                } else {
468                    $s .= $sep;
469                }
470                $s .= $this->expand( $node );
471            }
472        }
473        return $s;
474    }
475
476    /**
477     * Makes an object that, when expand()ed, will be the same as one obtained
478     * with implode()
479     *
480     * @param string $sep
481     * @param string|PPNode ...$args
482     * @return PPNode_Hash_Array
483     */
484    public function virtualImplode( $sep, ...$args ) {
485        $out = [];
486        $first = true;
487
488        foreach ( $args as $root ) {
489            if ( $root instanceof PPNode_Hash_Array ) {
490                $root = $root->value;
491            }
492            if ( !is_array( $root ) ) {
493                $root = [ $root ];
494            }
495            foreach ( $root as $node ) {
496                if ( $first ) {
497                    $first = false;
498                } else {
499                    $out[] = $sep;
500                }
501                $out[] = $node;
502            }
503        }
504        return new PPNode_Hash_Array( $out );
505    }
506
507    /**
508     * Virtual implode with brackets
509     *
510     * @param string $start
511     * @param string $sep
512     * @param string $end
513     * @param string|PPNode ...$args
514     * @return PPNode_Hash_Array
515     */
516    public function virtualBracketedImplode( $start, $sep, $end, ...$args ) {
517        $out = [ $start ];
518        $first = true;
519
520        foreach ( $args as $root ) {
521            if ( $root instanceof PPNode_Hash_Array ) {
522                $root = $root->value;
523            }
524            if ( !is_array( $root ) ) {
525                $root = [ $root ];
526            }
527            foreach ( $root as $node ) {
528                if ( $first ) {
529                    $first = false;
530                } else {
531                    $out[] = $sep;
532                }
533                $out[] = $node;
534            }
535        }
536        $out[] = $end;
537        return new PPNode_Hash_Array( $out );
538    }
539
540    public function __toString() {
541        return 'frame{}';
542    }
543
544    /**
545     * @param string|false $level
546     * @return false|string
547     */
548    public function getPDBK( $level = false ) {
549        if ( $level === false ) {
550            return $this->title->getPrefixedDBkey();
551        } else {
552            return $this->titleCache[$level] ?? false;
553        }
554    }
555
556    /**
557     * @return array
558     */
559    public function getArguments() {
560        return [];
561    }
562
563    /**
564     * @return array
565     */
566    public function getNumberedArguments() {
567        return [];
568    }
569
570    /**
571     * @return array
572     */
573    public function getNamedArguments() {
574        return [];
575    }
576
577    /**
578     * Returns true if there are no arguments in this frame
579     *
580     * @return bool
581     */
582    public function isEmpty() {
583        return true;
584    }
585
586    /**
587     * @param int|string $name
588     * @return bool Always false in this implementation.
589     */
590    public function getArgument( $name ) {
591        return false;
592    }
593
594    /**
595     * Returns true if the infinite loop check is OK, false if a loop is detected
596     *
597     * @param Title $title
598     *
599     * @return bool
600     */
601    public function loopCheck( $title ) {
602        return !isset( $this->loopCheckHash[$title->getPrefixedDBkey()] );
603    }
604
605    /**
606     * Return true if the frame is a template frame
607     *
608     * @return bool
609     */
610    public function isTemplate() {
611        return false;
612    }
613
614    /**
615     * Get a title of frame
616     *
617     * @return Title
618     */
619    public function getTitle() {
620        return $this->title;
621    }
622
623    /**
624     * Set the volatile flag
625     *
626     * @param bool $flag
627     */
628    public function setVolatile( $flag = true ) {
629        $this->volatile = $flag;
630    }
631
632    /**
633     * Get the volatile flag
634     *
635     * @return bool
636     */
637    public function isVolatile() {
638        return $this->volatile;
639    }
640
641    /**
642     * @param int $ttl
643     * @deprecated since 1.44, use ParserOutput::updateCacheExpiry()
644     */
645    public function setTTL( $ttl ) {
646        wfDeprecated( __METHOD__, '1.44' );
647        if ( $ttl !== null && ( $this->ttl === null || $ttl < $this->ttl ) ) {
648            $this->ttl = $ttl;
649        }
650    }
651
652    /**
653     * @return int|null
654     * @deprecated since 1.46, use ParserOutput::getCacheExpiry()
655     */
656    public function getTTL() {
657        wfDeprecated( __METHOD__, '1.46' );
658        return $this->ttl;
659    }
660}
661
662/** @deprecated class alias since 1.43 */
663class_alias( PPFrame_Hash::class, 'PPFrame_Hash' );