[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License along 15 * with this program; if not, write to the Free Software Foundation, Inc., 16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 * http://www.gnu.org/copyleft/gpl.html 18 */ 19 20 class SyntaxHighlight_GeSHi { 21 /** 22 * Has GeSHi been initialised this session? 23 */ 24 private static $initialised = false; 25 26 /** 27 * List of languages available to GeSHi 28 * @var array 29 */ 30 private static $languages = null; 31 32 /** 33 * Parser hook 34 * 35 * @param string $text 36 * @param array $args 37 * @param Parser $parser 38 * @return string 39 */ 40 public static function parserHook( $text, $args = array(), $parser ) { 41 global $wgSyntaxHighlightDefaultLang, $wgUseSiteCss, $wgUseTidy; 42 wfProfileIn( __METHOD__ ); 43 self::initialise(); 44 $text = rtrim( $text ); 45 // Don't trim leading spaces away, just the linefeeds 46 $text = preg_replace( '/^\n+/', '', $text ); 47 48 // Validate language 49 if( isset( $args['lang'] ) && $args['lang'] ) { 50 $lang = $args['lang']; 51 } else { 52 // language is not specified. Check if default exists, if yes, use it. 53 if ( !is_null( $wgSyntaxHighlightDefaultLang ) ) { 54 $lang = $wgSyntaxHighlightDefaultLang; 55 } else { 56 $error = self::formatLanguageError( $text ); 57 wfProfileOut( __METHOD__ ); 58 return $error; 59 } 60 } 61 $lang = strtolower( $lang ); 62 if( !preg_match( '/^[a-z_0-9-]*$/', $lang ) ) { 63 $error = self::formatLanguageError( $text ); 64 wfProfileOut( __METHOD__ ); 65 return $error; 66 } 67 $geshi = self::prepare( $text, $lang ); 68 if( !$geshi instanceof GeSHi ) { 69 $error = self::formatLanguageError( $text ); 70 wfProfileOut( __METHOD__ ); 71 return $error; 72 } 73 74 $enclose = self::getEncloseType( $args ); 75 76 // Line numbers 77 if( isset( $args['line'] ) ) { 78 $geshi->enable_line_numbers( GESHI_FANCY_LINE_NUMBERS ); 79 } 80 // Highlighting specific lines 81 if( isset( $args['highlight'] ) ) { 82 $lines = self::parseHighlightLines( $args['highlight'] ); 83 if ( count( $lines ) ) { 84 $geshi->highlight_lines_extra( $lines ); 85 } 86 } 87 // Starting line number 88 if( isset( $args['start'] ) ) { 89 $geshi->start_line_numbers_at( $args['start'] ); 90 } 91 $geshi->set_header_type( $enclose ); 92 // Strict mode 93 if( isset( $args['strict'] ) ) { 94 $geshi->enable_strict_mode(); 95 } 96 // Format 97 $out = $geshi->parse_code(); 98 if ( $geshi->error == GESHI_ERROR_NO_SUCH_LANG ) { 99 // Common error :D 100 $error = self::formatLanguageError( $text ); 101 wfProfileOut( __METHOD__ ); 102 return $error; 103 } 104 $err = $geshi->error(); 105 if( $err ) { 106 // Other unknown error! 107 $error = self::formatError( $err ); 108 wfProfileOut( __METHOD__ ); 109 return $error; 110 } 111 // Armour for Parser::doBlockLevels() 112 if( $enclose === GESHI_HEADER_DIV ) { 113 $out = str_replace( "\n", '', $out ); 114 } 115 // HTML Tidy will convert tabs to spaces incorrectly (bug 30930). 116 // But the conversion from tab to space occurs while reading the input, 117 // before the conversion from 	 to tab, so we can armor it that way. 118 if( $wgUseTidy ) { 119 $out = str_replace( "\t", '	', $out ); 120 } 121 // Register CSS 122 $parser->getOutput()->addModuleStyles( "ext.geshi.language.$lang" ); 123 124 if ( $wgUseSiteCss ) { 125 $parser->getOutput()->addModuleStyles( 'ext.geshi.local' ); 126 } 127 128 $encloseTag = $enclose === GESHI_HEADER_NONE ? 'span' : 'div'; 129 $attribs = Sanitizer::validateTagAttributes( $args, $encloseTag ); 130 131 //lang is valid in HTML context, but also used on GeSHi 132 unset( $attribs['lang'] ); 133 134 if ( $enclose === GESHI_HEADER_NONE ) { 135 $attribs = self::addAttribute( $attribs, 'class', 'mw-geshi ' . $lang . ' source-' . $lang ); 136 } else { 137 // Default dir="ltr" (but allow dir="rtl", although unsure if needed) 138 $attribs['dir'] = isset( $attribs['dir'] ) && $attribs['dir'] === 'rtl' ? 'rtl' : 'ltr'; 139 $attribs = self::addAttribute( $attribs, 'class', 'mw-geshi mw-code mw-content-' . $attribs['dir'] ); 140 } 141 $out = Html::rawElement( $encloseTag, $attribs, $out ); 142 143 wfProfileOut( __METHOD__ ); 144 return $out; 145 } 146 147 /** 148 * @param $attribs array 149 * @param $name string 150 * @param $value string 151 * @return array 152 */ 153 private static function addAttribute( $attribs, $name, $value ) { 154 if( isset( $attribs[$name] ) ) { 155 $attribs[$name] = $value . ' ' . $attribs[$name]; 156 } else { 157 $attribs[$name] = $value; 158 } 159 return $attribs; 160 } 161 162 /** 163 * Take an input specifying a list of lines to highlight, returning 164 * a raw list of matching line numbers. 165 * 166 * Input is comma-separated list of lines or line ranges. 167 * 168 * @param $arg string 169 * @return array of ints 170 */ 171 protected static function parseHighlightLines( $arg ) { 172 $lines = array(); 173 $values = array_map( 'trim', explode( ',', $arg ) ); 174 foreach ( $values as $value ) { 175 if ( ctype_digit($value) ) { 176 $lines[] = (int) $value; 177 } elseif ( strpos( $value, '-' ) !== false ) { 178 list( $start, $end ) = array_map( 'trim', explode( '-', $value ) ); 179 if ( self::validHighlightRange( $start, $end ) ) { 180 for ($i = intval( $start ); $i <= $end; $i++ ) { 181 $lines[] = $i; 182 } 183 } else { 184 wfDebugLog( 'geshi', "Invalid range: $value\n" ); 185 } 186 } else { 187 wfDebugLog( 'geshi', "Invalid line: $value\n" ); 188 } 189 } 190 return $lines; 191 } 192 193 /** 194 * Validate a provided input range 195 * @param $start 196 * @param $end 197 * @return bool 198 */ 199 protected static function validHighlightRange( $start, $end ) { 200 // Since we're taking this tiny range and producing a an 201 // array of every integer between them, it would be trivial 202 // to DoS the system by asking for a huge range. 203 // Impose an arbitrary limit on the number of lines in a 204 // given range to reduce the impact. 205 $arbitrarilyLargeConstant = 10000; 206 return 207 ctype_digit($start) && 208 ctype_digit($end) && 209 $start > 0 && 210 $start < $end && 211 $end - $start < $arbitrarilyLargeConstant; 212 } 213 214 /** 215 * @param $args array 216 * @return int 217 */ 218 static function getEncloseType( $args ) { 219 // "Enclose" parameter 220 $enclose = GESHI_HEADER_PRE_VALID; 221 if ( isset( $args['enclose'] ) ) { 222 if ( $args['enclose'] === 'div' ) { 223 $enclose = GESHI_HEADER_DIV; 224 } elseif ( $args['enclose'] === 'none' ) { 225 $enclose = GESHI_HEADER_NONE; 226 } 227 } 228 229 return $enclose; 230 } 231 232 /** 233 * Hook into Content::getParserOutput to provide syntax highlighting for 234 * script content. 235 * 236 * @return bool 237 * @since MW 1.21 238 */ 239 public static function renderHook( Content $content, Title $title, 240 $revId, ParserOptions $options, $generateHtml, ParserOutput &$output 241 ) { 242 243 global $wgSyntaxHighlightModels, $wgUseSiteCss, 244 $wgParser, $wgTextModelsToParse; 245 246 // Determine the language 247 $model = $content->getModel(); 248 if ( !isset( $wgSyntaxHighlightModels[$model] ) ) { 249 // We don't care about this model, carry on. 250 return true; 251 } 252 253 if ( !$generateHtml ) { 254 // Nothing special for us to do, let MediaWiki handle this. 255 return true; 256 } 257 258 // Hope that $wgSyntaxHighlightModels does not contain silly types. 259 $text = ContentHandler::getContentText( $content ); 260 261 if ( $text === null || $text === false ) { 262 // Oops! Non-text content? Let MediaWiki handle this. 263 return true; 264 } 265 266 // Parse using the standard parser to get links etc. into the database, HTML is replaced below. 267 // We could do this using $content->fillParserOutput(), but alas it is 'protected'. 268 if ( $content instanceof TextContent && in_array( $model, $wgTextModelsToParse ) ) { 269 $output = $wgParser->parse( $text, $title, $options, true, true, $revId ); 270 } 271 272 $lang = $wgSyntaxHighlightModels[$model]; 273 274 // Attempt to format 275 $geshi = self::prepare( $text, $lang ); 276 if( $geshi instanceof GeSHi ) { 277 278 $out = $geshi->parse_code(); 279 if( !$geshi->error() ) { 280 // Done 281 $output->addModuleStyles( "ext.geshi.language.$lang" ); 282 $output->setText( "<div dir=\"ltr\">{$out}</div>" ); 283 284 if( $wgUseSiteCss ) { 285 $output->addModuleStyles( 'ext.geshi.local' ); 286 } 287 288 // Inform MediaWiki that we have parsed this page and it shouldn't mess with it. 289 return false; 290 } 291 } 292 293 // Bottle out 294 return true; 295 } 296 297 /** 298 * Initialise a GeSHi object to format some code, performing 299 * common setup for all our uses of it 300 * 301 * @param string $text 302 * @param string $lang 303 * @return GeSHi 304 */ 305 public static function prepare( $text, $lang ) { 306 307 global $wgSyntaxHighlightKeywordLinks; 308 309 self::initialise(); 310 $geshi = new GeSHi( $text, $lang ); 311 if( $geshi->error() == GESHI_ERROR_NO_SUCH_LANG ) { 312 return null; 313 } 314 $geshi->set_encoding( 'UTF-8' ); 315 $geshi->enable_classes(); 316 $geshi->set_overall_class( "source-$lang" ); 317 $geshi->enable_keyword_links( $wgSyntaxHighlightKeywordLinks ); 318 319 // If the source code is over 100 kB, disable higlighting of symbols. 320 // If over 200 kB, disable highlighting of strings too. 321 $bytes = strlen( $text ); 322 if ( $bytes > 102400 ) { 323 $geshi->set_symbols_highlighting( false ); 324 if ( $bytes > 204800 ) { 325 $geshi->set_strings_highlighting( false ); 326 } 327 } 328 329 /** 330 * GeSHi comes by default with a font-family set to monospace, which 331 * causes the font-size to be smaller than one would expect. 332 * We append a CSS hack to the default GeSHi styles: specifying 'monospace' 333 * twice "resets" the browser font-size specified for monospace. 334 * 335 * The hack is documented in MediaWiki core under 336 * docs/uidesign/monospace.html and in bug 33496. 337 */ 338 // Preserve default since we don't want to override the other style 339 // properties set by geshi (padding, font-size, vertical-align etc.) 340 $geshi->set_code_style( 341 'font-family: monospace, monospace;', 342 /* preserve defaults = */ true 343 ); 344 345 // No need to preserve default (which is just "font-family: monospace;") 346 // outputting both is unnecessary 347 $geshi->set_overall_style( 348 'font-family: monospace, monospace;', 349 /* preserve defaults = */ false 350 ); 351 352 return $geshi; 353 } 354 355 /** 356 * Prepare a CSS snippet suitable for use as a ParserOutput/OutputPage 357 * head item. 358 * 359 * Not used anymore, kept for backwards-compatibility with other extensions. 360 * 361 * @deprecated 362 * @param GeSHi $geshi 363 * @return string 364 */ 365 public static function buildHeadItem( $geshi ) { 366 wfDeprecated( __METHOD__ ); 367 $css = array(); 368 $css[] = '<style type="text/css">/*<![CDATA[*/'; 369 $css[] = self::getCSS( $geshi ); 370 $css[] = '/*]]>*/'; 371 $css[] = '</style>'; 372 return implode( "\n", $css ); 373 } 374 375 /** 376 * Get the complete CSS code necessary to display styles for given GeSHi instance. 377 * 378 * @param GeSHi $geshi 379 * @return string 380 */ 381 public static function getCSS( $geshi ) { 382 $lang = $geshi->language; 383 $css = array(); 384 $css[] = ".source-$lang {line-height: normal;}"; 385 $css[] = ".source-$lang li, .source-$lang pre {"; 386 $css[] = "\tline-height: normal; border: 0px none white;"; 387 $css[] = "}"; 388 $css[] = $geshi->get_stylesheet( /*$economy_mode*/ false ); 389 return implode( "\n", $css ); 390 } 391 392 /** 393 * Format an 'unknown language' error message and append formatted 394 * plain text to it. 395 * 396 * @param string $text 397 * @return string HTML fragment 398 */ 399 private static function formatLanguageError( $text ) { 400 $msg = wfMessage( 'syntaxhighlight-err-language' )->inContentLanguage()->escaped(); 401 $error = self::formatError( $msg, $text ); 402 return $error . '<pre>' . htmlspecialchars( $text ) . '</pre>'; 403 } 404 405 /** 406 * Format an error message 407 * 408 * @param string $error 409 * @return string 410 */ 411 private static function formatError( $error = '' ) { 412 $html = ''; 413 if( $error ) { 414 $html .= "<p>{$error}</p>"; 415 } 416 $html .= '<p>' . wfMessage( 'syntaxhighlight-specify')->inContentLanguage()->escaped() 417 . ' <samp><source lang="html4strict">...</source></samp></p>' 418 . '<p>' . wfMessage( 'syntaxhighlight-supported' )->inContentLanguage()->escaped() 419 . '</p>' . self::formatLanguages(); 420 return "<div style=\"border: solid red 1px; padding: .5em;\">{$html}</div>"; 421 } 422 423 /** 424 * Format the list of supported languages 425 * 426 * @return string 427 */ 428 private static function formatLanguages() { 429 $langs = self::getSupportedLanguages(); 430 $list = array(); 431 if( count( $langs ) > 0 ) { 432 foreach( $langs as $lang ) { 433 $list[] = '<samp>' . htmlspecialchars( $lang ) . '</samp>'; 434 } 435 return '<p class="mw-collapsible mw-collapsed" style="padding: 0em 1em;">' . implode( ', ', $list ) . '</p><br style="clear: all"/>'; 436 } else { 437 return '<p>' . wfMessage( 'syntaxhighlight-err-loading' )->inContentLanguage()->escaped() . '</p>'; 438 } 439 } 440 441 /** 442 * Get the list of supported languages 443 * 444 * @return array 445 */ 446 private static function getSupportedLanguages() { 447 if( !is_array( self::$languages ) ) { 448 self::initialise(); 449 self::$languages = array(); 450 foreach( glob( GESHI_LANG_ROOT . "/*.php" ) as $file ) { 451 self::$languages[] = basename( $file, '.php' ); 452 } 453 sort( self::$languages ); 454 } 455 return self::$languages; 456 } 457 458 /** 459 * Initialise messages and ensure the GeSHi class is loaded 460 * @return bool 461 */ 462 private static function initialise() { 463 if( !self::$initialised ) { 464 if( !class_exists( 'GeSHi' ) ) { 465 require( dirname( __FILE__ ) . '/geshi/geshi.php' ); 466 } 467 self::$initialised = true; 468 } 469 return true; 470 } 471 472 /** 473 * Get the GeSHI's version information while Special:Version is read. 474 * @param $extensionTypes 475 * @return bool 476 */ 477 public static function extensionTypes( &$extensionTypes ) { 478 global $wgExtensionCredits; 479 self::initialise(); 480 $wgExtensionCredits['parserhook']['SyntaxHighlight_GeSHi']['version'] = GESHI_VERSION; 481 return true; 482 } 483 484 /** 485 * Register a ResourceLoader module providing styles for each supported language. 486 * 487 * @param ResourceLoader $resourceLoader 488 * @return bool true 489 */ 490 public static function resourceLoaderRegisterModules( &$resourceLoader ) { 491 $modules = array(); 492 493 foreach ( self::getSupportedLanguages() as $lang ) { 494 $modules["ext.geshi.language.$lang" ] = array( 495 'class' => 'ResourceLoaderGeSHiModule', 496 'lang' => $lang, 497 ); 498 } 499 500 $resourceLoader->register( $modules ); 501 502 return true; 503 } 504 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |