diff --git a/.gitignore b/.gitignore index 93a2c77..d4366c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .svn *~ *.kate-swp .*.swp .idea node_modules +/vendor diff --git a/README b/README index b6af55c..738a2ce 100644 --- a/README +++ b/README @@ -1,58 +1,74 @@ This extension allows source code to be syntax highlighted on the wiki pages. This README file might be out of date, have a look at the extension page for updated informations: - http://www.mediawiki.org/wiki/Extension:SyntaxHighlight_GeSHi + https://www.mediawiki.org/wiki/Extension:SyntaxHighlight_GeSHi == Requirements == -This version of the extension has been tested with GeSHi 1.0.8.11 and MediaWiki 1.24 -as of 2014-05-10. It may or may not work with earlier versions of the aforementioned -software. To get releases of this extension compatible with earlier versions of -MediaWiki, visit: +This version of the extension has been tested with Pygments 1.6, 2.0.2 and +MediaWiki 1.25 as of 2015-06-19. To get releases of this extension compatible +with earlier versions of MediaWiki, visit: - http://www.mediawiki.org/wiki/Special:ExtensionDistributor/SyntaxHighlight_GeSHi + https://www.mediawiki.org/wiki/Special:ExtensionDistributor/SyntaxHighlight_GeSHi == Installation == -Add this line to your LocalSettings.php: +First, you will need to ensure that this extension's Composer-managed +dependencies are available. In your root MediaWiki directory, create a +composer.local.json file with the following contents: - require_once("extensions/SyntaxHighlight_GeSHi/SyntaxHighlight_GeSHi.php"); + { + "extra": { + "merge-plugin": { + "include": [ + "extensions/SyntaxHighlight_GeSHi/composer.json" + ] + } + } + } + +Then run 'composer update'. + +Next, Add this line to your LocalSettings.php: + + wfLoadExtension( 'SyntaxHighlight_GeSHi' ); + +You will also need to install Pygments, the syntax highlighting library that +this extension uses. On Ubuntu, Debian, RHEL, CentOS, and Fedora, you can use +your package manager to install the 'python-pygments' package. On other +systems, you can (usually) 'pip install pygments'. For additional help in +obtaining and installing Pygments, please see: + + http://pygments.org/download/ == Usage == On the wiki page, you can now use "source" elements: html text == Parameters == -Please see the documentation of GeSHi on http://qbnz.com/highlighter/geshi-doc.html -for detailed information to use some of the parameters. +For details information of these parameters, see the documentation of Pygments' +HtmlFormatter at . -* lang; Defines the language -* line; Corresponds to enable_line_numbers method on GeSHi -* start; Corresponds to start_line_numbers_at method on GeSHi -* strict; Corresponds to enable_strict_mode method on GeSHi +* lang; Defines the language. +* line; Corresponds to linenos="inline" option. +* start; Corresponds to linenostart opion. +* highlight; Corresponds to hl_lines option (comma separated). == Note == -GeSHi is generous about creating HTML elements: highlighting large blocks of -code can easily generate enough of them to crash a browser. As a guard, symbol -highlighting is turned off for code fragments larger than 100 kB. For fragments -larger than 200 kB, string highlighting is turned off as well. - -== Note to maintainers == - -Whenever updating the version of GeSHi embedded in the extension, run -maintenance/updateLanguageList.php to re-generate the list of supported -languages. +Pygments is generous about creating HTML elements: highlighting large blocks of +code can easily generate enough of them to crash a browser. As a guard, syntax +highlighting is turned off for code fragments larger than 100 kB. diff --git a/ResourceLoaderGeSHiLocalModule.php b/ResourceLoaderGeSHiLocalModule.php deleted file mode 100644 index ce62224..0000000 --- a/ResourceLoaderGeSHiLocalModule.php +++ /dev/null @@ -1,37 +0,0 @@ - array( 'type' => 'style' ), - ); - } -} diff --git a/ResourceLoaderGeSHiModule.php b/ResourceLoaderGeSHiModule.php deleted file mode 100644 index 97b181c..0000000 --- a/ResourceLoaderGeSHiModule.php +++ /dev/null @@ -1,95 +0,0 @@ - $option ) { - switch ( $member ) { - case 'position': - $this->isPositionDefined = true; - // Don't break, we want to set the property as well - case 'lang': - $this->{$member} = (string)$option; - break; - } - } - } - - /** - * @param ResourceLoaderContext $context - * @return array - */ - public function getStyles( ResourceLoaderContext $context ) { - $geshi = SyntaxHighlight_GeSHi::prepare( '', $this->lang ); - if ( !$geshi->error ) { - $css = SyntaxHighlight_GeSHi::getCSS( $geshi ); - } else { - $css = ResourceLoader::makeComment( $geshi->error() ); - } - - return array( 'all' => $css ); - } - - /** - * @param ResourceLoaderContext $context - * @return int - */ - public function getModifiedTime( ResourceLoaderContext $context ) { - static $selfmtime = null; - if ( $selfmtime === null ) { - // Cache this since there are 100s of instances of this module - // See also T93025, T85794. - $selfmtime = self::safeFilemtime( __FILE__ ); - } - - return max( array( - $selfmtime, - self::safeFilemtime( GESHI_LANG_ROOT . "/{$this->lang}.php" ), - ) ); - } - - /** - * @param $context ResourceLoaderContext - * @return array - */ - public function getDefinitionSummary( ResourceLoaderContext $context ) { - $summary = parent::getDefinitionSummary( $context ); - $summary[] = array( - 'lang' => $this->lang, - 'geshi' => GESHI_VERSION, - ); - return $summary; - } - - public function getPosition() { - return $this->position; - } -} diff --git a/SyntaxHighlight_GeSHi.class.php b/SyntaxHighlight_GeSHi.class.php index fcaa4a2..fc6a46c 100644 --- a/SyntaxHighlight_GeSHi.class.php +++ b/SyntaxHighlight_GeSHi.class.php @@ -1,563 +1,334 @@ 'javascript', + 'application/json' => 'javascript', + 'text/xml' => 'xml', + ); /** - * Executed after processing extension.json - * @todo we should figure out how to make this a real config setting. + * Get the Pygments lexer name for a particular language. + * + * @param string $lang Language name. + * @return string|null Lexer name, or null if no matching lexer. */ - public static function registerExtension() { - global $wgVersion; - if ( version_compare( $wgVersion, '1.25', '<' ) ) { - die( 'This version of SyntaxHighlight GeSHi requires MediaWiki 1.25' ); + private static function getLexer( $lang ) { + static $lexers = null; + + if ( !$lexers ) { + $lexers = require __DIR__ . '/SyntaxHighlight_GeSHi.lexers.php'; } - global $wgGeSHiSupportedLanguages; - if ( !$wgGeSHiSupportedLanguages ) { - // If not set already, load it (@see ExtensionRegistry::exportExtractedData) - require_once __DIR__ . '/SyntaxHighlight_GeSHi.langs.php'; + $lexer = strtolower( $lang ); + + if ( isset( GeSHi::$compatibleLexers[$lexer] ) ) { + $lexer = GeSHi::$compatibleLexers[$lexer]; + } + + if ( in_array( $lexer, $lexers ) ) { + return $lexer; } - // @fixme we shouldn't be loading this on every request - require_once __DIR__ . '/geshi/geshi.php'; + + return null; } /** * Register parser hook * * @param $parser Parser * @return bool */ - public static function configureParser( &$parser ) { - $parser->setHook( 'source', array( 'SyntaxHighlight_GeSHi', 'parserHook' ) ); - $parser->setHook( 'syntaxhighlight', array( 'SyntaxHighlight_GeSHi', 'parserHook' ) ); - return true; + public static function onParserFirstCallInit( Parser &$parser ) { + foreach ( array( 'source', 'syntaxhighlight' ) as $tag ) { + $parser->setHook( $tag, array( 'SyntaxHighlight_GeSHi', 'parserHook' ) ); + } } /** * Parser hook * * @param string $text * @param array $args * @param Parser $parser * @return string */ public static function parserHook( $text, $args = array(), $parser ) { - global $wgSyntaxHighlightDefaultLang, $wgUseTidy; - self::initialise(); - $text = rtrim( $text ); + global $wgUseTidy; + // Don't trim leading spaces away, just the linefeeds - $text = preg_replace( '/^\n+/', '', $text ); + $out = preg_replace( '/^\n+/', '', rtrim( $text ) ); // Validate language - if ( isset( $args['lang'] ) && $args['lang'] ) { - $lang = $args['lang']; + if ( isset( $args['lang'] ) ) { + $lexer = self::getLexer( $args['lang'] ); } else { - // language is not specified. Check if default exists, if yes, use it. - if ( !is_null( $wgSyntaxHighlightDefaultLang ) ) { - $lang = $wgSyntaxHighlightDefaultLang; - } else { - $error = self::formatLanguageError( $text ); - return $error; - } + $lexer = null; + } + + $out = self::highlight( $out, $lexer, $args ); + + // HTML Tidy will convert tabs to spaces incorrectly (bug 30930). + // But the conversion from tab to space occurs while reading the input, + // before the conversion from to tab, so we can armor it that way. + if ( $wgUseTidy ) { + $out = str_replace( "\t", ' ', $out ); } - $lang = strtolower( $lang ); - if ( !preg_match( '/^[a-z_0-9-]*$/', $lang ) ) { - $error = self::formatLanguageError( $text ); - return $error; + + // Register CSS + $parser->getOutput()->addModuleStyles( 'ext.pygments' ); + + return $out; + } + + /** + * Highlight a code-block using a particular lexer. + * + * @param string $code Code to highlight. + * @param string|null $lexer Lexer 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. + * @return string Highlighted code as HTML. + */ + protected static function highlight( $code, $lexer = null, $args = array() ) { + global $wgPygmentizePath; + + if ( strlen( $code ) > self::HIGHLIGHT_MAX_BYTES ) { + $lexer = null; } - $geshi = self::prepare( $text, $lang ); - if ( !$geshi instanceof GeSHi ) { - $error = self::formatLanguageError( $text ); - return $error; + + $inline = isset( $args['enclose'] ) && $args['enclose'] === 'span'; + $attrs = array( 'class' => self::HIGHLIGHT_CSS_CLASS ); + + if ( $lexer === null ) { + if ( $inline ) { + return Html::element( 'span', $attrs, trim( $code ) ); + } + $pre = Html::element( 'pre', array(), $code ); + return Html::rawElement( 'div', $attrs, $pre ); } - $enclose = self::getEncloseType( $args ); + $options = array( + 'cssclass' => self::HIGHLIGHT_CSS_CLASS, + 'encoding' => 'utf-8', + ); // Line numbers if ( isset( $args['line'] ) ) { - $geshi->enable_line_numbers( GESHI_NORMAL_LINE_NUMBERS ); + $options['linenos'] = 'inline'; + } + + if ( $lexer === 'php' && strpos( $code, 'highlight_lines_extra( $lines ); + $options['hl_lines'] = implode( ',', $lines ); } } + // Starting line number if ( isset( $args['start'] ) ) { - $geshi->start_line_numbers_at( $args['start'] ); + $options['linenostart'] = $args['start']; } - $geshi->set_header_type( $enclose ); - // Strict mode - if ( isset( $args['strict'] ) ) { - $geshi->enable_strict_mode(); - } - // Format - $out = $geshi->parse_code(); - if ( $geshi->error == GESHI_ERROR_NO_SUCH_LANG ) { - // Common error :D - $error = self::formatLanguageError( $text ); - return $error; - } - $err = $geshi->error(); - if ( $err ) { - // Other unknown error! - $error = self::formatError( $err ); - return $error; - } - // Armour for Parser::doBlockLevels() - if ( $enclose === GESHI_HEADER_DIV ) { - $out = str_replace( "\n", '', $out ); - } - // HTML Tidy will convert tabs to spaces incorrectly (bug 30930). - // But the conversion from tab to space occurs while reading the input, - // before the conversion from to tab, so we can armor it that way. - if ( $wgUseTidy ) { - $out = str_replace( "\t", ' ', $out ); - } - // Register CSS - $parser->getOutput()->addModuleStyles( array( "ext.geshi.language.$lang", 'ext.geshi.local' ) ); - $encloseTag = $enclose === GESHI_HEADER_NONE ? 'span' : 'div'; - $attribs = Sanitizer::validateTagAttributes( $args, $encloseTag ); + if ( $inline ) { + $options['nowrap'] = 1; + } - //lang is valid in HTML context, but also used on GeSHi - unset( $attribs['lang'] ); + $cache = wfGetMainCache(); + $cacheKey = 'highlight:' . md5( json_encode( array( $lexer, $code, $options ) ) ); + $output = $cache->get( $cacheKey ); + + if ( $output === false ) { + try { + $pygments = new Pygments( $wgPygmentizePath ); + $output = $pygments->highlight( $code, $lexer, 'html', $options ); + } catch ( RuntimeException $e ) { + wfWarn( 'Failed to invoke Pygments. Please check that Pygments is installed ' . + 'and that $wgPygmentizePath is accurate.' ); + return self::highlight( $code, null, $args ); + } + $cache->set( $cacheKey, $output ); + } - if ( $enclose === GESHI_HEADER_NONE ) { - $attribs = self::addAttribute( $attribs, 'class', 'mw-geshi ' . $lang . ' source-' . $lang ); - } else { - // Default dir="ltr" (but allow dir="rtl", although unsure if needed) - $attribs['dir'] = isset( $attribs['dir'] ) && $attribs['dir'] === 'rtl' ? 'rtl' : 'ltr'; - $attribs = self::addAttribute( $attribs, 'class', 'mw-geshi mw-code mw-content-' . $attribs['dir'] ); + if ( $inline ) { + return Html::rawElement( 'span', $attrs, trim( $output ) ); } - $out = Html::rawElement( $encloseTag, $attribs, $out ); - return $out; - } + return $output; - /** - * @param $attribs array - * @param $name string - * @param $value string - * @return array - */ - private static function addAttribute( $attribs, $name, $value ) { - if ( isset( $attribs[$name] ) ) { - $attribs[$name] = $value . ' ' . $attribs[$name]; - } else { - $attribs[$name] = $value; - } - return $attribs; } /** * 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 $arg string - * @return array of ints + * @param string $lineSpec + * @return int[] Line numbers. */ - protected static function parseHighlightLines( $arg ) { + protected static function parseHighlightLines( $lineSpec ) { $lines = array(); - $values = array_map( 'trim', explode( ',', $arg ) ); - foreach ( $values as $value ) { - if ( ctype_digit($value) ) { - $lines[] = (int) $value; - } elseif ( strpos( $value, '-' ) !== false ) { - list( $start, $end ) = array_map( 'trim', explode( '-', $value ) ); - if ( self::validHighlightRange( $start, $end ) ) { - for ($i = intval( $start ); $i <= $end; $i++ ) { - $lines[] = $i; - } - } else { - wfDebugLog( 'geshi', "Invalid range: $value\n" ); - } - } else { - wfDebugLog( 'geshi', "Invalid line: $value\n" ); + + foreach ( preg_split( '/\s*,\s*/', $lineSpec ) as $spec ) { + // Individual line + if ( ctype_digit( $spec ) ) { + $lines[] = (int) $spec; } - } - return $lines; - } - /** - * Validate a provided input range - * @param $start - * @param $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. - $arbitrarilyLargeConstant = 10000; - return - ctype_digit($start) && - ctype_digit($end) && - $start > 0 && - $start < $end && - $end - $start < $arbitrarilyLargeConstant; - } + // Range of lines + if ( preg_match( '/^(?P\d+) *- *(?P\d+)$/', $spec, $match ) ) { + $range = $match['start'] - $match['end']; + if ( $range > 0 && $range <= self::HIGHLIGHT_MAX_LINES ) { + $lines = array_merge( $lines, range( $match['start'], $match['end'] ) ); + } + } - /** - * @param $args array - * @return int - */ - static function getEncloseType( $args ) { - // "Enclose" parameter - $enclose = GESHI_HEADER_PRE_VALID; - if ( isset( $args['enclose'] ) ) { - if ( $args['enclose'] === 'div' ) { - $enclose = GESHI_HEADER_DIV; - } elseif ( $args['enclose'] === 'none' ) { - $enclose = GESHI_HEADER_NONE; + if ( count( $lines ) > self::HIGHLIGHT_MAX_LINES ) { + $lines = array_slice( $lines, 0, self::HIGHLIGHT_MAX_LINES ); + break; } } - return $enclose; + return $lines; } /** * Hook into Content::getParserOutput to provide syntax highlighting for * script content. * * @return bool * @since MW 1.21 */ - public static function renderHook( Content $content, Title $title, - $revId, ParserOptions $options, $generateHtml, ParserOutput &$output - ) { + public static function onContentGetParserOutput( Content $content, Title $title, + $revId, ParserOptions $options, $generateHtml, ParserOutput &$output ) { - global $wgSyntaxHighlightModels, $wgUseSiteCss, - $wgParser, $wgTextModelsToParse; + global $wgParser, $wgTextModelsToParse; - $highlightModels = ExtensionRegistry::getInstance()->getAttribute( 'SyntaxHighlightModels' ); + if ( !$generateHtml ) { + // Nothing special for us to do, let MediaWiki handle this. + return true; + } // Determine the language + $extension = ExtensionRegistry::getInstance(); + $models = $extension->getAttribute( 'SyntaxHighlightModels' ); $model = $content->getModel(); - if ( !isset( $highlightModels[$model] ) && !isset( $wgSyntaxHighlightModels[$model] ) ) { + if ( !isset( $models[$model] ) ) { // We don't care about this model, carry on. return true; } - - if ( !$generateHtml ) { - // Nothing special for us to do, let MediaWiki handle this. - return true; - } + $lexer = $models[$model]; // Hope that $wgSyntaxHighlightModels does not contain silly types. $text = ContentHandler::getContentText( $content ); - - if ( $text === null || $text === false ) { + if ( !$text ) { // Oops! Non-text content? Let MediaWiki handle this. return true; } // 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 ( $content instanceof TextContent && in_array( $model, $wgTextModelsToParse ) ) { $output = $wgParser->parse( $text, $title, $options, true, true, $revId ); } - if ( isset( $highlightModels[$model] ) ) { - $lang = $highlightModels[$model]; - } else { - // TODO: Add deprecation warning after a while? - $lang = $wgSyntaxHighlightModels[$model]; + $out = self::highlight( $text, $lexer ); + if ( !$out ) { + return true; } + $output->addModuleStyles( 'ext.pygments' ); + $output->setText( '
' . $out . '
' ); - // Attempt to format - $geshi = self::prepare( $text, $lang ); - if ( $geshi instanceof GeSHi ) { - - $out = $geshi->parse_code(); - if ( !$geshi->error() ) { - // Done - $output->addModuleStyles( "ext.geshi.language.$lang" ); - $output->setText( "
{$out}
" ); - - if ( $wgUseSiteCss ) { - $output->addModuleStyles( 'ext.geshi.local' ); - } - - // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. - return false; - } - } - - // Bottle out - return true; + // 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 */ - public static function apiFormatHighlight( IContextSource $context, $text, $mime, $format ) { - switch ( $mime ) { - case 'text/javascript': - case 'application/json': - $lang = 'javascript'; - break; - - case 'text/xml': - $lang = 'xml'; - break; - - default: - // Don't know how to handle this - return true; + public static function onApiFormatHighlight( IContextSource $context, $text, $mime, $format ) { + if ( !isset( self::$mimeLexers[$mime] ) ) { + return true; } - $geshi = self::prepare( $text, $lang ); - if ( $geshi instanceof GeSHi ) { - $out = $geshi->parse_code(); - if ( !$geshi->error() ) { - if ( preg_match( '/^]*)>/i', $out, $m ) ) { - $attrs = Sanitizer::decodeTagAttributes( $m[1] ); - $attrs['class'] .= ' api-pretty-content'; - $out = '' . - substr( $out, strlen( $m[0] ) ); - } - $output = $context->getOutput(); - $output->addModuleStyles( array( "ext.geshi.language.$lang", 'ext.geshi.local' ) ); - $output->addHTML( "
{$out}
" ); + $lexer = self::$mimeLexers[$mime]; + $out = self::highlight( $text, $lexer ); - // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. - return false; - } + if ( !$out ) { + return true; } - // Bottle out - return true; - } - - /** - * Initialise a GeSHi object to format some code, performing - * common setup for all our uses of it - * - * @param string $text - * @param string $lang - * @return GeSHi - */ - public static function prepare( $text, $lang ) { - - global $wgSyntaxHighlightKeywordLinks; - - self::initialise(); - $geshi = new GeSHi( $text, $lang ); - if ( $geshi->error == GESHI_ERROR_NO_SUCH_LANG ) { - return null; + if ( preg_match( '/^]*)>/i', $out, $m ) ) { + $attrs = Sanitizer::decodeTagAttributes( $m[1] ); + $attrs['class'] .= ' api-pretty-content'; + $encodedAttrs = Sanitizer::safeEncodeTagAttributes( $attrs ); + $out = '' . substr( $out, strlen( $m[0] ) ); } - $geshi->set_encoding( 'UTF-8' ); - $geshi->enable_classes(); - $geshi->set_overall_class( "source-$lang" ); - $geshi->enable_keyword_links( $wgSyntaxHighlightKeywordLinks ); - - // If the source code is over 100 kB, disable higlighting of symbols. - // If over 200 kB, disable highlighting of strings too. - $bytes = strlen( $text ); - if ( $bytes > 102400 ) { - $geshi->set_symbols_highlighting( false ); - if ( $bytes > 204800 ) { - $geshi->set_strings_highlighting( false ); - } - } - - /** - * GeSHi comes by default with a font-family set to monospace, which - * causes the font-size to be smaller than one would expect. - * We append a CSS hack to the default GeSHi styles: specifying 'monospace' - * twice "resets" the browser font-size specified for monospace. - * - * The hack is documented in MediaWiki core under - * docs/uidesign/monospace.html and in bug 33496. - */ - // Preserve default since we don't want to override the other style - // properties set by geshi (padding, font-size, vertical-align etc.) - $geshi->set_code_style( - 'font-family: monospace, monospace;', - /* preserve defaults = */ true - ); - - // No need to preserve default (which is just "font-family: monospace;") - // outputting both is unnecessary - $geshi->set_overall_style( - 'font-family: monospace, monospace;', - /* preserve defaults = */ false - ); + $output = $context->getOutput(); + $output->addModuleStyles( 'ext.pygments' ); + $output->addHTML( '
' . $out . '
' ); - return $geshi; + // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. + return false; } - /** - * Prepare a CSS snippet suitable for use as a ParserOutput/OutputPage - * head item. - * - * Not used anymore, kept for backwards-compatibility with other extensions. - * - * @deprecated - * @param GeSHi $geshi - * @return string - */ - public static function buildHeadItem( $geshi ) { + /** Backward-compatibility shim for extensions. */ + public static function prepare( $text, $lang ) { wfDeprecated( __METHOD__ ); - $css = array(); - $css[] = ''; - return implode( "\n", $css ); - } - - /** - * Get the complete CSS code necessary to display styles for given GeSHi instance. - * - * @param GeSHi $geshi - * @return string - */ - public static function getCSS( $geshi ) { - $lang = $geshi->language; - $css = array(); - $css[] = ".source-$lang {line-height: normal;}"; - $css[] = ".source-$lang li, .source-$lang pre {"; - $css[] = "\tline-height: normal; border: 0px none white;"; - $css[] = "}"; - $css[] = $geshi->get_stylesheet( /*$economy_mode*/ false ); - return implode( "\n", $css ); - } - - /** - * Format an 'unknown language' error message and append formatted - * plain text to it. - * - * @param string $text - * @return string HTML fragment - */ - private static function formatLanguageError( $text ) { - $msg = wfMessage( 'syntaxhighlight-err-language' )->inContentLanguage()->escaped(); - $error = self::formatError( $msg, $text ); - return $error . '
' . htmlspecialchars( $text ) . '
'; - } - - /** - * Format an error message - * - * @param string $error - * @return string - */ - private static function formatError( $error = '' ) { - $html = ''; - if ( $error ) { - $html .= "

