Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
TOCData
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 16
1260
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentTOCLevel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addSection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSections
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setExtensionData
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 appendExtensionData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtensionData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 toLegacy
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 fromLegacy
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 processHeading
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
156
 jsonSerialize
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 toJsonArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 newFromJsonArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 jsonClassHintFor
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 prettyPrint
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 __clone
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Core;
5
6use Wikimedia\JsonCodec\Hint;
7use Wikimedia\JsonCodec\JsonCodecable;
8use Wikimedia\JsonCodec\JsonCodecableTrait;
9use 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 */
18class 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}