Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
95.24% |
80 / 84 |
|
72.73% |
8 / 11 |
CRAP | |
0.00% |
0 / 1 |
HandleTOCMarkers | |
95.24% |
80 / 84 |
|
72.73% |
8 / 11 |
32 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldRun | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
transformText | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
injectTOC | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
resolveUserLanguage | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
4.37 | |||
tocIndent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tocUnindent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
tocLine | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
tocLineEnd | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
tocList | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
1 | |||
generateTOC | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
10 |
1 | <?php |
2 | |
3 | namespace MediaWiki\OutputTransform\Stages; |
4 | |
5 | use Language; |
6 | use MediaWiki\Context\RequestContext; |
7 | use MediaWiki\Html\Html; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\MediaWikiServices; |
10 | use MediaWiki\OutputTransform\ContentTextTransformStage; |
11 | use MediaWiki\Parser\Parser; |
12 | use MediaWiki\Parser\ParserOutput; |
13 | use MediaWiki\Parser\Sanitizer; |
14 | use MediaWiki\Tidy\TidyDriverBase; |
15 | use ParserOptions; |
16 | use Wikimedia\Parsoid\Core\TOCData; |
17 | |
18 | /** |
19 | * Inject table of contents (or empty string if there's no sections) |
20 | * @internal |
21 | */ |
22 | class HandleTOCMarkers extends ContentTextTransformStage { |
23 | |
24 | private TidyDriverBase $tidy; |
25 | |
26 | public function __construct( TidyDriverBase $tidy ) { |
27 | $this->tidy = $tidy; |
28 | } |
29 | |
30 | public function shouldRun( ParserOutput $po, ?ParserOptions $popts, array $options = [] ): bool { |
31 | return !( $options['allowTOC'] ?? true ) || ( $options['injectTOC'] ?? true ); |
32 | } |
33 | |
34 | protected function transformText( string $text, ParserOutput $po, ?ParserOptions $popts, array &$options ): string { |
35 | if ( ( $options['allowTOC'] ?? true ) && ( $options['injectTOC'] ?? true ) ) { |
36 | return $this->injectTOC( $text, $po, $options ); |
37 | } |
38 | if ( !( $options['allowTOC'] ?? true ) ) { |
39 | return Parser::replaceTableOfContentsMarker( $text, '' ); |
40 | } |
41 | return $text; |
42 | } |
43 | |
44 | private function injectTOC( string $text, ParserOutput $po, array $options ): string { |
45 | $lang = $this->resolveUserLanguage( $options ); |
46 | $numSections = count( $po->getSections() ); |
47 | $tocData = $po->getTOCData(); |
48 | if ( $numSections === 0 ) { |
49 | $toc = ''; |
50 | } else { |
51 | $toc = self::generateTOC( $tocData, $lang ); |
52 | // TODO: This may no longer be needed since Ic0a805f29c928d0c2edf266ea045b0d29bb45a28 |
53 | $toc = $this->tidy->tidy( $toc, [ Sanitizer::class, 'armorFrenchSpaces' ] ); |
54 | } |
55 | |
56 | return Parser::replaceTableOfContentsMarker( $text, $toc ); |
57 | } |
58 | |
59 | /** |
60 | * Extracts the userLanguage from the $options array, with a fallback on skin language and request |
61 | * context language |
62 | * @param array $options |
63 | * @return Language |
64 | */ |
65 | private function resolveUserLanguage( array $options ): Language { |
66 | $userLang = $options['userLang'] ?? null; |
67 | $skin = $options['skin'] ?? null; |
68 | if ( ( !$userLang ) && $skin ) { |
69 | // TODO: We probably don't need a full Skin option here |
70 | $userLang = $skin->getLanguage(); |
71 | } |
72 | if ( !$userLang ) { |
73 | // T348853 passing either userLang or skin will be mandatory in the future |
74 | $userLang = RequestContext::getMain()->getLanguage(); |
75 | } |
76 | return $userLang; |
77 | } |
78 | |
79 | /** |
80 | * Add another level to the Table of Contents |
81 | * |
82 | * @return string |
83 | */ |
84 | private static function tocIndent() { |
85 | return "\n<ul>\n"; |
86 | } |
87 | |
88 | /** |
89 | * Finish one or more sublevels on the Table of Contents |
90 | * |
91 | * @param int $level |
92 | * @return string |
93 | */ |
94 | private static function tocUnindent( $level ) { |
95 | return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 ); |
96 | } |
97 | |
98 | /** |
99 | * parameter level defines if we are on an indentation level |
100 | * |
101 | * @param string $linkAnchor Identifier |
102 | * @param string $tocline Properly escaped HTML |
103 | * @param string $tocnumber Unescaped text |
104 | * @param int $level |
105 | * @param string|false $sectionIndex |
106 | * @return string |
107 | */ |
108 | private static function tocLine( $linkAnchor, $tocline, $tocnumber, $level, $sectionIndex = false ) { |
109 | $classes = "toclevel-$level"; |
110 | |
111 | // Parser.php used to suppress tocLine by setting $sectionindex to false. |
112 | // In those circumstances, we can now encounter '' or a "T-" prefixed index |
113 | // for when the section comes from templates. |
114 | if ( $sectionIndex !== false && $sectionIndex !== '' && !str_starts_with( $sectionIndex, "T-" ) ) { |
115 | $classes .= " tocsection-$sectionIndex"; |
116 | } |
117 | |
118 | // <li class="$classes"><a href="#$linkAnchor"><span class="tocnumber"> |
119 | // $tocnumber</span> <span class="toctext">$tocline</span></a> |
120 | return Html::openElement( 'li', [ 'class' => $classes ] ) |
121 | . Html::rawElement( 'a', |
122 | [ 'href' => "#$linkAnchor" ], |
123 | Html::element( 'span', [ 'class' => 'tocnumber' ], $tocnumber ) |
124 | . ' ' |
125 | . Html::rawElement( 'span', [ 'class' => 'toctext' ], $tocline ) |
126 | ); |
127 | } |
128 | |
129 | /** |
130 | * End a Table Of Contents line. |
131 | * tocUnindent() will be used instead if we're ending a line below |
132 | * the new level. |
133 | * @return string |
134 | */ |
135 | private static function tocLineEnd() { |
136 | return "</li>\n"; |
137 | } |
138 | |
139 | /** |
140 | * Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript. |
141 | * |
142 | * @param string $toc Html of the Table Of Contents |
143 | * @param Language|null $lang Language for the toc title, defaults to user language |
144 | * @return string Full html of the TOC |
145 | */ |
146 | private static function tocList( $toc, Language $lang = null ) { |
147 | $lang ??= RequestContext::getMain()->getLanguage(); |
148 | |
149 | $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); |
150 | |
151 | return '<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading">' |
152 | . Html::element( 'input', [ |
153 | 'type' => 'checkbox', |
154 | 'role' => 'button', |
155 | 'id' => 'toctogglecheckbox', |
156 | 'class' => 'toctogglecheckbox', |
157 | 'style' => 'display:none', |
158 | ] ) |
159 | . Html::openElement( 'div', [ |
160 | 'class' => 'toctitle', |
161 | 'lang' => $lang->getHtmlCode(), |
162 | 'dir' => $lang->getDir(), |
163 | ] ) |
164 | . '<h2 id="mw-toc-heading">' . $title . '</h2>' |
165 | . '<span class="toctogglespan">' |
166 | . Html::label( '', 'toctogglecheckbox', [ |
167 | 'class' => 'toctogglelabel', |
168 | ] ) |
169 | . '</span>' |
170 | . '</div>' |
171 | . $toc |
172 | . "</ul>\n</div>\n"; |
173 | } |
174 | |
175 | /** |
176 | * Generate a table of contents from a section tree. |
177 | * |
178 | * @param ?TOCData $tocData Return value of ParserOutput::getSections() |
179 | * @param Language|null $lang Language for the toc title, defaults to user language |
180 | * @param array $options |
181 | * - 'maxtoclevel' Max TOC level to generate |
182 | * @return string HTML fragment |
183 | */ |
184 | private static function generateTOC( ?TOCData $tocData, Language $lang = null, array $options = [] ): string { |
185 | $toc = ''; |
186 | $lastLevel = 0; |
187 | $maxTocLevel = $options['maxtoclevel'] ?? null; |
188 | if ( $maxTocLevel === null ) { |
189 | // Use wiki-configured default |
190 | $services = MediaWikiServices::getInstance(); |
191 | $config = $services->getMainConfig(); |
192 | $maxTocLevel = $config->get( MainConfigNames::MaxTocLevel ); |
193 | } |
194 | foreach ( ( $tocData ? $tocData->getSections() : [] ) as $section ) { |
195 | $tocLevel = $section->tocLevel; |
196 | if ( $tocLevel < $maxTocLevel ) { |
197 | if ( $tocLevel > $lastLevel ) { |
198 | $toc .= self::tocIndent(); |
199 | } elseif ( $tocLevel < $lastLevel ) { |
200 | if ( $lastLevel < $maxTocLevel ) { |
201 | $toc .= self::tocUnindent( |
202 | $lastLevel - $tocLevel ); |
203 | } else { |
204 | $toc .= self::tocLineEnd(); |
205 | } |
206 | } else { |
207 | $toc .= self::tocLineEnd(); |
208 | } |
209 | |
210 | $toc .= self::tocLine( $section->linkAnchor, |
211 | $section->line, $section->number, |
212 | $tocLevel, $section->index ); |
213 | $lastLevel = $tocLevel; |
214 | } |
215 | } |
216 | if ( $lastLevel < $maxTocLevel && $lastLevel > 0 ) { |
217 | $toc .= self::tocUnindent( $lastLevel - 1 ); |
218 | } |
219 | return self::tocList( $toc, $lang ); |
220 | } |
221 | } |