{$error}

"; - } - $html .= '

' . wfMessage( 'syntaxhighlight-specify')->inContentLanguage()->escaped() - . ' <source lang="html4strict">...</source>

' - . '

' . wfMessage( 'syntaxhighlight-supported' )->inContentLanguage()->escaped() - . '

' . self::formatLanguages(); - return "
{$html}
"; - } - - /** - * Format the list of supported languages - * - * @return string - */ - private static function formatLanguages() { - $langs = self::getSupportedLanguages(); - $list = array(); - if ( count( $langs ) > 0 ) { - foreach ( $langs as $lang ) { - $list[] = '' . htmlspecialchars( $lang ) . ''; - } - return '

' . implode( ', ', $list ) . '


'; - } else { - return '

' . wfMessage( 'syntaxhighlight-err-loading' )->inContentLanguage()->escaped() . '

'; - } - } - - /** - * Get the list of supported languages - * - * @return array - */ - private static function getSupportedLanguages() { - global $wgGeSHiSupportedLanguages; - self::initialise(); - return $wgGeSHiSupportedLanguages; - } - - /** - * Initialise messages and ensure the GeSHi class is loaded - * @return bool - */ - private static function initialise() { - if ( !self::$initialised ) { - if ( !class_exists( 'GeSHi' ) ) { - require ( dirname( __FILE__ ) . '/geshi/geshi.php' ); - } - self::$initialised = true; - } - return true; + $html = self::highlight( $text, $lang ); + return new GeSHi( $html ); } - /** - * Register a ResourceLoader module providing styles for each supported language. - * - * @param ResourceLoader $resourceLoader - * @return bool true - */ - public static function resourceLoaderRegisterModules( &$resourceLoader ) { - $modules = array(); - - foreach ( self::getSupportedLanguages() as $lang ) { - $modules["ext.geshi.language.$lang" ] = array( - 'position' => 'top', - 'class' => 'ResourceLoaderGeSHiModule', - 'lang' => $lang, - ); - } - - $resourceLoader->register( $modules ); - - return true; + /** Backward-compatibility shim for extensions. */ + public static function buildHeadItem( $geshi ) { + wfDeprecated( __METHOD__ ); + $geshi->parse_code(); + return ''; } } diff --git a/SyntaxHighlight_GeSHi.compat.php b/SyntaxHighlight_GeSHi.compat.php new file mode 100644 index 0000000..9a89c87 --- /dev/null +++ b/SyntaxHighlight_GeSHi.compat.php @@ -0,0 +1,113 @@ + 'basic', + 'thinbasic' => 'basic', + 'sdlbasic' => 'basic', + 'purebasic' => 'basic', + 'mapbasic' => 'basic', + 'locobasic' => 'basic', + 'gwbasic' => 'basic', + 'freebasic' => 'basic', + 'basic4gl' => 'basic', + 'zxbasic' => 'basic', + 'gambas' => 'basic', + 'oobas' => 'basic', + 'bascomavr' => 'basic', + + // C / C++ + 'c_loadrunner' => 'c', + 'c_mac' => 'c', + 'c_winapi' => 'c', + 'upc' => 'c', + 'cpp-qt' => 'cpp', + 'cpp-winapi' => 'cpp', + 'urbi' => 'cpp', + + // HTML + 'html4strict' => 'html', + 'html5' => 'html', + + // JavaScript + 'jquery' => 'javascript', + 'ecmascript' => 'javascript', + + // Microsoft + 'vb' => 'vbnet', + 'asp' => 'aspx-vb', + 'visualfoxpro' => 'foxpro', + 'dos' => 'bat', + 'visualprolog' => 'prolog', + 'reg' => 'registry', + + // Miscellaneous + 'cadlisp' => 'lisp', + 'j' => 'objj', + 'java5' => 'java', + 'php-brief' => 'php', + 'povray' => 'pov', + 'pys60' => 'python', + 'rails' => 'ruby', + 'rpmspec' => 'spec', + 'rsplus' => 'splus', + + // ML + 'ocaml-brief' => 'ocaml', + 'standardml' => 'sml', + + // Modula 2 + 'modula3' => 'modula2', + 'oberon2' => 'modula2', + + // SQL + 'tsql' => 'sql', + 'plsql' => 'sql', + 'oracle11' => 'sql', + 'oracle8' => 'sql', + + // REXX + 'oorexx' => 'rexx', + 'netrexx' => 'rexx', + ); + + public function __construct( $html ) { + $this->html = $html; + } + + public function error() { + } + + public function set_language( $language ) { + } + + public function parse_code() { + global $wgOut; + $wgOut->addModuleStyles( 'ext.pygments' ); + return $this->html; + } +} diff --git a/SyntaxHighlight_GeSHi.langs.php b/SyntaxHighlight_GeSHi.langs.php deleted file mode 100644 index 535ec02..0000000 --- a/SyntaxHighlight_GeSHi.langs.php +++ /dev/null @@ -1,245 +0,0 @@ - * @ingroup Maintenance */ +use KzykHys\Pygments\Pygments; + $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; require_once "$IP/maintenance/Maintenance.php"; -require_once __DIR__ . "/../geshi/geshi.php"; -class UpdateLanguageList extends Maintenance { +class UpdateCSS extends Maintenance { + public function __construct() { parent::__construct(); - $this->addDescription( 'Update list of languages supported by SyntaxHighlight_GeSHi' ); + $this->addDescription( 'Generate CSS code for SyntaxHighlight_GeSHi' ); } public function execute() { - global $IP; + global $wgPygmentizePath; - function lang_filter( $val ) { - return preg_match('/^[a-zA-Z0-9\-_]+$/', $val); + $target = __DIR__ . '/../modules/pygments.generated.css'; + $pygments = new Pygments( $wgPygmentizePath ); + $css = "/* Stylesheet generated by updateCSS.php */\n"; + $css .= $pygments->getCss( 'default', '.' . SyntaxHighlight_GeSHi::HIGHLIGHT_CSS_CLASS ); + if ( file_put_contents( $target, $css ) === false ) { + $this->output( "Failed to write to {$target}\n" ); + } else { + $this->output( 'CSS written to ' . realpath( $target ) . "\n" ); } - - $geshi = new GeSHi; - $header = '// Generated by ' . basename( __FILE__ ) . ' on ' . date( DATE_RFC2822 ) . "\n"; - $langs = array_values( array_filter( $geshi->get_supported_languages( false ), 'lang_filter' ) ); - sort( $langs ); - $replace = array( '[' => "array(\n\t", ']' => "\n);\n", '",' => "\",\n\t" ); - $code = "output( "Updated language list written to SyntaxHighlight_GeSHi.langs.php\n" ); } } -$maintClass = "UpdateLanguageList"; -require_once ( RUN_MAINTENANCE_IF_MAIN ); +$maintClass = "UpdateCSS"; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/maintenance/updateLanguageList.php b/maintenance/updateLexerList.php similarity index 63% rename from maintenance/updateLanguageList.php rename to maintenance/updateLexerList.php index fe122e9..b8c792c 100644 --- a/maintenance/updateLanguageList.php +++ b/maintenance/updateLexerList.php @@ -1,58 +1,60 @@ * @ingroup Maintenance */ +use KzykHys\Pygments\Pygments; + $IP = getenv( 'MW_INSTALL_PATH' ) ?: __DIR__ . '/../../..'; require_once "$IP/maintenance/Maintenance.php"; -require_once __DIR__ . "/../geshi/geshi.php"; class UpdateLanguageList extends Maintenance { public function __construct() { parent::__construct(); - $this->addDescription( 'Update list of languages supported by SyntaxHighlight_GeSHi' ); + $this->addDescription( 'Update list of lexers supported by SyntaxHighlight_GeSHi' ); } public function execute() { - global $IP; + global $wgPygmentizePath; function lang_filter( $val ) { return preg_match('/^[a-zA-Z0-9\-_]+$/', $val); } - $geshi = new GeSHi; - $header = '// Generated by ' . basename( __FILE__ ) . ' on ' . date( DATE_RFC2822 ) . "\n"; - $langs = array_values( array_filter( $geshi->get_supported_languages( false ), 'lang_filter' ) ); - sort( $langs ); - $replace = array( '[' => "array(\n\t", ']' => "\n);\n", '",' => "\",\n\t" ); - $code = "output( "Updated language list written to SyntaxHighlight_GeSHi.langs.php\n" ); + $header = '// Generated by ' . basename( __FILE__ ) . "\n\n"; + + $pygments = new Pygments( $wgPygmentizePath ); + $lexers = array_keys( $pygments->getLexers() ); + sort( $lexers ); + + $code = "| (?=\())/i', '', $code ); + $code = preg_replace( "/^ +/m", "\t", $code ); + + file_put_contents( __DIR__ . '/../SyntaxHighlight_GeSHi.lexers.php', $code ); + $this->output( "Updated language list written to SyntaxHighlight_GeSHi.lexers.php\n" ); } } $maintClass = "UpdateLanguageList"; require_once ( RUN_MAINTENANCE_IF_MAIN ); diff --git a/modules/pygments.generated.css b/modules/pygments.generated.css new file mode 100644 index 0000000..12b23df --- /dev/null +++ b/modules/pygments.generated.css @@ -0,0 +1,64 @@ +/* Stylesheet generated by updateCSS.php */ +.mw-highlight .hll { background-color: #ffffcc } +.mw-highlight { background: #f8f8f8; } +.mw-highlight .c { color: #408080; font-style: italic } /* Comment */ +.mw-highlight .err { border: 1px solid #FF0000 } /* Error */ +.mw-highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.mw-highlight .o { color: #666666 } /* Operator */ +.mw-highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.mw-highlight .cp { color: #BC7A00 } /* Comment.Preproc */ +.mw-highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ +.mw-highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ +.mw-highlight .gd { color: #A00000 } /* Generic.Deleted */ +.mw-highlight .ge { font-style: italic } /* Generic.Emph */ +.mw-highlight .gr { color: #FF0000 } /* Generic.Error */ +.mw-highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.mw-highlight .gi { color: #00A000 } /* Generic.Inserted */ +.mw-highlight .go { color: #888888 } /* Generic.Output */ +.mw-highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.mw-highlight .gs { font-weight: bold } /* Generic.Strong */ +.mw-highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.mw-highlight .gt { color: #0044DD } /* Generic.Traceback */ +.mw-highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.mw-highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.mw-highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.mw-highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.mw-highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.mw-highlight .kt { color: #B00040 } /* Keyword.Type */ +.mw-highlight .m { color: #666666 } /* Literal.Number */ +.mw-highlight .s { color: #BA2121 } /* Literal.String */ +.mw-highlight .na { color: #7D9029 } /* Name.Attribute */ +.mw-highlight .nb { color: #008000 } /* Name.Builtin */ +.mw-highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.mw-highlight .no { color: #880000 } /* Name.Constant */ +.mw-highlight .nd { color: #AA22FF } /* Name.Decorator */ +.mw-highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ +.mw-highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.mw-highlight .nf { color: #0000FF } /* Name.Function */ +.mw-highlight .nl { color: #A0A000 } /* Name.Label */ +.mw-highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.mw-highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.mw-highlight .nv { color: #19177C } /* Name.Variable */ +.mw-highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.mw-highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.mw-highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.mw-highlight .mf { color: #666666 } /* Literal.Number.Float */ +.mw-highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.mw-highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.mw-highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.mw-highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.mw-highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.mw-highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.mw-highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.mw-highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.mw-highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.mw-highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.mw-highlight .sx { color: #008000 } /* Literal.String.Other */ +.mw-highlight .sr { color: #BB6688 } /* Literal.String.Regex */ +.mw-highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.mw-highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.mw-highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.mw-highlight .vc { color: #19177C } /* Name.Variable.Class */ +.mw-highlight .vg { color: #19177C } /* Name.Variable.Global */ +.mw-highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.mw-highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/modules/pygments.wrapper.css b/modules/pygments.wrapper.css new file mode 100644 index 0000000..fceb4ab --- /dev/null +++ b/modules/pygments.wrapper.css @@ -0,0 +1,8 @@ +.mw-highlight { + font-family: monospace; +} + +span.mw-highlight { + font-size: 1.2em; + padding: 0.2em; +}