Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
TOCData
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 12
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
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
 prettyPrint
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace 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 */
13class 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}