Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
TOCData
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 15
1122
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 / 1
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
 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
2
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;
10use 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 */
19class 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}