Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.78% covered (success)
97.78%
44 / 45
94.44% covered (success)
94.44%
17 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
State
97.78% covered (success)
97.78%
44 / 45
94.44% covered (success)
94.44%
17 / 18
27
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
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getUsages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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%
7 / 7
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<string,int> Total number of <maplink> and <mapframe> tags on the page, to be
24     *  stored as a page property
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] = ( $this->usages[$key] ?? 0 ) + 1;
98    }
99
100    /**
101     * @return array<string,int>
102     */
103    public function getUsages(): array {
104        return $this->usages;
105    }
106
107    /**
108     * @param string[] $groupIds
109     */
110    public function addInteractiveGroups( array $groupIds ): void {
111        $this->interactiveGroups += array_fill_keys( $groupIds, null );
112    }
113
114    /**
115     * @return string[] Group ids, guaranteed to be unique
116     */
117    public function getInteractiveGroups(): array {
118        return array_keys( $this->interactiveGroups );
119    }
120
121    /**
122     * @param string[] $groupIds
123     */
124    public function addRequestedGroups( array $groupIds ): void {
125        $this->requestedGroups += array_fill_keys( $groupIds, null );
126    }
127
128    /**
129     * @return string[] Group ids, guaranteed to be unique
130     */
131    public function getRequestedGroups(): array {
132        return array_keys( $this->requestedGroups );
133    }
134
135    /**
136     * @return array<string,int>
137     */
138    public function getCounters(): array {
139        return $this->counters;
140    }
141
142    /**
143     * @param array<string,int> $counters A JSON-serializable structure
144     */
145    public function setCounters( array $counters ): void {
146        $this->counters = $counters;
147    }
148
149    /**
150     * @param string $groupId
151     * @param array $data A JSON-serializable structure
152     */
153    public function addData( $groupId, array $data ): void {
154        // There is no way to ever add anything to a private group starting with `_`
155        if ( isset( $this->data[$groupId] ) && !str_starts_with( $groupId, '_' ) ) {
156            $this->data[$groupId] = array_merge( $this->data[$groupId], $data );
157        } else {
158            $this->data[$groupId] = $data;
159        }
160    }
161
162    /**
163     * @return array<string,array> Associative key-value array, build up by {@see addData}
164     */
165    public function getData(): array {
166        return $this->data;
167    }
168
169    /**
170     * @return array A JSON serializable associative array
171     */
172    public function jsonSerialize(): array {
173        // TODO: Replace with the ...$this->usages syntax when we can use PHP 8.1
174        return array_merge( [
175            'broken' => $this->broken,
176            'interactiveGroups' => $this->getInteractiveGroups(),
177            'requestedGroups' => $this->getRequestedGroups(),
178            'counters' => $this->counters ?: null,
179            'data' => $this->data,
180        ], $this->usages );
181    }
182
183    /**
184     * @param array $data A JSON serializable associative array, as returned by jsonSerialize()
185     *
186     * @return self
187     */
188    private static function newFromJson( array $data ): self {
189        $status = new self();
190        $status->broken = (int)( $data['broken'] ?? 0 );
191        $status->usages = array_filter( $data, static function ( $count, $key ) {
192            return is_int( $count ) && $count > 0 && str_starts_with( $key, 'map' );
193        }, ARRAY_FILTER_USE_BOTH );
194
195        // TODO: Backwards compatibility, can be removed 30 days later
196        if ( !array_is_list( $data['interactiveGroups'] ?? [] ) ) {
197            $data['interactiveGroups'] = array_keys( $data['interactiveGroups'] );
198        }
199        if ( !array_is_list( $data['requestedGroups'] ?? [] ) ) {
200            $data['requestedGroups'] = array_keys( $data['requestedGroups'] );
201        }
202
203        $status->addInteractiveGroups( $data['interactiveGroups'] ?? [] );
204        $status->addRequestedGroups( $data['requestedGroups'] ?? [] );
205        $status->counters = $data['counters'] ?? [];
206        $status->data = $data['data'] ?? [];
207
208        return $status;
209    }
210
211}