[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/extensions/SyntaxHighlight_GeSHi/ -> SyntaxHighlight_GeSHi.class.php (source)

   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 &#9; to tab, so we can armor it that way.
 118          if( $wgUseTidy ) {
 119              $out = str_replace( "\t", '&#9;', $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>&lt;source lang=&quot;html4strict&quot;&gt;...&lt;/source&gt;</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  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1