Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
38.36% |
89 / 232 |
|
10.53% |
2 / 19 |
CRAP | |
0.00% |
0 / 1 |
SyntaxHighlight | |
38.36% |
89 / 232 |
|
10.53% |
2 / 19 |
1502.73 | |
0.00% |
0 / 1 |
getMaxLines | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getMaxBytes | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getLexer | |
46.15% |
6 / 13 |
|
0.00% |
0 / 1 |
8.90 | |||
onParserFirstCallInit | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
parserHookSource | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getModuleStyles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
processContent | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
parserHook | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
sourceToDom | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
unwrap | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
plainCodeWrap | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
highlightInner | |
64.06% |
41 / 64 |
|
0.00% |
0 / 1 |
44.46 | |||
highlight | |
56.25% |
27 / 48 |
|
0.00% |
0 / 1 |
18.37 | |||
makeCacheKeyHash | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
parseHighlightLines | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
validHighlightRange | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
onContentGetParserOutput | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
onApiFormatHighlight | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
onSoftwareInfo | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | */ |
18 | |
19 | namespace MediaWiki\SyntaxHighlight; |
20 | |
21 | use Content; |
22 | use ExtensionRegistry; |
23 | use FormatJson; |
24 | use IContextSource; |
25 | use MediaWiki\Api\Hook\ApiFormatHighlightHook; |
26 | use MediaWiki\Content\Hook\ContentGetParserOutputHook; |
27 | use MediaWiki\Hook\ParserFirstCallInitHook; |
28 | use MediaWiki\Hook\SoftwareInfoHook; |
29 | use MediaWiki\Html\Html; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Parser\ParserOutput; |
32 | use MediaWiki\Parser\Sanitizer; |
33 | use MediaWiki\Status\Status; |
34 | use MediaWiki\Title\Title; |
35 | use Parser; |
36 | use ParserOptions; |
37 | use RuntimeException; |
38 | use TextContent; |
39 | use WANObjectCache; |
40 | use Wikimedia\Parsoid\DOM\DocumentFragment; |
41 | use Wikimedia\Parsoid\Ext\ExtensionTagHandler; |
42 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
43 | |
44 | class SyntaxHighlight extends ExtensionTagHandler implements |
45 | ParserFirstCallInitHook, |
46 | ContentGetParserOutputHook, |
47 | ApiFormatHighlightHook, |
48 | SoftwareInfoHook |
49 | { |
50 | |
51 | /** @var string CSS class for syntax-highlighted code. Public as used by the updateCSS maintenance script. */ |
52 | public const HIGHLIGHT_CSS_CLASS = 'mw-highlight'; |
53 | |
54 | /** @var int Cache version. Increment whenever the HTML changes. */ |
55 | private const CACHE_VERSION = 2; |
56 | |
57 | /** @var array<string,string> Mapping of MIME-types to lexer names. */ |
58 | private static $mimeLexers = [ |
59 | 'text/javascript' => 'javascript', |
60 | 'application/json' => 'javascript', |
61 | 'text/xml' => 'xml', |
62 | ]; |
63 | |
64 | /** |
65 | * Returns the maximum number of lines that may be selected for highlighting |
66 | * |
67 | * @return int |
68 | */ |
69 | private static function getMaxLines(): int { |
70 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
71 | return $config->get( 'SyntaxHighlightMaxLines' ); |
72 | } |
73 | |
74 | /** |
75 | * Returns the maximum input size for the highlighter |
76 | * |
77 | * @return int |
78 | */ |
79 | private static function getMaxBytes(): int { |
80 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
81 | return $config->get( 'SyntaxHighlightMaxBytes' ); |
82 | } |
83 | |
84 | /** |
85 | * Get the Pygments lexer name for a particular language. |
86 | * |
87 | * @param string|null $lang Language name. |
88 | * @return string|null Lexer name, or null if no matching lexer. |
89 | */ |
90 | private static function getLexer( $lang ) { |
91 | static $lexers = null; |
92 | |
93 | if ( $lang === null ) { |
94 | return null; |
95 | } |
96 | |
97 | $lexers ??= Pygmentize::getLexers(); |
98 | |
99 | $lexer = strtolower( $lang ); |
100 | |
101 | if ( isset( $lexers[$lexer] ) ) { |
102 | return $lexer; |
103 | } |
104 | |
105 | $geshi2pygments = SyntaxHighlightGeSHiCompat::getGeSHiToPygmentsMap(); |
106 | |
107 | // Check if this is a GeSHi lexer name for which there exists |
108 | // a compatible Pygments lexer with a different name. |
109 | if ( isset( $geshi2pygments[$lexer] ) ) { |
110 | $lexer = $geshi2pygments[$lexer]; |
111 | if ( isset( $lexers[$lexer] ) ) { |
112 | return $lexer; |
113 | } |
114 | } |
115 | |
116 | return null; |
117 | } |
118 | |
119 | /** |
120 | * Register parser hook |
121 | * |
122 | * @param Parser $parser |
123 | */ |
124 | public function onParserFirstCallInit( $parser ) { |
125 | $parser->setHook( 'source', [ self::class, 'parserHookSource' ] ); |
126 | $parser->setHook( 'syntaxhighlight', [ self::class, 'parserHook' ] ); |
127 | } |
128 | |
129 | /** |
130 | * Parser hook for <source> to add deprecated tracking category |
131 | * |
132 | * @param ?string $text |
133 | * @param array $args |
134 | * @param Parser $parser |
135 | * @return string |
136 | */ |
137 | public static function parserHookSource( $text, $args, $parser ) { |
138 | $parser->addTrackingCategory( 'syntaxhighlight-source-category' ); |
139 | return self::parserHook( $text, $args, $parser ); |
140 | } |
141 | |
142 | /** |
143 | * @return string[] |
144 | */ |
145 | private static function getModuleStyles(): array { |
146 | return [ 'ext.pygments' ]; |
147 | } |
148 | |
149 | /** |
150 | * |
151 | * @param string $text |
152 | * @param array $args |
153 | * @param ?Parser $parser |
154 | * @return array |
155 | */ |
156 | private static function processContent( string $text, array $args, ?Parser $parser = null ): array { |
157 | // Don't trim leading spaces away, just the linefeeds |
158 | $out = rtrim( trim( $text, "\n" ) ); |
159 | $trackingCats = []; |
160 | |
161 | // Convert deprecated attributes |
162 | if ( isset( $args['enclose'] ) ) { |
163 | if ( $args['enclose'] === 'none' ) { |
164 | $args['inline'] = true; |
165 | } |
166 | unset( $args['enclose'] ); |
167 | $trackingCats[] = 'syntaxhighlight-enclose-category'; |
168 | } |
169 | |
170 | $lexer = $args['lang'] ?? ''; |
171 | |
172 | $result = self::highlight( $out, $lexer, $args, $parser ); |
173 | if ( !$result->isGood() ) { |
174 | $trackingCats[] = 'syntaxhighlight-error-category'; |
175 | } |
176 | |
177 | return [ 'html' => $result->getValue(), 'cats' => $trackingCats ]; |
178 | } |
179 | |
180 | /** |
181 | * Parser hook for both <source> and <syntaxhighlight> logic |
182 | * |
183 | * @param ?string $text |
184 | * @param array $args |
185 | * @param Parser $parser |
186 | * @return string |
187 | */ |
188 | public static function parserHook( $text, $args, $parser ) { |
189 | // Replace strip markers (For e.g. {{#tag:syntaxhighlight|<nowiki>...}}) |
190 | $out = $parser->getStripState()->unstripNoWiki( $text ?? '' ); |
191 | |
192 | $result = self::processContent( $out, $args, $parser ); |
193 | foreach ( $result['cats'] as $cat ) { |
194 | $parser->addTrackingCategory( $cat ); |
195 | } |
196 | |
197 | // Register CSS |
198 | $parser->getOutput()->addModuleStyles( self::getModuleStyles() ); |
199 | if ( !empty( $args['linelinks'] ) ) { |
200 | $parser->getOutput()->addModules( [ 'ext.pygments.linenumbers' ] ); |
201 | } |
202 | |
203 | return $result['html']; |
204 | } |
205 | |
206 | /** @inheritDoc */ |
207 | public function sourceToDom( |
208 | ParsoidExtensionAPI $extApi, string $text, array $extArgs |
209 | ): ?DocumentFragment { |
210 | $result = self::processContent( $text, $extApi->extArgsToArray( $extArgs ) ); |
211 | |
212 | // FIXME: There is no API method in Parsoid to add tracking categories |
213 | // So, $result['cats'] is being ignored |
214 | |
215 | // Register CSS |
216 | $extApi->addModuleStyles( self::getModuleStyles() ); |
217 | |
218 | return $extApi->htmlToDom( $result['html'] ); |
219 | } |
220 | |
221 | /** |
222 | * Unwrap the <div> wrapper of the Pygments output |
223 | * |
224 | * @param string $out Output |
225 | * @return string Unwrapped output |
226 | */ |
227 | private static function unwrap( string $out ): string { |
228 | if ( $out !== '' ) { |
229 | $m = []; |
230 | if ( preg_match( '/^<div class="?mw-highlight"?>(.*)<\/div>$/s', trim( $out ), $m ) ) { |
231 | $out = trim( $m[1] ); |
232 | } else { |
233 | throw new RuntimeException( 'Unexpected output from Pygments encountered' ); |
234 | } |
235 | } |
236 | return $out; |
237 | } |
238 | |
239 | /** |
240 | * @param string $code |
241 | * @param bool $isInline |
242 | * @return string HTML |
243 | */ |
244 | private static function plainCodeWrap( $code, $isInline ) { |
245 | if ( $isInline ) { |
246 | return htmlspecialchars( $code, ENT_NOQUOTES ); |
247 | } |
248 | |
249 | return Html::rawElement( |
250 | 'div', |
251 | [ 'class' => self::HIGHLIGHT_CSS_CLASS ], |
252 | Html::element( 'pre', [], $code ) |
253 | ); |
254 | } |
255 | |
256 | /** |
257 | * @param string $code |
258 | * @param string|null $lang |
259 | * @param array $args |
260 | * @param Parser|null $parser Parser, if generating content to be parsed. |
261 | * @return Status |
262 | */ |
263 | private static function highlightInner( $code, $lang = null, $args = [], ?Parser $parser = null ) { |
264 | $status = new Status; |
265 | |
266 | $lexer = self::getLexer( $lang ); |
267 | if ( $lexer === null && $lang !== null ) { |
268 | $status->warning( 'syntaxhighlight-error-unknown-language', $lang ); |
269 | } |
270 | |
271 | // For empty tag, output nothing instead of empty <pre>. |
272 | if ( $code === '' ) { |
273 | $status->value = ''; |
274 | return $status; |
275 | } |
276 | |
277 | $length = strlen( $code ); |
278 | if ( strlen( $code ) > self::getMaxBytes() ) { |
279 | // Disable syntax highlighting |
280 | $lexer = null; |
281 | $status->warning( |
282 | 'syntaxhighlight-error-exceeds-size-limit', |
283 | $length, |
284 | self::getMaxBytes() |
285 | ); |
286 | } |
287 | |
288 | $isInline = isset( $args['inline'] ); |
289 | $showLines = isset( $args['line'] ); |
290 | |
291 | if ( $isInline ) { |
292 | $code = trim( $code ); |
293 | } |
294 | |
295 | if ( $lexer === null ) { |
296 | // When syntax highlighting is disabled.. |
297 | $status->value = self::plainCodeWrap( $code, $isInline ); |
298 | return $status; |
299 | } |
300 | |
301 | if ( $parser && !$parser->incrementExpensiveFunctionCount() ) { |
302 | // Highlighting is expensive, return unstyled |
303 | $status->value = self::plainCodeWrap( $code, $isInline ); |
304 | return $status; |
305 | } |
306 | |
307 | $options = [ |
308 | 'cssclass' => self::HIGHLIGHT_CSS_CLASS, |
309 | 'encoding' => 'utf-8', |
310 | ]; |
311 | |
312 | // Line numbers |
313 | if ( $showLines ) { |
314 | $options['linenos'] = 'inline'; |
315 | } |
316 | |
317 | if ( $lexer === 'php' && strpos( $code, '<?php' ) === false ) { |
318 | $options['startinline'] = 1; |
319 | } |
320 | |
321 | // Highlight specific lines |
322 | if ( isset( $args['highlight'] ) ) { |
323 | $lines = self::parseHighlightLines( $args['highlight'] ); |
324 | if ( count( $lines ) ) { |
325 | $options['hl_lines'] = implode( ' ', $lines ); |
326 | } |
327 | } |
328 | |
329 | // Starting line number |
330 | if ( isset( $args['start'] ) && ctype_digit( $args['start'] ) ) { |
331 | $options['linenostart'] = (int)$args['start']; |
332 | } |
333 | |
334 | if ( !empty( $args['linelinks'] ) ) { |
335 | $options['linespans'] = $args['linelinks']; |
336 | } |
337 | |
338 | if ( $isInline ) { |
339 | $options['nowrap'] = 1; |
340 | } |
341 | |
342 | $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); |
343 | $error = null; |
344 | $output = $cache->getWithSetCallback( |
345 | $cache->makeGlobalKey( 'highlight', self::makeCacheKeyHash( $code, $lexer, $options ) ), |
346 | $cache::TTL_MONTH, |
347 | static function ( $oldValue, &$ttl ) use ( $code, $lexer, $options, &$error ) { |
348 | try { |
349 | return Pygmentize::highlight( $lexer, $code, $options ); |
350 | } catch ( PygmentsException $e ) { |
351 | $ttl = WANObjectCache::TTL_UNCACHEABLE; |
352 | $error = $e->getMessage(); |
353 | return null; |
354 | } |
355 | } |
356 | ); |
357 | |
358 | if ( $error !== null || $output === null ) { |
359 | $status->warning( 'syntaxhighlight-error-pygments-invocation-failure' ); |
360 | if ( $error !== null ) { |
361 | wfWarn( 'Failed to invoke Pygments: ' . $error ); |
362 | } else { |
363 | wfWarn( 'Invoking Pygments returned blank output with no error response' ); |
364 | } |
365 | |
366 | // Fall back to preformatted code without syntax highlighting |
367 | $output = self::plainCodeWrap( $code, $isInline ); |
368 | } |
369 | |
370 | $status->value = $output; |
371 | |
372 | return $status; |
373 | } |
374 | |
375 | /** |
376 | * Highlight a code-block using a particular lexer. |
377 | * |
378 | * This produces raw HTML (wrapped by Status), the caller is responsible |
379 | * for making sure the "ext.pygments" module is loaded in the output. |
380 | * |
381 | * @param string $code Code to highlight. |
382 | * @param string|null $lang Language name, or null to use plain markup. |
383 | * @param array $args Associative array of additional arguments. |
384 | * If it contains a 'line' key, the output will include line numbers. |
385 | * If it includes a 'highlight' key, the value will be parsed as a |
386 | * comma-separated list of lines and line-ranges to highlight. |
387 | * If it contains a 'start' key, the value will be used as the line at which to |
388 | * start highlighting. |
389 | * If it contains a 'inline' key, the output will not be wrapped in `<div><pre/></div>`. |
390 | * If it contains a 'linelinks' key, lines will have links and anchors with a prefix |
391 | * of the value. Similar to the lineanchors+linespans features in Pygments. |
392 | * @param Parser|null $parser Parser, if generating content to be parsed. |
393 | * @return Status Status object, with HTML representing the highlighted |
394 | * code as its value. |
395 | */ |
396 | public static function highlight( $code, $lang = null, $args = [], ?Parser $parser = null ) { |
397 | $status = self::highlightInner( $code, $lang, $args, $parser ); |
398 | $output = $status->getValue(); |
399 | |
400 | $isInline = isset( $args['inline'] ); |
401 | $showLines = isset( $args['line'] ); |
402 | $lexer = self::getLexer( $lang ); |
403 | |
404 | // Post-Pygment HTML transformations. |
405 | |
406 | if ( $showLines ) { |
407 | $lineReplace = Html::element( 'span', [ 'class' => 'linenos', 'data-line' => '$1' ] ); |
408 | if ( !empty( $args['linelinks'] ) ) { |
409 | $lineReplace = Html::rawElement( |
410 | 'a', |
411 | [ 'href' => '#' . $args['linelinks'] . '-$1' ], |
412 | $lineReplace |
413 | ); |
414 | } |
415 | // Convert line numbers to data attributes so they |
416 | // can be displayed as CSS generated content and be |
417 | // unselectable in all browsers. |
418 | $output = preg_replace( |
419 | '`<span class="linenos">\s*([^<]*)\s*</span>`', |
420 | $lineReplace, |
421 | $output |
422 | ); |
423 | } |
424 | |
425 | // Allow certain HTML attributes |
426 | $htmlAttribs = Sanitizer::validateAttributes( |
427 | $args, array_flip( [ 'style', 'class', 'id' ] ) |
428 | ); |
429 | |
430 | $dir = ( isset( $args['dir'] ) && $args['dir'] === 'rtl' ) ? 'rtl' : 'ltr'; |
431 | |
432 | // Build class list |
433 | $classList = []; |
434 | if ( isset( $htmlAttribs['class'] ) ) { |
435 | $classList[] = $htmlAttribs['class']; |
436 | } |
437 | $classList[] = self::HIGHLIGHT_CSS_CLASS; |
438 | if ( $lexer !== null ) { |
439 | $classList[] = self::HIGHLIGHT_CSS_CLASS . '-lang-' . $lexer; |
440 | } |
441 | $classList[] = 'mw-content-' . $dir; |
442 | if ( $showLines ) { |
443 | $classList[] = self::HIGHLIGHT_CSS_CLASS . '-lines'; |
444 | } |
445 | $htmlAttribs['class'] = implode( ' ', $classList ); |
446 | $htmlAttribs['dir'] = $dir; |
447 | '@phan-var array{class:string,dir:string} $htmlAttribs'; |
448 | |
449 | if ( $isInline ) { |
450 | // We've already trimmed the input $code before highlighting, |
451 | // but pygment's standard out adds a line break afterwards, |
452 | // which would then be preserved in the paragraph that wraps this, |
453 | // and become visible as a space. Avoid that. |
454 | $output = trim( $output ); |
455 | |
456 | // Enforce inlineness. Stray newlines may result in unexpected list and paragraph processing |
457 | // (also known as doBlockLevels()). |
458 | $output = str_replace( "\n", ' ', $output ); |
459 | $output = Html::rawElement( 'code', $htmlAttribs, $output ); |
460 | } else { |
461 | $output = self::unwrap( $output ); |
462 | |
463 | if ( $parser ) { |
464 | // Use 'nowiki' strip marker to prevent list processing (also known as doBlockLevels()). |
465 | // However, leave the wrapping <div/> outside to prevent <p/>-wrapping. |
466 | $marker = $parser::MARKER_PREFIX . '-syntaxhighlightinner-' . |
467 | sprintf( '%08X', $parser->mMarkerIndex++ ) . $parser::MARKER_SUFFIX; |
468 | $parser->getStripState()->addNoWiki( $marker, $output ); |
469 | $output = $marker; |
470 | } |
471 | |
472 | $output = Html::openElement( 'div', $htmlAttribs ) . |
473 | $output . |
474 | Html::closeElement( 'div' ); |
475 | } |
476 | |
477 | $status->value = $output; |
478 | return $status; |
479 | } |
480 | |
481 | /** |
482 | * Construct a cache key for the results of a Pygments invocation. |
483 | * |
484 | * @param string $code Code to be highlighted. |
485 | * @param string $lexer Lexer name. |
486 | * @param array $options Options array. |
487 | * @return string Cache key. |
488 | */ |
489 | private static function makeCacheKeyHash( $code, $lexer, $options ) { |
490 | $optionString = FormatJson::encode( $options, false, FormatJson::ALL_OK ); |
491 | return md5( "{$code}|{$lexer}|{$optionString}|" . self::CACHE_VERSION ); |
492 | } |
493 | |
494 | /** |
495 | * Take an input specifying a list of lines to highlight, returning |
496 | * a raw list of matching line numbers. |
497 | * |
498 | * Input is comma-separated list of lines or line ranges. |
499 | * |
500 | * @param string $lineSpec |
501 | * @return int[] Line numbers. |
502 | */ |
503 | protected static function parseHighlightLines( $lineSpec ) { |
504 | $lines = []; |
505 | $values = array_map( 'trim', explode( ',', $lineSpec ) ); |
506 | foreach ( $values as $value ) { |
507 | if ( ctype_digit( $value ) ) { |
508 | $lines[] = (int)$value; |
509 | } elseif ( strpos( $value, '-' ) !== false ) { |
510 | [ $start, $end ] = array_map( 'intval', explode( '-', $value ) ); |
511 | if ( self::validHighlightRange( $start, $end ) ) { |
512 | for ( $i = $start; $i <= $end; $i++ ) { |
513 | $lines[] = $i; |
514 | } |
515 | } |
516 | } |
517 | if ( count( $lines ) > self::getMaxLines() ) { |
518 | $lines = array_slice( $lines, 0, self::getMaxLines() ); |
519 | break; |
520 | } |
521 | } |
522 | return $lines; |
523 | } |
524 | |
525 | /** |
526 | * Validate a provided input range |
527 | * @param int $start |
528 | * @param int $end |
529 | * @return bool |
530 | */ |
531 | protected static function validHighlightRange( $start, $end ) { |
532 | // Since we're taking this tiny range and producing a an |
533 | // array of every integer between them, it would be trivial |
534 | // to DoS the system by asking for a huge range. |
535 | // Impose an arbitrary limit on the number of lines in a |
536 | // given range to reduce the impact. |
537 | return $start > 0 && |
538 | $start < $end && |
539 | $end - $start < self::getMaxLines(); |
540 | } |
541 | |
542 | /** |
543 | * Hook into Content::getParserOutput to provide syntax highlighting for |
544 | * script content. |
545 | * |
546 | * @param Content $content |
547 | * @param Title $title |
548 | * @param int $revId |
549 | * @param ParserOptions $options |
550 | * @param bool $generateHtml |
551 | * @param ParserOutput &$parserOutput |
552 | * @return bool |
553 | * @since MW 1.21 |
554 | */ |
555 | public function onContentGetParserOutput( $content, $title, |
556 | $revId, $options, $generateHtml, &$parserOutput |
557 | ) { |
558 | global $wgTextModelsToParse; |
559 | |
560 | // Hope that the "SyntaxHighlightModels" attribute does not contain silly types. |
561 | if ( !( $content instanceof TextContent ) ) { |
562 | // Oops! Non-text content? Let MediaWiki handle this. |
563 | return true; |
564 | } |
565 | |
566 | if ( !$generateHtml ) { |
567 | // Nothing special for us to do, let MediaWiki handle this. |
568 | return true; |
569 | } |
570 | |
571 | // Determine the SyntaxHighlight language from the page's |
572 | // content model. Extensions can extend the default CSS/JS |
573 | // mapping by setting the SyntaxHighlightModels attribute. |
574 | $extension = ExtensionRegistry::getInstance(); |
575 | $models = $extension->getAttribute( 'SyntaxHighlightModels' ) + [ |
576 | CONTENT_MODEL_CSS => 'css', |
577 | CONTENT_MODEL_JAVASCRIPT => 'javascript', |
578 | ]; |
579 | $model = $content->getModel(); |
580 | if ( !isset( $models[$model] ) ) { |
581 | // We don't care about this model, carry on. |
582 | return true; |
583 | } |
584 | $lexer = $models[$model]; |
585 | $text = $content->getText(); |
586 | |
587 | // Parse using the standard parser to get links etc. into the database, HTML is replaced below. |
588 | // We could do this using $content->fillParserOutput(), but alas it is 'protected'. |
589 | if ( in_array( $model, $wgTextModelsToParse, true ) ) { |
590 | $parserOutput = MediaWikiServices::getInstance()->getParser() |
591 | ->parse( $text, $title, $options, true, true, $revId ); |
592 | } |
593 | |
594 | $status = self::highlight( $text, $lexer, [ 'line' => true, 'linelinks' => 'L' ] ); |
595 | if ( !$status->isOK() ) { |
596 | return true; |
597 | } |
598 | $out = $status->getValue(); |
599 | |
600 | $parserOutput->addModuleStyles( self::getModuleStyles() ); |
601 | $parserOutput->addModules( [ 'ext.pygments.linenumbers' ] ); |
602 | $parserOutput->setText( $out ); |
603 | |
604 | // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. |
605 | return false; |
606 | } |
607 | |
608 | /** |
609 | * Hook to provide syntax highlighting for API pretty-printed output |
610 | * |
611 | * @param IContextSource $context |
612 | * @param string $text |
613 | * @param string $mime |
614 | * @param string $format |
615 | * @since MW 1.24 |
616 | * @return bool |
617 | */ |
618 | public function onApiFormatHighlight( $context, $text, $mime, $format ) { |
619 | if ( !isset( self::$mimeLexers[$mime] ) ) { |
620 | return true; |
621 | } |
622 | |
623 | $lexer = self::$mimeLexers[$mime]; |
624 | $status = self::highlight( $text, $lexer ); |
625 | if ( !$status->isOK() ) { |
626 | return true; |
627 | } |
628 | |
629 | $out = $status->getValue(); |
630 | if ( preg_match( '/^<pre([^>]*)>/i', $out, $m ) ) { |
631 | $attrs = Sanitizer::decodeTagAttributes( $m[1] ); |
632 | $attrs['class'] .= ' api-pretty-content'; |
633 | $encodedAttrs = Sanitizer::safeEncodeTagAttributes( $attrs ); |
634 | $out = '<pre' . $encodedAttrs . '>' . substr( $out, strlen( $m[0] ) ); |
635 | } |
636 | $output = $context->getOutput(); |
637 | $output->addModuleStyles( self::getModuleStyles() ); |
638 | $output->addHTML( '<div dir="ltr">' . $out . '</div>' ); |
639 | |
640 | // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. |
641 | return false; |
642 | } |
643 | |
644 | /** |
645 | * Hook to add Pygments version to Special:Version |
646 | * |
647 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/SoftwareInfo |
648 | * @param array &$software |
649 | */ |
650 | public function onSoftwareInfo( &$software ) { |
651 | try { |
652 | $software['[https://pygments.org/ Pygments]'] = Pygmentize::getVersion(); |
653 | } catch ( PygmentsException $e ) { |
654 | // pass |
655 | } |
656 | } |
657 | } |