Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.30% covered (danger)
34.30%
59 / 172
17.86% covered (danger)
17.86%
10 / 56
CRAP
0.00% covered (danger)
0.00%
0 / 1
MockSiteConfig
34.30% covered (danger)
34.30%
59 / 172
17.86% covered (danger)
17.86%
10 / 56
1379.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
4.11
 getLinterSiteConfig
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allowedExternalImagePrefixes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 baseURI
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 exportMetadataToHeadBcp47
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 redirectRegexp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 categoryRegexp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canonicalNamespaceId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 namespaceId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 namespaceName
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 namespaceHasSubpages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 namespaceCase
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 specialPageLocalName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setInterwikiMagic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 interwikiMagic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 interwikiMap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 iwp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 legalTitleChars
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 linkPrefixRegex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 linkTrail
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 linkTrailRegex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 langBcp47
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 mainPageLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMWConfigValue
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 rtl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 langConverterEnabledBcp47
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 script
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scriptpath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 server
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 timezoneOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 variantsFor
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 widthOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariableIDs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDoubleUnderscoreIDs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 haveComputedFunctionSynonyms
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateFunctionSynonym
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMagicWords
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 getMagicWordMatcher
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getParameterizedAliasMatcher
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
30
 getNonNativeExtensionTags
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 getMaxTemplateDepth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSpecialPageAliases
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSpecialNSAliases
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProtocols
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fakeTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFakeTimestamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTimezoneOffset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scrubBidiChars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNoFollowConfig
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getExternalLinkTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUploadUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 metrics
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 incrementCounter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 observeTiming
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 observeHistogram
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHistogramBuckets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Mocks;
5
6use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
7use Monolog\Formatter\LineFormatter;
8use Monolog\Handler\ErrorLogHandler;
9use Monolog\Logger;
10use Wikimedia\Bcp47Code\Bcp47Code;
11use Wikimedia\Bcp47Code\Bcp47CodeValue;
12use Wikimedia\Parsoid\Config\SiteConfig;
13use Wikimedia\Parsoid\Config\StubMetadataCollector;
14use Wikimedia\Parsoid\Core\ContentMetadataCollector;
15use Wikimedia\Parsoid\Core\LinkTarget;
16use Wikimedia\Parsoid\DOM\Document;
17use Wikimedia\Parsoid\Utils\Title;
18use Wikimedia\Parsoid\Utils\Utils;
19
20class MockSiteConfig extends SiteConfig {
21
22    /** @var ?int Unix timestamp */
23    private $fakeTimestamp = 946782245; // 2000-01-02T03:04:05Z
24
25    /** @var int */
26    private $timezoneOffset = 0; // UTC
27
28    /** @var bool */
29    private $interwikiMagic = true;
30
31    /** @var array */
32    private $linterOverrides = [];
33
34    /** If set, generate Parsoid v3 parser function output for all parser functions */
35    private bool $v3pf;
36
37    private const NAMESPACE_MAP = [
38        'media' => -2,
39        'special' => -1,
40        '' => 0,
41        'talk' => 1,
42        'user' => 2,
43        'user_talk' => 3,
44        // Last one will be used by namespaceName
45        'project' => 4, 'wp' => 4, 'wikipedia' => 4,
46        'project_talk' => 5, 'wt' => 5, 'wikipedia_talk' => 5,
47        'file' => 6,
48        'file_talk' => 7,
49        'template' => 10,
50        'template_talk' => 11,
51        'help' => 12,
52        'category' => 14,
53        'category_talk' => 15,
54    ];
55
56    /** @var array<int,bool> */
57    protected $namespacesWithSubpages = [];
58
59    /** @var array */
60    protected $interwikiMap = [];
61
62    /** @var int */
63    private $maxDepth = 40;
64
65    /** @var string|null */
66    private $linkPrefixRegex = null;
67
68    /** @var string|bool */
69    private $externalLinkTarget;
70
71    public function __construct( array $opts ) {
72        parent::__construct();
73
74        if ( isset( $opts['linting'] ) ) {
75            $this->linterEnabled = $opts['linting'];
76        }
77        if ( isset( $opts['maxDepth'] ) ) {
78            $this->maxDepth = $opts['maxDepth'];
79        }
80        if ( isset( $opts['linterOverrides'] ) ) {
81            $this->linterOverrides = $opts['linterOverrides'];
82        }
83        $this->linkPrefixRegex = $opts['linkPrefixRegex'] ?? null;
84        $this->linkTrailRegex = $opts['linkTrailRegex'] ?? '/^([a-z]+)/sD'; // enwiki default
85        $this->externalLinkTarget = $opts['externallinktarget'] ?? false;
86        $this->v3pf = $opts['v3pf'] ?? false;
87
88        // Use Monolog's PHP console handler
89        $logger = new Logger( "Parsoid CLI" );
90        $handler = new ErrorLogHandler();
91        $handler->setFormatter( new LineFormatter( '%message%' ) );
92        $logger->pushHandler( $handler );
93        $this->setLogger( $logger );
94    }
95
96    public function getLinterSiteConfig(): array {
97        return $this->linterOverrides + parent::getLinterSiteConfig();
98    }
99
100    public function allowedExternalImagePrefixes(): array {
101        return [];
102    }
103
104    public function baseURI(): string {
105        return '//my.wiki.example/wikix/';
106    }
107
108    /**
109     * @inheritDoc
110     */
111    public function exportMetadataToHeadBcp47(
112        Document $document,
113        ContentMetadataCollector $metadata,
114        string $defaultTitle,
115        Bcp47Code $lang
116    ): void {
117        '@phan-var StubMetadataCollector $metadata'; // @var StubMetadataCollector $metadata
118        $moduleLoadURI = $this->server() . $this->scriptpath() . '/load.php';
119        // Look for a displaytitle.
120        $displayTitle = $metadata->getPageProperty( 'displaytitle' ) ??
121            // Use the default title, properly escaped
122            Utils::escapeHtml( $defaultTitle );
123        $this->exportMetadataHelper(
124            $document,
125            $moduleLoadURI,
126            $metadata->getModules(),
127            $metadata->getModuleStyles(),
128            $metadata->getJsConfigVars(),
129            $displayTitle,
130            $lang
131        );
132    }
133
134    public function redirectRegexp(): string {
135        return '/(?i:#REDIRECT)/';
136    }
137
138    public function categoryRegexp(): string {
139        return '/Category/';
140    }
141
142    /** @inheritDoc */
143    public function canonicalNamespaceId( string $name ): ?int {
144        return self::NAMESPACE_MAP[$name] ?? null;
145    }
146
147    /** @inheritDoc */
148    public function namespaceId( string $name ): ?int {
149        $name = Utils::normalizeNamespaceName( $name );
150        return self::NAMESPACE_MAP[$name] ?? null;
151    }
152
153    /** @inheritDoc */
154    public function namespaceName( int $ns ): ?string {
155        static $map = null;
156        if ( $map === null ) {
157            $map = array_flip( self::NAMESPACE_MAP );
158        }
159        if ( !isset( $map[$ns] ) ) {
160            return null;
161        }
162        return ucwords( strtr( $map[$ns], '_', ' ' ) );
163    }
164
165    /** @inheritDoc */
166    public function namespaceHasSubpages( int $ns ): bool {
167        return !empty( $this->namespacesWithSubpages[$ns] );
168    }
169
170    /** @inheritDoc */
171    public function namespaceCase( int $ns ): string {
172        return 'first-letter';
173    }
174
175    /** @inheritDoc */
176    public function specialPageLocalName( string $alias ): ?string {
177        return null;
178    }
179
180    public function setInterwikiMagic( bool $val ): void {
181        $this->interwikiMagic = $val;
182    }
183
184    public function interwikiMagic(): bool {
185        return $this->interwikiMagic;
186    }
187
188    public function interwikiMap(): array {
189        return $this->interwikiMap;
190    }
191
192    public function iwp(): string {
193        return 'mywiki';
194    }
195
196    public function legalTitleChars(): string {
197        return ' %!"$&\'()*,\-.\/0-9:;=?@A-Z\\\\^_`a-z~\x80-\xFF+';
198    }
199
200    public function linkPrefixRegex(): ?string {
201        return $this->linkPrefixRegex;
202    }
203
204    protected function linkTrail(): string {
205        // @phan-suppress-previous-line PhanPluginNeverReturnMethod
206        throw new \BadMethodCallException(
207            'Should not be used. linkTrailRegex() is overridden here.' );
208    }
209
210    public function linkTrailRegex(): ?string {
211        return $this->linkTrailRegex;
212    }
213
214    public function langBcp47(): Bcp47Code {
215        return new Bcp47CodeValue( 'en' );
216    }
217
218    public function mainPageLinkTarget(): LinkTarget {
219        return Title::newFromText( 'Main Page', $this );
220    }
221
222    /** @inheritDoc */
223    public function getMWConfigValue( string $key ) {
224        return match ( $key ) {
225            'CiteResponsiveReferences' => true,
226            'CiteResponsiveReferencesThreshold' => 10,
227            'ParsoidExperimentalParserFunctionOutput' => $this->v3pf,
228            default => null
229        };
230    }
231
232    public function rtl(): bool {
233        return false;
234    }
235
236    /** @inheritDoc */
237    public function langConverterEnabledBcp47( Bcp47Code $lang ): bool {
238        return $lang->toBcp47Code() === 'sr';
239    }
240
241    public function script(): string {
242        return '/wx/index.php';
243    }
244
245    public function scriptpath(): string {
246        return '/wx';
247    }
248
249    public function server(): string {
250        return '//my.wiki.example';
251    }
252
253    public function timezoneOffset(): int {
254        return $this->timezoneOffset;
255    }
256
257    /** @inheritDoc */
258    public function variantsFor( Bcp47Code $lang ): ?array {
259        return match ( $lang->toBcp47Code() ) {
260            'sr' => [
261                'base' => new Bcp47CodeValue( 'sr' ),
262                'fallbacks' => [ new Bcp47CodeValue( 'sr-Cyrl' ) ]
263            ],
264            'sr-Cyrl', 'sr-Latn' => [
265                'base' => new Bcp47CodeValue( 'sr' ),
266                'fallbacks' => [ new Bcp47CodeValue( 'sr' ) ]
267            ],
268            default => null
269        };
270    }
271
272    public function widthOption(): int {
273        return 220;
274    }
275
276    /** @inheritDoc */
277    protected function getVariableIDs(): array {
278        return []; // None for now
279    }
280
281    /** @inheritDoc */
282    protected function getDoubleUnderscoreIDs(): array {
283        return [ 'toc', 'notoc', ];
284    }
285
286    /** @inheritDoc */
287    protected function haveComputedFunctionSynonyms(): bool {
288        return false;
289    }
290
291    /** @inheritDoc */
292    protected function updateFunctionSynonym( string $func, string $magicword, bool $caseSensitive ): void {
293        /* Nothing for now. Look at src/Config/Api/SiteConfig when mocking is needed. */
294    }
295
296    /** @inheritDoc */
297    protected function getMagicWords(): array {
298        return [
299            'toc'             => [ 0, '__TOC__' ],
300            'img_thumbnail'   => [ 1, 'thumb' ],
301            'img_framed'      => [ 1, 'frame', 'framed' ],
302            'img_frameless'   => [ 1, 'frameless' ],
303            'img_manualthumb' => [ 1, 'thumbnail=$1', 'thumb=$1' ],
304            'img_none'        => [ 1, 'none' ],
305            'img_left'        => [ 1, 'left' ],
306            'img_right'       => [ 1, 'right' ],
307            // T345026: 'sub' should follow 'img_sub' to match dewikivoyage
308            'img_sub'         => [ 1, 'sub' ],
309            'sub'             => [ 0, 'sub' ],
310            'notoc'           => [ 0, '__NOTOC__' ],
311            'timedmedia_loop' => [ 0, 'loop' ],
312            'timedmedia_muted' => [ 0, 'muted' ],
313        ];
314    }
315
316    /** @inheritDoc */
317    public function getMagicWordMatcher( string $id ): string {
318        if ( $id === 'toc' ) {
319            return '/^TOC$/';
320        } else {
321            return '/(?!)/';
322        }
323    }
324
325    /** @inheritDoc */
326    public function getParameterizedAliasMatcher( array $words ): callable {
327        $paramMWs = [
328            'img_lossy' => "/^(?:(?i:lossy\=(.*?)))$/uS",
329            'timedmedia_thumbtime' => "/^(?:(?i:thumbtime\=(.*?)))$/uS",
330            'timedmedia_starttime' => "/^(?:(?i:start\=(.*?)))$/uS",
331            'timedmedia_endtime' => "/^(?:(?i:end\=(.*?)))$/uS",
332            'timedmedia_disablecontrols' => "/^(?:(?i:disablecontrols\=(.*?)))$/uS",
333            'img_manualthumb' => "/^(?:(?:thumbnail\=(.*?)|thumb\=(.*?)))$/uS",
334            'img_width' => "/^(?:(?:(.*?)px))$/uS",
335            'img_lang' => "/^(?:(?:lang\=(.*?)))$/uS",
336            'img_page' => "/^(?:(?:page\=(.*?)|page (.*?)))$/uS",
337            'img_upright' => "/^(?:(?:upright\=(.*?)|upright (.*?)))$/uS",
338            'img_link' => "/^(?:(?:link\=(.*?)))$/uS",
339            'img_alt' => "/^(?:(?:alt\=(.*?)))$/uS",
340            'img_class' => "/^(?:(?:class\=(.*?)))$/uS"
341        ];
342        $regexes = array_intersect_key( $paramMWs, array_flip( $words ) );
343        return static function ( $text ) use ( $regexes ) {
344            /**
345             * $name is the canonical magic word name
346             * $re has patterns for matching aliases
347             */
348            foreach ( $regexes as $name => $re ) {
349                if ( preg_match( $re, $text, $m ) ) {
350                    unset( $m[0] );
351
352                    // Ex. regexp here is, /^(?:(?:|vinculo\=(.*?)|enlace\=(.*?)|link\=(.*?)))$/uS
353                    // Check all the capture groups for a value, if not, it's safe to return an
354                    // empty string since we did get a match.
355                    foreach ( $m as $v ) {
356                        if ( $v !== '' ) {
357                            return [ 'k' => $name, 'v' => $v ];
358                        }
359                    }
360                    return [ 'k' => $name, 'v' => '' ];
361                }
362            }
363            return null;
364        };
365    }
366
367    /** @inheritDoc */
368    protected function getNonNativeExtensionTags(): array {
369        return [
370            'indicator' => true,
371            'timeline' => true,
372            'hiero' => true,
373            'charinsert' => true,
374            'inputbox' => true,
375            'source' => true,
376            'syntaxhighlight' => true,
377            'section' => true,
378            'score' => true,
379            'templatedata' => true,
380            'math' => true,
381            'ce' => true,
382            'chem' => true,
383            'graph' => true,
384            'maplink' => true,
385            'categorytree' => true,
386            'templatestyles' => true
387        ];
388    }
389
390    /** @inheritDoc */
391    public function getMaxTemplateDepth(): int {
392        return $this->maxDepth;
393    }
394
395    /** @inheritDoc */
396    protected function getSpecialPageAliases( string $specialPage ): array {
397        if ( $specialPage === 'Booksources' ) {
398            return [ 'Booksources', 'BookSources' ]; // Mock value
399        } else {
400            throw new \BadMethodCallException( 'Not implemented' );
401        }
402    }
403
404    /** @inheritDoc */
405    protected function getSpecialNSAliases(): array {
406        return [ "Special", "special" ]; // Mock value
407    }
408
409    /** @inheritDoc */
410    protected function getProtocols(): array {
411        return [ "http:", "https:", "irc:", "ircs:", "news:", "ftp:", "mailto:", "gopher:", "//" ];
412    }
413
414    public function fakeTimestamp(): ?int {
415        return $this->fakeTimestamp;
416    }
417
418    /**
419     * Set the fake timestamp for testing
420     * @param ?int $ts Unix timestamp
421     */
422    public function setFakeTimestamp( ?int $ts ): void {
423        $this->fakeTimestamp = $ts;
424    }
425
426    /**
427     * Set the timezone offset for testing
428     * @param int $offset Offset from UTC
429     */
430    public function setTimezoneOffset( int $offset ): void {
431        $this->timezoneOffset = $offset;
432    }
433
434    public function scrubBidiChars(): bool {
435        return true;
436    }
437
438    /** @inheritDoc */
439    public function getNoFollowConfig(): array {
440        return [
441            'nofollow' => true,
442            'nsexceptions' => [ 1 ],
443            'domainexceptions' => [ 'www.example.com' ]
444        ];
445    }
446
447    /** @inheritDoc */
448    public function getExternalLinkTarget() {
449        return $this->externalLinkTarget;
450    }
451
452    /** @inheritDoc */
453    public function getUploadUrl( LinkTarget $fileName ): string {
454        $encodedFileName = str_replace( [ '%3A', '%2A' ], [ ':', '*' ], urlencode( $fileName->getDBkey() ) );
455        return './Special:Upload?wpDestFile=' . $encodedFileName;
456    }
457
458    /** @var ?MockMetrics */
459    private $metrics;
460
461    /** @inheritDoc */
462    public function metrics(): ?StatsdDataFactoryInterface {
463        if ( $this->metrics === null ) {
464            $this->metrics = new MockMetrics();
465        }
466        return $this->metrics;
467    }
468
469    /**
470     * Increment a counter metric
471     * @param string $name
472     * @param array $labels
473     * @param float $amount
474     * @return void
475     */
476    public function incrementCounter( string $name, array $labels, float $amount = 1 ): void {
477        // We don't use the labels for now, using MockMetrics instead
478        $this->metrics->increment( $name );
479    }
480
481    /**
482     * Record a timing metric
483     * @param string $name
484     * @param float $value
485     * @param array $labels
486     * @return void
487     */
488    public function observeTiming( string $name, float $value, array $labels ): void {
489        // We don't use the labels for now, using MockMetrics instead
490        $this->metrics->timing( $name, $value );
491    }
492
493    public function observeHistogram( string $name, float $value, array $buckets, array $labels ) {
494        $this->metrics->histogram( $name, $value, $buckets, $labels );
495    }
496
497    /**
498     * Generate mock histogram buckets
499     *
500     * @param float $mean
501     * @param int $skip
502     * @return list<float>
503     */
504    public function getHistogramBuckets( float $mean, int $skip ) {
505        return [ 0., $mean, 2 * $mean ];
506    }
507}