Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.83% covered (success)
97.83%
45 / 46
94.12% covered (success)
94.12%
16 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
State
97.83% covered (success)
97.83%
45 / 46
94.12% covered (success)
94.12%
16 / 17
26
0.00% covered (danger)
0.00%
0 / 1
 getState
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getOrCreate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveState
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasValidTags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasBrokenTags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 incrementBrokenTags
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 incrementUsage
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 addInteractiveGroups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getInteractiveGroups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addRequestedGroups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequestedGroups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCounters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setCounters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 jsonSerialize
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 newFromJson
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace Kartographer;
4
5use JsonSerializable;
6use MediaWiki\Parser\ParserOutput;
7use UnexpectedValueException;
8use Wikimedia\Parsoid\Core\ContentMetadataCollector;
9
10/**
11 * Stores information about map tags on page in ParserOutput
12 *
13 * @license MIT
14 */
15class State implements JsonSerializable {
16
17    public const DATA_KEY = 'kartographer';
18
19    /** @var int Total number of invalid <map…> tags on the page */
20    private int $broken = 0;
21
22    /**
23     * @var array{maplink?: int, mapframes?: int} Total number of <maplink> and <mapframe> tags on
24     *  the page
25     */
26    private array $usages = [];
27
28    /** @var array<string,null> Flipped set, values are meaningless */
29    private array $interactiveGroups = [];
30    /** @var array<string,null> Flipped set, values are meaningless */
31    private array $requestedGroups = [];
32    /** @var array<string,int> */
33    private array $counters = [];
34
35    /**
36     * @var array<string,array> Indexed per group identifier
37     */
38    private array $data = [];
39
40    /**
41     * Retrieves an instance of self from ParserOutput, if present
42     *
43     * @param ParserOutput $output
44     * @return self|null
45     */
46    public static function getState( ParserOutput $output ): ?self {
47        // $state may be null or a JSON serializable array.
48        // When reading old cache entries, it may for a while still be a State object (T266260).
49        $state = $output->getExtensionData( self::DATA_KEY );
50
51        if ( is_array( $state ) ) {
52            $state = self::newFromJson( $state );
53        }
54
55        return $state;
56    }
57
58    /**
59     * Retrieves an instance of self from ParserOutput if possible,
60     * otherwise creates a new instance.
61     *
62     * @param ParserOutput $output
63     * @return self
64     */
65    public static function getOrCreate( ParserOutput $output ): self {
66        return self::getState( $output ) ?? new self();
67    }
68
69    /**
70     * Stores an instance of self in the ParserOutput.
71     *
72     * @param ContentMetadataCollector $output
73     * @param self $state
74     */
75    public static function saveState( ContentMetadataCollector $output, self $state ): void {
76        $output->setExtensionData( self::DATA_KEY, $state->jsonSerialize() );
77    }
78
79    public function hasValidTags(): bool {
80        return ( array_sum( $this->usages ) - $this->broken ) > 0;
81    }
82
83    public function hasBrokenTags(): bool {
84        return $this->broken > 0;
85    }
86
87    public function incrementBrokenTags(): void {
88        $this->broken++;
89    }
90
91    public function incrementUsage( string $tag ): void {
92        if ( !str_starts_with( $tag, 'map' ) ) {
93            throw new UnexpectedValueException( 'Unsupported tag name' );
94        }
95        // Resulting keys will be "maplinks" and "mapframes"
96        $key = "{$tag}s";
97        $this->usages[$key] ??= 0;
98        $this->usages[$key]++;
99    }
100
101    /**
102     * @param string[] $groupIds
103     */
104    public function addInteractiveGroups( array $groupIds ): void {
105        $this->interactiveGroups += array_fill_keys( $groupIds, null );
106    }
107
108    /**
109     * @return string[] Group ids, guaranteed to be unique
110     */
111    public function getInteractiveGroups(): array {
112        return array_keys( $this->interactiveGroups );
113    }
114
115    /**
116     * @param string[] $groupIds
117     */
118    public function addRequestedGroups( array $groupIds ): void {
119        $this->requestedGroups += array_fill_keys( $groupIds, null );
120    }
121
122    /**
123     * @return string[] Group ids, guaranteed to be unique
124     */
125    public function getRequestedGroups(): array {
126        return array_keys( $this->requestedGroups );
127    }
128
129    /**
130     * @return array<string,int>
131     */
132    public function getCounters(): array {
133        return $this->counters;
134    }
135
136    /**
137     * @param array<string,int> $counters A JSON-serializable structure
138     */
139    public function setCounters( array $counters ): void {
140        $this->counters = $counters;
141    }
142
143    /**
144     * @param string $groupId
145     * @param array $data A JSON-serializable structure
146     */
147    public function addData( $groupId, array $data ): void {
148        // There is no way to ever add anything to a private group starting with `_`
149        if ( isset( $this->data[$groupId] ) && !str_starts_with( $groupId, '_' ) ) {
150            $this->data[$groupId] = array_merge( $this->data[$groupId], $data );
151        } else {
152            $this->data[$groupId] = $data;
153        }
154    }
155
156    /**
157     * @return array<string,array> Associative key-value array, build up by {@see addData}
158     */
159    public function getData(): array {
160        return $this->data;
161    }
162
163    /**
164     * @return array A JSON serializable associative array
165     */
166    public function jsonSerialize(): array {
167        return [
168            'broken' => $this->broken,
169            'interactiveGroups' => $this->getInteractiveGroups(),
170            'requestedGroups' => $this->getRequestedGroups(),
171            'counters' => $this->counters ?: null,
172            'data' => $this->data,
173            ...$this->usages,
174        ];
175    }
176
177    /**
178     * @param array $data A JSON serializable associative array, as returned by jsonSerialize()
179     */
180    private static function newFromJson( array $data ): self {
181        $status = new self();
182        $status->broken = (int)( $data['broken'] ?? 0 );
183        $status->usages = array_filter( $data, static function ( $count, $key ) {
184            return is_int( $count ) && $count > 0 && str_starts_with( $key, 'map' );
185        }, ARRAY_FILTER_USE_BOTH );
186
187        // TODO: Backwards compatibility, can be removed 30 days later
188        if ( !array_is_list( $data['interactiveGroups'] ?? [] ) ) {
189            $data['interactiveGroups'] = array_keys( $data['interactiveGroups'] );
190        }
191        if ( !array_is_list( $data['requestedGroups'] ?? [] ) ) {
192            $data['requestedGroups'] = array_keys( $data['requestedGroups'] );
193        }
194
195        $status->addInteractiveGroups( $data['interactiveGroups'] ?? [] );
196        $status->addRequestedGroups( $data['requestedGroups'] ?? [] );
197        $status->counters = $data['counters'] ?? [];
198        $status->data = $data['data'] ?? [];
199
200        return $status;
201    }
202
203}