Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 69 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
| TOCData | |
0.00% |
0 / 69 |
|
0.00% |
0 / 15 |
1122 | |
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 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| processHeading | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
156 | |||
| 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 |
2 | |||
| 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 | use Wikimedia\Parsoid\Utils\Utils; |
| 11 | |
| 12 | /** |
| 13 | * Table of Contents data, including an array of section metadata. |
| 14 | * |
| 15 | * This is simply an array of SectionMetadata objects for now along |
| 16 | * with extension data, but may include additional ToC properties in |
| 17 | * the future. |
| 18 | */ |
| 19 | class TOCData implements JsonCodecable { |
| 20 | use JsonCodecableTrait; |
| 21 | |
| 22 | /** |
| 23 | * The sections in this Table of Contents. |
| 24 | * @var SectionMetadata[] |
| 25 | */ |
| 26 | private array $sections; |
| 27 | |
| 28 | /** |
| 29 | * Arbitrary data attached to this Table of Contents by |
| 30 | * extensions. This data will be stored and cached in the |
| 31 | * ParserOutput object along with the rest of the table of |
| 32 | * contents data, and made available to external clients via the |
| 33 | * action API. |
| 34 | * |
| 35 | * See ParserOutput::setExtensionData() for more information on typical |
| 36 | * use, and SectionMetadata::setExtensionData() for a method appropriate |
| 37 | * for attaching information to a specific section of the ToC. |
| 38 | */ |
| 39 | private array $extensionData = []; |
| 40 | |
| 41 | /** |
| 42 | * -------------------------------------------------- |
| 43 | * These next 4 properties are temporary state needed |
| 44 | * to construct section metadata objects used in TOC. |
| 45 | * These state properties are not useful once that is |
| 46 | * done and will not be exported or serialized. |
| 47 | * -------------------------------------------------- |
| 48 | */ |
| 49 | |
| 50 | /** @var int Temporary TOC State */ |
| 51 | private $tocLevel = 0; |
| 52 | |
| 53 | /** @var int Temporary TOC State */ |
| 54 | private $prevLevel = 0; |
| 55 | |
| 56 | /** @var array<int> Temporary TOC State */ |
| 57 | private $levelCount = []; |
| 58 | |
| 59 | /** @var array<int> Temporary TOC State */ |
| 60 | private $subLevelCount = []; |
| 61 | |
| 62 | /** |
| 63 | * Create a new TOCData object with the given sections and no |
| 64 | * extension data. |
| 65 | * @param SectionMetadata ...$sections |
| 66 | */ |
| 67 | public function __construct( ...$sections ) { |
| 68 | $this->sections = $sections; |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Return current TOC level while headings are being |
| 73 | * processed and section metadata is being constructed. |
| 74 | * @return int |
| 75 | */ |
| 76 | public function getCurrentTOCLevel(): int { |
| 77 | return $this->tocLevel; |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Add a new section to this TOCData. |
| 82 | * |
| 83 | * @param SectionMetadata $s |
| 84 | */ |
| 85 | public function addSection( SectionMetadata $s ): void { |
| 86 | $this->sections[] = $s; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * Get the list of sections in the TOCData. |
| 91 | * @return SectionMetadata[] |
| 92 | */ |
| 93 | public function getSections() { |
| 94 | return $this->sections; |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * Attaches arbitrary data to this TOCData object. This can be |
| 99 | * used to store some information about the table of contents in |
| 100 | * the ParserOutput object for later use during page output. The |
| 101 | * data will be cached along with the ParserOutput object. |
| 102 | * |
| 103 | * See ParserOutput::setExtensionData() in core for further information |
| 104 | * about typical usage in hooks, and SectionMetadata::setExtensionData() |
| 105 | * for a similar method appropriate for information about a specific |
| 106 | * section of the ToC. |
| 107 | * |
| 108 | * Setting conflicting values for the same key is not allowed. |
| 109 | * If you call ::setExtensionData() multiple times with the same key |
| 110 | * on a TOCData, is is expected that the value will be identical |
| 111 | * each time. If you want to collect multiple pieces of data under a |
| 112 | * single key, use ::appendExtensionData(). |
| 113 | * |
| 114 | * @note Only scalars (numbers, strings, or arrays) or |
| 115 | * `JsonCodecable` objects are supported for `$value`. Attempts to set |
| 116 | * other types as extension data values will break ParserCache for the |
| 117 | * page. Object values should support the built-in PHP `clone` |
| 118 | * operator. |
| 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 | * |
| 185 | * @return array<array> |
| 186 | */ |
| 187 | public function toLegacy(): array { |
| 188 | return array_map( |
| 189 | static function ( $s ) { |
| 190 | return $s->toLegacy(); |
| 191 | }, |
| 192 | $this->sections |
| 193 | ); |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * Create a new TOCData object from the legacy associative array format. |
| 198 | * |
| 199 | * This is used for backward compatibility, but the associative array |
| 200 | * format does not include any properties of the TOCData other than the |
| 201 | * section list. |
| 202 | * |
| 203 | * @param array $data Associative array with ToC data in legacy format |
| 204 | * @return TOCData |
| 205 | */ |
| 206 | public static function fromLegacy( array $data ): TOCData { |
| 207 | // The legacy format has no way to represent extension data. |
| 208 | return new TOCData( ...array_map( SectionMetadata::fromLegacy( ... ), $data ) ); |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * @param int $oldLevel level of the heading (H1/H2, etc.) |
| 213 | * @param int $level level of the heading (H1/H2, etc.) |
| 214 | * @param SectionMetadata $metadata This metadata will be updated |
| 215 | * This logic is copied from Parser.php::finalizeHeadings |
| 216 | */ |
| 217 | public function processHeading( int $oldLevel, int $level, SectionMetadata $metadata ): void { |
| 218 | if ( $this->tocLevel ) { |
| 219 | $this->prevLevel = $oldLevel; |
| 220 | } |
| 221 | |
| 222 | if ( $level > $this->prevLevel ) { |
| 223 | # increase TOC level |
| 224 | $this->tocLevel++; |
| 225 | $this->subLevelCount[$this->tocLevel] = 0; |
| 226 | } elseif ( $level < $this->prevLevel && $this->tocLevel > 1 ) { |
| 227 | # Decrease TOC level, find level to jump to |
| 228 | for ( $i = $this->tocLevel; $i > 0; $i-- ) { |
| 229 | if ( $this->levelCount[$i] === $level ) { |
| 230 | # Found last matching level |
| 231 | $this->tocLevel = $i; |
| 232 | break; |
| 233 | } elseif ( $this->levelCount[$i] < $level ) { |
| 234 | # Found first matching level below current level |
| 235 | $this->tocLevel = $i + 1; |
| 236 | break; |
| 237 | } |
| 238 | } |
| 239 | if ( $i === 0 ) { |
| 240 | $this->tocLevel = 1; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | $this->levelCount[$this->tocLevel] = $level; |
| 245 | |
| 246 | # count number of headlines for each level |
| 247 | $this->subLevelCount[$this->tocLevel]++; |
| 248 | $numbering = ''; |
| 249 | $dot = false; |
| 250 | for ( $i = 1; $i <= $this->tocLevel; $i++ ) { |
| 251 | if ( !empty( $this->subLevelCount[$i] ) ) { |
| 252 | if ( $dot ) { |
| 253 | $numbering .= '.'; |
| 254 | } |
| 255 | $numbering .= $this->subLevelCount[$i]; |
| 256 | $dot = true; |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | $metadata->hLevel = $level; |
| 261 | $metadata->tocLevel = $this->tocLevel; |
| 262 | $metadata->number = $numbering; |
| 263 | } |
| 264 | |
| 265 | // JsonCodecable interface |
| 266 | |
| 267 | /** |
| 268 | * Serialize all data in the TOCData as JSON. |
| 269 | * |
| 270 | * Unlike the `:toLegacy()` method, this method will include *all* |
| 271 | * of the properties in the TOCData so that the serialization is |
| 272 | * reversible. |
| 273 | * |
| 274 | * @inheritDoc |
| 275 | */ |
| 276 | public function toJsonArray(): array { |
| 277 | return [ |
| 278 | 'sections' => $this->sections, |
| 279 | 'extensionData' => $this->extensionData, |
| 280 | ]; |
| 281 | } |
| 282 | |
| 283 | /** @inheritDoc */ |
| 284 | public static function newFromJsonArray( array $json ) { |
| 285 | $tocData = new TOCData( ...$json['sections'] ); |
| 286 | foreach ( ( $json['extensionData'] ?? [] ) as $key => $value ) { |
| 287 | $tocData->setExtensionData( $key, $value ); |
| 288 | } |
| 289 | return $tocData; |
| 290 | } |
| 291 | |
| 292 | /** |
| 293 | * @inheritDoc |
| 294 | * |
| 295 | * @return ?Hint<SectionMetadata> |
| 296 | */ |
| 297 | public static function jsonClassHintFor( string $keyName ) { |
| 298 | if ( $keyName === 'sections' ) { |
| 299 | return Hint::build( SectionMetadata::class, Hint::LIST ); |
| 300 | } |
| 301 | return null; |
| 302 | } |
| 303 | |
| 304 | // Pretty-printing |
| 305 | |
| 306 | /** |
| 307 | * For use in parser tests and wherever else humans might appreciate |
| 308 | * some formatting in the JSON encoded output. |
| 309 | * @return string |
| 310 | */ |
| 311 | public function prettyPrint(): string { |
| 312 | $out = [ "Sections:" ]; |
| 313 | foreach ( $this->sections as $s ) { |
| 314 | $out[] = $s->prettyPrint(); |
| 315 | } |
| 316 | if ( $this->extensionData ) { |
| 317 | $out[] = "Extension Data:"; |
| 318 | $codec = new CompatJsonCodec(); |
| 319 | $out[] = json_encode( $codec->toJsonArray( $this->extensionData ) ); |
| 320 | } |
| 321 | return implode( "\n", $out ); |
| 322 | } |
| 323 | |
| 324 | public function __clone() { |
| 325 | $this->sections = Utils::cloneArray( $this->sections ); |
| 326 | $this->extensionData = Utils::cloneArray( $this->extensionData ); |
| 327 | } |
| 328 | } |