Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 85 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
TOCData | |
0.00% |
0 / 85 |
|
0.00% |
0 / 16 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCurrentTOCLevel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addSection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSections | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setExtensionData | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
appendExtensionData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getExtensionData | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
toLegacy | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
fromLegacy | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
processHeading | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
156 | |||
jsonSerialize | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
toJsonArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
newFromJsonArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
jsonClassHintFor | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
prettyPrint | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
__clone | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Core; |
5 | |
6 | use Wikimedia\JsonCodec\Hint; |
7 | use Wikimedia\JsonCodec\JsonCodecable; |
8 | use Wikimedia\JsonCodec\JsonCodecableTrait; |
9 | use Wikimedia\Parsoid\Utils\CompatJsonCodec; |
10 | |
11 | /** |
12 | * Table of Contents data, including an array of section metadata. |
13 | * |
14 | * This is simply an array of SectionMetadata objects for now along |
15 | * with extension data, but may include additional ToC properties in |
16 | * the future. |
17 | */ |
18 | class TOCData implements \JsonSerializable, JsonCodecable { |
19 | use JsonCodecableTrait; |
20 | |
21 | /** |
22 | * The sections in this Table of Contents. |
23 | * @var SectionMetadata[] |
24 | */ |
25 | private array $sections; |
26 | |
27 | /** |
28 | * Arbitrary data attached to this Table of Contents by |
29 | * extensions. This data will be stored and cached in the |
30 | * ParserOutput object along with the rest of the table of |
31 | * contents data, and made available to external clients via the |
32 | * action API. |
33 | * |
34 | * See ParserOutput::setExtensionData() for more information on typical |
35 | * use, and SectionMetadata::setExtensionData() for a method appropriate |
36 | * for attaching information to a specific section of the ToC. |
37 | */ |
38 | private array $extensionData = []; |
39 | |
40 | /** |
41 | * -------------------------------------------------- |
42 | * These next 4 properties are temporary state needed |
43 | * to construct section metadata objects used in TOC. |
44 | * These state properties are not useful once that is |
45 | * done and will not be exported or serialized. |
46 | * -------------------------------------------------- |
47 | */ |
48 | |
49 | /** @var int Temporary TOC State */ |
50 | private $tocLevel = 0; |
51 | |
52 | /** @var int Temporary TOC State */ |
53 | private $prevLevel = 0; |
54 | |
55 | /** @var array<int> Temporary TOC State */ |
56 | private $levelCount = []; |
57 | |
58 | /** @var array<int> Temporary TOC State */ |
59 | private $subLevelCount = []; |
60 | |
61 | /** |
62 | * Create a new TOCData object with the given sections and no |
63 | * extension data. |
64 | * @param SectionMetadata ...$sections |
65 | */ |
66 | public function __construct( ...$sections ) { |
67 | $this->sections = $sections; |
68 | } |
69 | |
70 | /** |
71 | * Return current TOC level while headings are being |
72 | * processed and section metadata is being constructed. |
73 | * @return int |
74 | */ |
75 | public function getCurrentTOCLevel(): int { |
76 | return $this->tocLevel; |
77 | } |
78 | |
79 | /** |
80 | * Add a new section to this TOCData. |
81 | * @param SectionMetadata $s |
82 | */ |
83 | public function addSection( SectionMetadata $s ) { |
84 | $this->sections[] = $s; |
85 | } |
86 | |
87 | /** |
88 | * Get the list of sections in the TOCData. |
89 | * @return SectionMetadata[] |
90 | */ |
91 | public function getSections() { |
92 | return $this->sections; |
93 | } |
94 | |
95 | /** |
96 | * Attaches arbitrary data to this TOCData object. This can be |
97 | * used to store some information about the table of contents in |
98 | * the ParserOutput object for later use during page output. The |
99 | * data will be cached along with the ParserOutput object. |
100 | * |
101 | * See ParserOutput::setExtensionData() in core for further information |
102 | * about typical usage in hooks, and SectionMetadata::setExtensionData() |
103 | * for a similar method appropriate for information about a specific |
104 | * section of the ToC. |
105 | * |
106 | * Setting conflicting values for the same key is not allowed. |
107 | * If you call ::setExtensionData() multiple times with the same key |
108 | * on a TOCData, is is expected that the value will be identical |
109 | * each time. If you want to collect multiple pieces of data under a |
110 | * single key, use ::appendExtensionData(). |
111 | * |
112 | * @note Only scalar values (numbers, strings, or arrays) are |
113 | * supported as a value. (A future revision will allow anything |
114 | * that core's JsonCodec can handle.) Attempts to set other types |
115 | * as extension data values will break ParserCache for the page. |
116 | * |
117 | * @todo When values more complex than scalar values get supported, |
118 | * __clone needs to be updated accordingly. |
119 | * |
120 | * @param string $key The key for accessing the data. Extensions |
121 | * should take care to avoid conflicts in naming keys. It is |
122 | * suggested to use the extension's name as a prefix. Using |
123 | * the prefix `mw:` is reserved for core. |
124 | * |
125 | * @param mixed $value The value to set. |
126 | * Setting a value to null is equivalent to removing the value. |
127 | */ |
128 | public function setExtensionData( string $key, $value ): void { |
129 | if ( |
130 | array_key_exists( $key, $this->extensionData ) && |
131 | $this->extensionData[$key] !== $value |
132 | ) { |
133 | throw new \InvalidArgumentException( "Conflicting data for $key" ); |
134 | } |
135 | if ( $value === null ) { |
136 | unset( $this->extensionData[$key] ); |
137 | } else { |
138 | $this->extensionData[$key] = $value; |
139 | } |
140 | } |
141 | |
142 | /** |
143 | * Appends arbitrary data to this TOCData. This can be used to |
144 | * store some information about the table of contents in the |
145 | * ParserOutput object for later use during page output. |
146 | * |
147 | * See ::setExtensionData() for more details on rationale and use. |
148 | * |
149 | * @param string $key The key for accessing the data. Extensions should take care to avoid |
150 | * conflicts in naming keys. It is suggested to use the extension's name as a prefix. |
151 | * |
152 | * @param int|string $value The value to append to the list. |
153 | * @return never This method is not yet implemented. |
154 | */ |
155 | public function appendExtensionData( string $key, $value ): void { |
156 | // This implementation would mirror that of |
157 | // ParserOutput::appendExtensionData, but let's defer implementing |
158 | // this until we're sure we need it. In particular, we might need |
159 | // to figure out how a merge on section data is expected to work |
160 | // before we can determine the right semantics for this. |
161 | throw new \InvalidArgumentException( "Not yet implemented" ); |
162 | } |
163 | |
164 | /** |
165 | * Gets extension data previously attached to this TOCData. |
166 | * |
167 | * @param string $key The key to look up |
168 | * @return mixed|null The value(s) previously set for the given key using |
169 | * ::setExtensionData() or ::appendExtensionData(), or null if no |
170 | * value was set for this key. |
171 | */ |
172 | public function getExtensionData( $key ) { |
173 | $value = $this->extensionData[$key] ?? null; |
174 | return $value; |
175 | } |
176 | |
177 | /** |
178 | * Return as associative array, in the legacy format returned by the |
179 | * action API. |
180 | * |
181 | * This is helpful as b/c support while we transition to objects, |
182 | * but it drops some properties from this class and shouldn't be used |
183 | * in new code. |
184 | * @return array |
185 | */ |
186 | public function toLegacy(): array { |
187 | return array_map( |
188 | static function ( $s ) { |
189 | return $s->toLegacy(); |
190 | }, |
191 | $this->sections |
192 | ); |
193 | } |
194 | |
195 | /** |
196 | * Create a new TOCData object from the legacy associative array format. |
197 | * |
198 | * This is used for backward compatibility, but the associative array |
199 | * format does not include any properties of the TOCData other than the |
200 | * section list. |
201 | * |
202 | * @param array $data Associative array with ToC data in legacy format |
203 | * @return TOCData |
204 | */ |
205 | public static function fromLegacy( array $data ): TOCData { |
206 | // The legacy format has no way to represent extension data. |
207 | $sections = array_map( |
208 | static function ( $d ) { |
209 | return SectionMetadata::fromLegacy( $d ); |
210 | }, |
211 | $data |
212 | ); |
213 | return new TOCData( ...$sections ); |
214 | } |
215 | |
216 | /** |
217 | * @param int $oldLevel level of the heading (H1/H2, etc.) |
218 | * @param int $level level of the heading (H1/H2, etc.) |
219 | * @param SectionMetadata $metadata This metadata will be updated |
220 | * This logic is copied from Parser.php::finalizeHeadings |
221 | */ |
222 | public function processHeading( int $oldLevel, int $level, SectionMetadata $metadata ): void { |
223 | if ( $this->tocLevel ) { |
224 | $this->prevLevel = $oldLevel; |
225 | } |
226 | |
227 | if ( $level > $this->prevLevel ) { |
228 | # increase TOC level |
229 | $this->tocLevel++; |
230 | $this->subLevelCount[$this->tocLevel] = 0; |
231 | } elseif ( $level < $this->prevLevel && $this->tocLevel > 1 ) { |
232 | # Decrease TOC level, find level to jump to |
233 | for ( $i = $this->tocLevel; $i > 0; $i-- ) { |
234 | if ( $this->levelCount[$i] === $level ) { |
235 | # Found last matching level |
236 | $this->tocLevel = $i; |
237 | break; |
238 | } elseif ( $this->levelCount[$i] < $level ) { |
239 | # Found first matching level below current level |
240 | $this->tocLevel = $i + 1; |
241 | break; |
242 | } |
243 | } |
244 | if ( $i === 0 ) { |
245 | $this->tocLevel = 1; |
246 | } |
247 | } |
248 | |
249 | $this->levelCount[$this->tocLevel] = $level; |
250 | |
251 | # count number of headlines for each level |
252 | $this->subLevelCount[$this->tocLevel]++; |
253 | $numbering = ''; |
254 | $dot = false; |
255 | for ( $i = 1; $i <= $this->tocLevel; $i++ ) { |
256 | if ( !empty( $this->subLevelCount[$i] ) ) { |
257 | if ( $dot ) { |
258 | $numbering .= '.'; |
259 | } |
260 | $numbering .= $this->subLevelCount[$i]; |
261 | $dot = true; |
262 | } |
263 | } |
264 | |
265 | $metadata->hLevel = $level; |
266 | $metadata->tocLevel = $this->tocLevel; |
267 | $metadata->number = $numbering; |
268 | } |
269 | |
270 | /** |
271 | * Serialize all data in the TOCData as JSON. |
272 | * |
273 | * Unlike the `:toLegacy()` method, this method will include *all* |
274 | * of the properties in the TOCData so that the serialization is |
275 | * reversible. |
276 | * |
277 | * @inheritDoc |
278 | */ |
279 | public function jsonSerialize(): array { |
280 | # T312589 explicitly calling jsonSerialize() on the elements of |
281 | # $this->sections will be unnecessary in the future. |
282 | $sections = array_map( |
283 | static function ( SectionMetadata $s ) { |
284 | return $s->jsonSerialize(); |
285 | }, |
286 | $this->sections |
287 | ); |
288 | return [ |
289 | 'sections' => $sections, |
290 | 'extensionData' => $this->extensionData, |
291 | ]; |
292 | } |
293 | |
294 | // JsonCodecable interface |
295 | |
296 | /** @inheritDoc */ |
297 | public function toJsonArray(): array { |
298 | return [ |
299 | 'sections' => $this->sections, |
300 | 'extensionData' => $this->extensionData, |
301 | ]; |
302 | } |
303 | |
304 | /** @inheritDoc */ |
305 | public static function newFromJsonArray( array $json ) { |
306 | $tocData = new TOCData( ...$json['sections'] ); |
307 | foreach ( $json['extensionData'] as $key => $value ) { |
308 | $tocData->setExtensionData( $key, $value ); |
309 | } |
310 | return $tocData; |
311 | } |
312 | |
313 | /** @inheritDoc */ |
314 | public static function jsonClassHintFor( string $keyName ) { |
315 | if ( $keyName === 'sections' ) { |
316 | return Hint::build( SectionMetadata::class, Hint::LIST ); |
317 | } |
318 | return null; |
319 | } |
320 | |
321 | // Pretty-printing |
322 | |
323 | /** |
324 | * For use in parser tests and wherever else humans might appreciate |
325 | * some formatting in the JSON encoded output. |
326 | * @return string |
327 | */ |
328 | public function prettyPrint(): string { |
329 | $out = [ "Sections:" ]; |
330 | foreach ( $this->sections as $s ) { |
331 | $out[] = $s->prettyPrint(); |
332 | } |
333 | if ( $this->extensionData ) { |
334 | $out[] = "Extension Data:"; |
335 | $codec = new CompatJsonCodec(); |
336 | $out[] = json_encode( $codec->toJsonArray( $this->extensionData ) ); |
337 | } |
338 | return implode( "\n", $out ); |
339 | } |
340 | |
341 | public function __clone() { |
342 | foreach ( $this->sections as $k => $v ) { |
343 | $this->sections[$k] = clone $v; |
344 | } |
345 | } |
346 | } |