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