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