3
class SyntaxHighlight_GeSHi {
6
* Has GeSHi been initialised this session?
8
private static $initialised = false;
11
* List of languages available to GeSHi
13
private static $languages = null;
20
* @param Parser $parser
23
public static function parserHook( $text, $args = array(), $parser ) {
25
$text = rtrim( $text );
26
// Don't trim leading spaces away, just the linefeeds
27
$text = preg_replace( '/^\n+/', '', $text );
29
if( isset( $args['lang'] ) ) {
30
$lang = strtolower( $args['lang'] );
32
return self::formatError( htmlspecialchars( wfMsgForContent( 'syntaxhighlight-err-language' ) ) );
34
if( !preg_match( '/^[a-z_0-9-]*$/', $lang ) )
35
return self::formatError( htmlspecialchars( wfMsgForContent( 'syntaxhighlight-err-language' ) ) );
36
$geshi = self::prepare( $text, $lang );
37
if( !$geshi instanceof GeSHi )
38
return self::formatError( htmlspecialchars( wfMsgForContent( 'syntaxhighlight-err-language' ) ) );
40
$enclose = self::getEncloseType( $args );
43
if( isset( $args['line'] ) ) {
44
$geshi->enable_line_numbers( GESHI_FANCY_LINE_NUMBERS );
46
// Highlighting specific lines
47
if( isset( $args['highlight'] ) ) {
48
$lines = self::parseHighlightLines( $args['highlight'] );
49
if ( count($lines) ) $geshi->highlight_lines_extra( $lines );
51
// Starting line number
52
if( isset( $args['start'] ) )
53
$geshi->start_line_numbers_at( $args['start'] );
54
$geshi->set_header_type( $enclose );
56
if( isset( $args['strict'] ) )
57
$geshi->enable_strict_mode();
59
$out = $geshi->parse_code();
60
$err = $geshi->error();
63
return self::formatError( $err );
65
// Armour for Parser::doBlockLevels()
66
if( $enclose === GESHI_HEADER_DIV )
67
$out = str_replace( "\n", '', $out );
69
$parser->mOutput->addHeadItem( self::buildHeadItem( $geshi ), "source-{$lang}" );
70
if ( $enclose === GESHI_HEADER_NONE ) {
71
return '<span class="'.$lang.' source-'.$lang.'"> '.$out . '</span>';
73
return '<div dir="ltr" style="text-align: left;">' . $out . '</div>';
79
* Take an input specifying a list of lines to highlight, returning
80
* a raw list of matching line numbers.
82
* Input is comma-separated list of lines or line ranges.
85
* @return array of ints
87
protected static function parseHighlightLines( $arg ) {
89
$values = array_map( 'trim', explode( ',', $arg ) );
90
foreach ( $values as $value ) {
91
if ( ctype_digit($value) ) {
92
$lines[] = (int) $value;
93
} elseif ( strpos( $value, '-' ) !== false ) {
94
list( $start, $end ) = array_map( 'trim', explode( '-', $value ) );
95
if ( self::validHighlightRange( $start, $end ) ) {
96
for ($i = intval( $start ); $i <= $end; $i++ ) {
100
wfDebugLog( 'geshi', "Invalid range: $value\n" );
103
wfDebugLog( 'geshi', "Invalid line: $value\n" );
110
* Validate a provided input range
112
protected static function validHighlightRange( $start, $end ) {
113
// Since we're taking this tiny range and producing a an
114
// array of every integer between them, it would be trivial
115
// to DoS the system by asking for a huge range.
116
// Impose an arbitrary limit on the number of lines in a
117
// given range to reduce the impact.
118
$arbitrarilyLargeConstant = 10000;
120
ctype_digit($start) &&
124
$end - $start < $arbitrarilyLargeConstant;
127
static function getEncloseType( $args ) {
128
// Since version 1.0.8 geshi can produce valid pre, but we need to check for it
129
if ( defined('GESHI_HEADER_PRE_VALID') ) {
130
$pre = GESHI_HEADER_PRE_VALID;
132
$pre = GESHI_HEADER_PRE;
135
// "Enclose" parameter
137
if ( isset( $args['enclose'] ) ) {
138
if ( $args['enclose'] === 'div' ) {
139
$enclose = GESHI_HEADER_DIV;
140
} elseif ( $args['enclose'] === 'none' ) {
141
$enclose = GESHI_HEADER_NONE;
145
if( isset( $args['line'] ) && $pre === GESHI_HEADER_PRE ) {
146
// Force <div> mode to maintain valid XHTML, see
147
// http://sourceforge.net/tracker/index.php?func=detail&aid=1201963&group_id=114997&atid=670231
148
$enclose = GESHI_HEADER_DIV;
155
* Hook into Article::view() to provide syntax highlighting for
156
* custom CSS and JavaScript pages
158
* @param string $text
159
* @param Title $title
160
* @param OutputPage $output
163
public static function viewHook( $text, $title, $output ) {
164
// Determine the language
165
preg_match( '!\.(css|js)$!u', $title->getText(), $matches );
166
$lang = $matches[1] == 'css' ? 'css' : 'javascript';
168
$geshi = self::prepare( $text, $lang );
169
if( $geshi instanceof GeSHi ) {
170
$out = $geshi->parse_code();
171
if( !$geshi->error() ) {
173
$output->addHeadItem( "source-$lang", self::buildHeadItem( $geshi ) );
174
$output->addHTML( "<div dir=\"ltr\">{$out}</div>" );
183
* Initialise a GeSHi object to format some code, performing
184
* common setup for all our uses of it
186
* @param string $text
187
* @param string $lang
190
private static function prepare( $text, $lang ) {
192
$geshi = new GeSHi( $text, $lang );
193
if( $geshi->error() == GESHI_ERROR_NO_SUCH_LANG )
195
$geshi->set_encoding( 'UTF-8' );
196
$geshi->enable_classes();
197
$geshi->set_overall_class( "source-$lang" );
198
$geshi->enable_keyword_links( false );
203
* Prepare a CSS snippet suitable for use as a ParserOutput/OutputPage
206
* @param GeSHi $geshi
209
private static function buildHeadItem( $geshi ) {
210
global $wgUseSiteCss, $wgSquidMaxage;
211
$lang = $geshi->language;
212
$css[] = '<style type="text/css">/*<![CDATA[*/';
213
$css[] = ".source-$lang {line-height: normal;}";
214
$css[] = ".source-$lang li, .source-$lang pre {";
215
$css[] = "\tline-height: normal; border: 0px none white;";
217
$css[] = $geshi->get_stylesheet( false );
220
if( $wgUseSiteCss ) {
221
$title = Title::makeTitle( NS_MEDIAWIKI, 'Geshi.css' );
222
$q = "usemsgcache=yes&action=raw&ctype=text/css&smaxage={$wgSquidMaxage}";
223
$css[] = '<style type="text/css">/*<![CDATA[*/';
224
$css[] = '@import "' . $title->getLocalUrl( $q ) . '";';
228
return implode( "\n", $css );
232
* Format an error message
234
* @param string $error
237
private static function formatError( $error = '' ) {
240
$html .= "<p>{$error}</p>";
241
$html .= '<p>' . htmlspecialchars( wfMsgForContent( 'syntaxhighlight-specify' ) )
242
. ' <samp><source lang="html4strict">...</source></samp></p>'
243
. '<p>' . htmlspecialchars( wfMsgForContent( 'syntaxhighlight-supported' ) ) . '</p>'
244
. self::formatLanguages();
245
return "<div style=\"border: solid red 1px; padding: .5em;\">{$html}</div>";
249
* Format the list of supported languages
253
private static function formatLanguages() {
254
$langs = self::getSupportedLanguages();
256
if( count( $langs ) > 0 ) {
257
foreach( $langs as $lang ) {
258
$list[] = '<samp>' . htmlspecialchars( $lang ) . '</samp>';
260
return '<p style="padding: 0em 1em;">' . implode( ', ', $list ) . '</p>';
262
return '<p>' . htmlspecialchars( wfMsgForContent( 'syntaxhighlight-err-loading' ) ) . '</p>';
267
* Get the list of supported languages
271
private static function getSupportedLanguages() {
272
if( !is_array( self::$languages ) ) {
274
self::$languages = array();
275
foreach( glob( GESHI_LANG_ROOT . "/*.php" ) as $file ) {
276
self::$languages[] = basename( $file, '.php' );
278
sort( self::$languages );
280
return self::$languages;
284
* Initialise messages and ensure the GeSHi class is loaded
286
private static function initialise() {
287
if( !self::$initialised ) {
288
wfLoadExtensionMessages( 'SyntaxHighlight_GeSHi' );
289
if( !class_exists( 'GeSHi' ) )
290
require( 'geshi/geshi.php' );
291
self::$initialised = true;