mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/SyntaxHighlight_GeSHi
synced 2024-11-13 17:50:37 +00:00
863391d7b8
This path is needed for URL expansion in debug mode. Bug: T378878 Change-Id: Ic7b067df2c03286575d0a390d90d6d2d4b3169e5
693 lines
21 KiB
PHP
693 lines
21 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*/
|
|
|
|
namespace MediaWiki\SyntaxHighlight;
|
|
|
|
use MediaWiki\Api\Hook\ApiFormatHighlightHook;
|
|
use MediaWiki\Content\Content;
|
|
use MediaWiki\Content\Hook\ContentGetParserOutputHook;
|
|
use MediaWiki\Content\TextContent;
|
|
use MediaWiki\Context\IContextSource;
|
|
use MediaWiki\Hook\ParserFirstCallInitHook;
|
|
use MediaWiki\Hook\SoftwareInfoHook;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\Json\FormatJson;
|
|
use MediaWiki\MainConfigNames;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Parser\Parser;
|
|
use MediaWiki\Parser\ParserOptions;
|
|
use MediaWiki\Parser\ParserOutput;
|
|
use MediaWiki\Parser\Sanitizer;
|
|
use MediaWiki\Registration\ExtensionRegistry;
|
|
use MediaWiki\ResourceLoader\Hook\ResourceLoaderRegisterModulesHook;
|
|
use MediaWiki\ResourceLoader\ResourceLoader;
|
|
use MediaWiki\Status\Status;
|
|
use MediaWiki\Title\Title;
|
|
use RuntimeException;
|
|
use Wikimedia\ObjectCache\WANObjectCache;
|
|
use Wikimedia\Parsoid\Core\ContentMetadataCollectorStringSets as CMCSS;
|
|
use Wikimedia\Parsoid\DOM\DocumentFragment;
|
|
use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
|
|
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
|
|
|
|
class SyntaxHighlight extends ExtensionTagHandler implements
|
|
ParserFirstCallInitHook,
|
|
ContentGetParserOutputHook,
|
|
ResourceLoaderRegisterModulesHook,
|
|
ApiFormatHighlightHook,
|
|
SoftwareInfoHook
|
|
{
|
|
|
|
/** @var string CSS class for syntax-highlighted code. Public as used by the updateCSS maintenance script. */
|
|
public const HIGHLIGHT_CSS_CLASS = 'mw-highlight';
|
|
|
|
/** @var int Cache version. Increment whenever the HTML changes. */
|
|
private const CACHE_VERSION = 2;
|
|
|
|
/** @var array<string,string> Mapping of MIME-types to lexer names. */
|
|
private static $mimeLexers = [
|
|
'text/javascript' => 'javascript',
|
|
'application/json' => 'javascript',
|
|
'text/xml' => 'xml',
|
|
];
|
|
|
|
/**
|
|
* Returns the maximum number of lines that may be selected for highlighting
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function getMaxLines(): int {
|
|
$config = MediaWikiServices::getInstance()->getMainConfig();
|
|
return $config->get( 'SyntaxHighlightMaxLines' );
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum input size for the highlighter
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function getMaxBytes(): int {
|
|
$config = MediaWikiServices::getInstance()->getMainConfig();
|
|
return $config->get( 'SyntaxHighlightMaxBytes' );
|
|
}
|
|
|
|
/**
|
|
* Get the Pygments lexer name for a particular language.
|
|
*
|
|
* @param string|null $lang Language name.
|
|
* @return string|null Lexer name, or null if no matching lexer.
|
|
*/
|
|
private static function getLexer( $lang ) {
|
|
static $lexers = null;
|
|
|
|
if ( $lang === null ) {
|
|
return null;
|
|
}
|
|
|
|
$lexers ??= Pygmentize::getLexers();
|
|
|
|
$lexer = strtolower( $lang );
|
|
|
|
if ( isset( $lexers[$lexer] ) ) {
|
|
return $lexer;
|
|
}
|
|
|
|
$geshi2pygments = SyntaxHighlightGeSHiCompat::getGeSHiToPygmentsMap();
|
|
|
|
// Check if this is a GeSHi lexer name for which there exists
|
|
// a compatible Pygments lexer with a different name.
|
|
if ( isset( $geshi2pygments[$lexer] ) ) {
|
|
$lexer = $geshi2pygments[$lexer];
|
|
if ( isset( $lexers[$lexer] ) ) {
|
|
return $lexer;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Register parser hook
|
|
*
|
|
* @param Parser $parser
|
|
*/
|
|
public function onParserFirstCallInit( $parser ) {
|
|
$parser->setHook( 'source', [ self::class, 'parserHookSource' ] );
|
|
$parser->setHook( 'syntaxhighlight', [ self::class, 'parserHook' ] );
|
|
}
|
|
|
|
/**
|
|
* Parser hook for <source> to add deprecated tracking category
|
|
*
|
|
* @param ?string $text
|
|
* @param array $args
|
|
* @param Parser $parser
|
|
* @return string
|
|
*/
|
|
public static function parserHookSource( $text, $args, $parser ) {
|
|
$parser->addTrackingCategory( 'syntaxhighlight-source-category' );
|
|
return self::parserHook( $text, $args, $parser );
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
private static function getModuleStyles(): array {
|
|
return [ 'ext.pygments' ];
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param string $text
|
|
* @param array $args
|
|
* @param ?Parser $parser
|
|
* @return array
|
|
*/
|
|
private static function processContent( string $text, array $args, ?Parser $parser = null ): array {
|
|
// Don't trim leading spaces away, just the linefeeds
|
|
$out = rtrim( trim( $text, "\n" ) );
|
|
$trackingCats = [];
|
|
|
|
// Convert deprecated attributes
|
|
if ( isset( $args['enclose'] ) ) {
|
|
if ( $args['enclose'] === 'none' ) {
|
|
$args['inline'] = true;
|
|
}
|
|
unset( $args['enclose'] );
|
|
$trackingCats[] = 'syntaxhighlight-enclose-category';
|
|
}
|
|
|
|
$lexer = $args['lang'] ?? '';
|
|
|
|
$result = self::highlight( $out, $lexer, $args, $parser );
|
|
if ( !$result->isGood() ) {
|
|
$trackingCats[] = 'syntaxhighlight-error-category';
|
|
}
|
|
|
|
return [ 'html' => $result->getValue(), 'cats' => $trackingCats ];
|
|
}
|
|
|
|
/**
|
|
* Parser hook for both <source> and <syntaxhighlight> logic
|
|
*
|
|
* @param ?string $text
|
|
* @param array $args
|
|
* @param Parser $parser
|
|
* @return string
|
|
*/
|
|
public static function parserHook( $text, $args, $parser ) {
|
|
// Replace strip markers (For e.g. {{#tag:syntaxhighlight|<nowiki>...}})
|
|
$out = $parser->getStripState()->unstripNoWiki( $text ?? '' );
|
|
|
|
$result = self::processContent( $out, $args, $parser );
|
|
foreach ( $result['cats'] as $cat ) {
|
|
$parser->addTrackingCategory( $cat );
|
|
}
|
|
|
|
// Register modules
|
|
$parser->getOutput()->addModuleStyles( self::getModuleStyles() );
|
|
$parser->getOutput()->addModules( [ 'ext.pygments.view' ] );
|
|
return $result['html'];
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
public function sourceToDom(
|
|
ParsoidExtensionAPI $extApi, string $text, array $extArgs
|
|
): ?DocumentFragment {
|
|
$result = self::processContent( $text, $extApi->extArgsToArray( $extArgs ) );
|
|
|
|
// FIXME: There is no API method in Parsoid to add tracking categories
|
|
// So, $result['cats'] is being ignored
|
|
|
|
// Register modules
|
|
$extApi->getMetadata()->appendOutputStrings( CMCSS::MODULE_STYLE, self::getModuleStyles() );
|
|
$extApi->getMetadata()->appendOutputStrings( CMCSS::MODULE, [ 'ext.pygments.view' ] );
|
|
|
|
return $extApi->htmlToDom( $result['html'] );
|
|
}
|
|
|
|
/**
|
|
* Unwrap the <div> wrapper of the Pygments output
|
|
*
|
|
* @param string $out Output
|
|
* @return string Unwrapped output
|
|
*/
|
|
private static function unwrap( string $out ): string {
|
|
if ( $out !== '' ) {
|
|
$m = [];
|
|
if ( preg_match( '/^<div class="?mw-highlight"?>(.*)<\/div>$/s', trim( $out ), $m ) ) {
|
|
$out = trim( $m[1] );
|
|
} else {
|
|
throw new RuntimeException( 'Unexpected output from Pygments encountered' );
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param string $code
|
|
* @param bool $isInline
|
|
* @return string HTML
|
|
*/
|
|
private static function plainCodeWrap( $code, $isInline ) {
|
|
if ( $isInline ) {
|
|
return htmlspecialchars( $code, ENT_NOQUOTES );
|
|
}
|
|
|
|
return Html::rawElement(
|
|
'div',
|
|
[ 'class' => self::HIGHLIGHT_CSS_CLASS ],
|
|
Html::element( 'pre', [], $code )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string $code
|
|
* @param string|null $lang
|
|
* @param array $args
|
|
* @param Parser|null $parser Parser, if generating content to be parsed.
|
|
* @return Status
|
|
*/
|
|
private static function highlightInner( $code, $lang = null, $args = [], ?Parser $parser = null ) {
|
|
$status = new Status;
|
|
|
|
$lexer = self::getLexer( $lang );
|
|
if ( $lexer === null && $lang !== null ) {
|
|
$status->warning( 'syntaxhighlight-error-unknown-language', $lang );
|
|
}
|
|
|
|
// For empty tag, output nothing instead of empty <pre>.
|
|
if ( $code === '' ) {
|
|
$status->value = '';
|
|
return $status;
|
|
}
|
|
|
|
$length = strlen( $code );
|
|
if ( strlen( $code ) > self::getMaxBytes() ) {
|
|
// Disable syntax highlighting
|
|
$lexer = null;
|
|
$status->warning(
|
|
'syntaxhighlight-error-exceeds-size-limit',
|
|
$length,
|
|
self::getMaxBytes()
|
|
);
|
|
}
|
|
|
|
$isInline = isset( $args['inline'] );
|
|
$showLines = isset( $args['line'] );
|
|
|
|
if ( $isInline ) {
|
|
$code = trim( $code );
|
|
}
|
|
|
|
if ( $lexer === null ) {
|
|
// When syntax highlighting is disabled..
|
|
$status->value = self::plainCodeWrap( $code, $isInline );
|
|
return $status;
|
|
}
|
|
|
|
if ( $parser && !$parser->incrementExpensiveFunctionCount() ) {
|
|
// Highlighting is expensive, return unstyled
|
|
$status->value = self::plainCodeWrap( $code, $isInline );
|
|
return $status;
|
|
}
|
|
|
|
$options = [
|
|
'cssclass' => self::HIGHLIGHT_CSS_CLASS,
|
|
'encoding' => 'utf-8',
|
|
];
|
|
|
|
// Line numbers
|
|
if ( $showLines ) {
|
|
$options['linenos'] = 'inline';
|
|
}
|
|
|
|
if ( $lexer === 'php' && strpos( $code, '<?php' ) === false ) {
|
|
$options['startinline'] = 1;
|
|
}
|
|
|
|
// Highlight specific lines
|
|
if ( isset( $args['highlight'] ) ) {
|
|
$lines = self::parseHighlightLines( $args['highlight'] );
|
|
if ( count( $lines ) ) {
|
|
$options['hl_lines'] = implode( ' ', $lines );
|
|
}
|
|
}
|
|
|
|
// Starting line number
|
|
if ( isset( $args['start'] ) && ctype_digit( $args['start'] ) ) {
|
|
$options['linenostart'] = (int)$args['start'];
|
|
}
|
|
|
|
if ( !empty( $args['linelinks'] ) ) {
|
|
$options['linespans'] = $args['linelinks'];
|
|
}
|
|
|
|
if ( $isInline ) {
|
|
$options['nowrap'] = 1;
|
|
}
|
|
|
|
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
|
|
$error = null;
|
|
$output = $cache->getWithSetCallback(
|
|
$cache->makeGlobalKey( 'highlight', self::makeCacheKeyHash( $code, $lexer, $options ) ),
|
|
$cache::TTL_MONTH,
|
|
static function ( $oldValue, &$ttl ) use ( $code, $lexer, $options, &$error ) {
|
|
try {
|
|
return Pygmentize::highlight( $lexer, $code, $options );
|
|
} catch ( PygmentsException $e ) {
|
|
$ttl = WANObjectCache::TTL_UNCACHEABLE;
|
|
$error = $e->getMessage();
|
|
return null;
|
|
}
|
|
}
|
|
);
|
|
|
|
if ( $error !== null || $output === null ) {
|
|
$status->warning( 'syntaxhighlight-error-pygments-invocation-failure' );
|
|
if ( $error !== null ) {
|
|
wfWarn( 'Failed to invoke Pygments: ' . $error );
|
|
} else {
|
|
wfWarn( 'Invoking Pygments returned blank output with no error response' );
|
|
}
|
|
|
|
// Fall back to preformatted code without syntax highlighting
|
|
$output = self::plainCodeWrap( $code, $isInline );
|
|
}
|
|
|
|
$status->value = $output;
|
|
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Highlight a code-block using a particular lexer.
|
|
*
|
|
* This produces raw HTML (wrapped by Status), the caller is responsible
|
|
* for making sure the "ext.pygments" module is loaded in the output.
|
|
*
|
|
* @param string $code Code to highlight.
|
|
* @param string|null $lang Language name, or null to use plain markup.
|
|
* @param array $args Associative array of additional arguments.
|
|
* If it contains a 'line' key, the output will include line numbers.
|
|
* If it includes a 'highlight' key, the value will be parsed as a
|
|
* comma-separated list of lines and line-ranges to highlight.
|
|
* If it contains a 'start' key, the value will be used as the line at which to
|
|
* start highlighting.
|
|
* If it contains a 'inline' key, the output will not be wrapped in `<div><pre/></div>`.
|
|
* If it contains a 'linelinks' key, lines will have links and anchors with a prefix
|
|
* of the value. Similar to the lineanchors+linespans features in Pygments.
|
|
* If it contains a 'copy' key, a link will be shown for copying content to the clipboard.
|
|
* @param Parser|null $parser Parser, if generating content to be parsed.
|
|
* @return Status Status object, with HTML representing the highlighted
|
|
* code as its value.
|
|
*/
|
|
public static function highlight( $code, $lang = null, $args = [], ?Parser $parser = null ) {
|
|
$status = self::highlightInner( $code, $lang, $args, $parser );
|
|
$output = $status->getValue();
|
|
|
|
$isInline = isset( $args['inline'] );
|
|
$showLines = isset( $args['line'] );
|
|
$lexer = self::getLexer( $lang );
|
|
|
|
// Post-Pygment HTML transformations.
|
|
|
|
if ( $showLines ) {
|
|
$lineReplace = Html::element( 'span', [ 'class' => 'linenos', 'data-line' => '$1' ] );
|
|
if ( !empty( $args['linelinks'] ) ) {
|
|
$lineReplace = Html::rawElement(
|
|
'a',
|
|
[ 'href' => '#' . $args['linelinks'] . '-$1' ],
|
|
$lineReplace
|
|
);
|
|
}
|
|
// Convert line numbers to data attributes so they
|
|
// can be displayed as CSS generated content and be
|
|
// unselectable in all browsers.
|
|
$output = preg_replace(
|
|
'`<span class="linenos">\s*([^<]*)\s*</span>`',
|
|
$lineReplace,
|
|
$output
|
|
);
|
|
}
|
|
|
|
// Allow certain HTML attributes
|
|
$htmlAttribs = Sanitizer::validateAttributes(
|
|
$args, array_flip( [ 'style', 'class', 'id' ] )
|
|
);
|
|
|
|
$dir = ( isset( $args['dir'] ) && $args['dir'] === 'rtl' ) ? 'rtl' : 'ltr';
|
|
|
|
// Build class list
|
|
$classList = [];
|
|
if ( isset( $htmlAttribs['class'] ) ) {
|
|
$classList[] = $htmlAttribs['class'];
|
|
}
|
|
$classList[] = self::HIGHLIGHT_CSS_CLASS;
|
|
if ( $lexer !== null ) {
|
|
$classList[] = self::HIGHLIGHT_CSS_CLASS . '-lang-' . $lexer;
|
|
}
|
|
$classList[] = 'mw-content-' . $dir;
|
|
if ( $showLines ) {
|
|
$classList[] = self::HIGHLIGHT_CSS_CLASS . '-lines';
|
|
}
|
|
if ( !$isInline && isset( $args['copy'] ) ) {
|
|
$classList[] = 'mw-highlight-copy';
|
|
}
|
|
$htmlAttribs['class'] = implode( ' ', $classList );
|
|
$htmlAttribs['dir'] = $dir;
|
|
'@phan-var array{class:string,dir:string} $htmlAttribs';
|
|
|
|
if ( $isInline ) {
|
|
// We've already trimmed the input $code before highlighting,
|
|
// but pygment's standard out adds a line break afterwards,
|
|
// which would then be preserved in the paragraph that wraps this,
|
|
// and become visible as a space. Avoid that.
|
|
$output = trim( $output );
|
|
|
|
// Enforce inlineness. Stray newlines may result in unexpected list and paragraph processing
|
|
// (also known as doBlockLevels()).
|
|
$output = str_replace( "\n", ' ', $output );
|
|
$output = Html::rawElement( 'code', $htmlAttribs, $output );
|
|
} else {
|
|
$output = self::unwrap( $output );
|
|
|
|
if ( $parser ) {
|
|
// Use 'nowiki' strip marker to prevent list processing (also known as doBlockLevels()).
|
|
// However, leave the wrapping <div/> outside to prevent <p/>-wrapping.
|
|
$marker = $parser::MARKER_PREFIX . '-syntaxhighlightinner-' .
|
|
sprintf( '%08X', $parser->mMarkerIndex++ ) . $parser::MARKER_SUFFIX;
|
|
$parser->getStripState()->addNoWiki( $marker, $output );
|
|
$output = $marker;
|
|
}
|
|
|
|
$output = Html::openElement( 'div', $htmlAttribs ) .
|
|
$output .
|
|
Html::closeElement( 'div' );
|
|
}
|
|
|
|
$status->value = $output;
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* Construct a cache key for the results of a Pygments invocation.
|
|
*
|
|
* @param string $code Code to be highlighted.
|
|
* @param string $lexer Lexer name.
|
|
* @param array $options Options array.
|
|
* @return string Cache key.
|
|
*/
|
|
private static function makeCacheKeyHash( $code, $lexer, $options ) {
|
|
$optionString = FormatJson::encode( $options, false, FormatJson::ALL_OK );
|
|
return md5( "{$code}|{$lexer}|{$optionString}|" . self::CACHE_VERSION );
|
|
}
|
|
|
|
/**
|
|
* Take an input specifying a list of lines to highlight, returning
|
|
* a raw list of matching line numbers.
|
|
*
|
|
* Input is comma-separated list of lines or line ranges.
|
|
*
|
|
* @param string $lineSpec
|
|
* @return int[] Line numbers.
|
|
*/
|
|
protected static function parseHighlightLines( $lineSpec ) {
|
|
$lines = [];
|
|
$values = array_map( 'trim', explode( ',', $lineSpec ) );
|
|
foreach ( $values as $value ) {
|
|
if ( ctype_digit( $value ) ) {
|
|
$lines[] = (int)$value;
|
|
} elseif ( strpos( $value, '-' ) !== false ) {
|
|
[ $start, $end ] = array_map( 'intval', explode( '-', $value ) );
|
|
if ( self::validHighlightRange( $start, $end ) ) {
|
|
for ( $i = $start; $i <= $end; $i++ ) {
|
|
$lines[] = $i;
|
|
}
|
|
}
|
|
}
|
|
if ( count( $lines ) > self::getMaxLines() ) {
|
|
$lines = array_slice( $lines, 0, self::getMaxLines() );
|
|
break;
|
|
}
|
|
}
|
|
return $lines;
|
|
}
|
|
|
|
/**
|
|
* Validate a provided input range
|
|
* @param int $start
|
|
* @param int $end
|
|
* @return bool
|
|
*/
|
|
protected static function validHighlightRange( $start, $end ) {
|
|
// Since we're taking this tiny range and producing a an
|
|
// array of every integer between them, it would be trivial
|
|
// to DoS the system by asking for a huge range.
|
|
// Impose an arbitrary limit on the number of lines in a
|
|
// given range to reduce the impact.
|
|
return $start > 0 &&
|
|
$start < $end &&
|
|
$end - $start < self::getMaxLines();
|
|
}
|
|
|
|
/**
|
|
* Hook into Content::getParserOutput to provide syntax highlighting for
|
|
* script content.
|
|
*
|
|
* @param Content $content
|
|
* @param Title $title
|
|
* @param int $revId
|
|
* @param ParserOptions $options
|
|
* @param bool $generateHtml
|
|
* @param ParserOutput &$parserOutput
|
|
* @return bool
|
|
* @since MW 1.21
|
|
*/
|
|
public function onContentGetParserOutput( $content, $title,
|
|
$revId, $options, $generateHtml, &$parserOutput
|
|
) {
|
|
// Hope that the "SyntaxHighlightModels" attribute does not contain silly types.
|
|
if ( !( $content instanceof TextContent ) ) {
|
|
// Oops! Non-text content? Let MediaWiki handle this.
|
|
return true;
|
|
}
|
|
|
|
if ( !$generateHtml ) {
|
|
// Nothing special for us to do, let MediaWiki handle this.
|
|
return true;
|
|
}
|
|
|
|
// Determine the SyntaxHighlight language from the page's
|
|
// content model. Extensions can extend the default CSS/JS
|
|
// mapping by setting the SyntaxHighlightModels attribute.
|
|
$extension = ExtensionRegistry::getInstance();
|
|
$models = $extension->getAttribute( 'SyntaxHighlightModels' ) + [
|
|
CONTENT_MODEL_CSS => 'css',
|
|
CONTENT_MODEL_JAVASCRIPT => 'javascript',
|
|
];
|
|
$model = $content->getModel();
|
|
if ( !isset( $models[$model] ) ) {
|
|
// We don't care about this model, carry on.
|
|
return true;
|
|
}
|
|
$lexer = $models[$model];
|
|
$text = $content->getText();
|
|
|
|
$config = MediaWikiServices::getInstance()->getMainConfig();
|
|
// Parse using the standard parser to get links etc. into the database, HTML is replaced below.
|
|
// We could do this using $content->fillParserOutput(), but alas it is 'protected'.
|
|
if ( in_array( $model, $config->get( MainConfigNames::TextModelsToParse ), true ) ) {
|
|
$parserOutput = MediaWikiServices::getInstance()->getParser()
|
|
->parse( $text, $title, $options, true, true, $revId );
|
|
}
|
|
|
|
$status = self::highlight( $text, $lexer, [ 'line' => true, 'linelinks' => 'L' ] );
|
|
if ( !$status->isOK() ) {
|
|
return true;
|
|
}
|
|
$out = $status->getValue();
|
|
|
|
$parserOutput->addModuleStyles( self::getModuleStyles() );
|
|
$parserOutput->addModules( [ 'ext.pygments.view' ] );
|
|
$parserOutput->setText( $out );
|
|
|
|
// Inform MediaWiki that we have parsed this page and it shouldn't mess with it.
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Hook to provide syntax highlighting for API pretty-printed output
|
|
*
|
|
* @param IContextSource $context
|
|
* @param string $text
|
|
* @param string $mime
|
|
* @param string $format
|
|
* @since MW 1.24
|
|
* @return bool
|
|
*/
|
|
public function onApiFormatHighlight( $context, $text, $mime, $format ) {
|
|
if ( !isset( self::$mimeLexers[$mime] ) ) {
|
|
return true;
|
|
}
|
|
|
|
$lexer = self::$mimeLexers[$mime];
|
|
$status = self::highlight( $text, $lexer );
|
|
if ( !$status->isOK() ) {
|
|
return true;
|
|
}
|
|
|
|
$out = $status->getValue();
|
|
if ( preg_match( '/^<pre([^>]*)>/i', $out, $m ) ) {
|
|
$attrs = Sanitizer::decodeTagAttributes( $m[1] );
|
|
$attrs['class'] .= ' api-pretty-content';
|
|
$encodedAttrs = Sanitizer::safeEncodeTagAttributes( $attrs );
|
|
$out = '<pre' . $encodedAttrs . '>' . substr( $out, strlen( $m[0] ) );
|
|
}
|
|
$output = $context->getOutput();
|
|
$output->addModuleStyles( self::getModuleStyles() );
|
|
$output->addHTML( '<div dir="ltr">' . $out . '</div>' );
|
|
|
|
// Inform MediaWiki that we have parsed this page and it shouldn't mess with it.
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Hook to add Pygments version to Special:Version
|
|
*
|
|
* @see https://www.mediawiki.org/wiki/Manual:Hooks/SoftwareInfo
|
|
* @param array &$software
|
|
*/
|
|
public function onSoftwareInfo( &$software ) {
|
|
try {
|
|
$software['[https://pygments.org/ Pygments]'] = Pygmentize::getVersion();
|
|
} catch ( PygmentsException $e ) {
|
|
// pass
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hook to register ext.pygments.view module.
|
|
* @param ResourceLoader $rl
|
|
*/
|
|
public function onResourceLoaderRegisterModules( ResourceLoader $rl ): void {
|
|
$rl->register( 'ext.pygments.view', [
|
|
'localBasePath' => MW_INSTALL_PATH . '/extensions/SyntaxHighlight_GeSHi/modules',
|
|
'remoteExtPath' => 'SyntaxHighlight_GeSHi/modules',
|
|
'scripts' => array_merge( [
|
|
'pygments.linenumbers.js',
|
|
'pygments.links.js',
|
|
'pygments.copy.js'
|
|
], ExtensionRegistry::getInstance()->isLoaded( 'Scribunto' ) ? [
|
|
'pygments.links.scribunto.js'
|
|
] : [] ),
|
|
'styles' => [
|
|
'pygments.copy.less'
|
|
],
|
|
'messages' => [
|
|
'syntaxhighlight-button-copy',
|
|
'syntaxhighlight-button-copied'
|
|
],
|
|
'dependencies' => [
|
|
'mediawiki.util',
|
|
'mediawiki.Title'
|
|
]
|
|
] );
|
|
}
|
|
}
|