Advertisement
lcawte

Untitled

Nov 20th, 2011
71
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 11.21 KB | None | 0 0
  1. <?php
  2.  
  3. class SyntaxHighlight_GeSHi {
  4.  
  5.     /**
  6.      * Has GeSHi been initialised this session?
  7.      */
  8.     private static $initialised = false;
  9.  
  10.     /**
  11.      * List of languages available to GeSHi
  12.      */
  13.     private static $languages = null;
  14.  
  15.     /**
  16.      * Parser hook
  17.      *
  18.      * @param string $text
  19.      * @param array $args
  20.      * @param Parser $parser
  21.      * @return string
  22.      */
  23.     public static function parserHook( $text, $args = array(), $parser ) {
  24.         global $wgSyntaxHighlightDefaultLang, $wgUseSiteCSS, $wgUseTidy;
  25.         wfProfileIn( __METHOD__ );
  26.         self::initialise();
  27.         $text = rtrim( $text );
  28.         // Don't trim leading spaces away, just the linefeeds
  29.         $text = preg_replace( '/^\n+/', '', $text );
  30.  
  31.         if( $wgUseTidy ) {
  32.             // HTML Tidy will convert tabs to spaces incorrectly (bug 30930).
  33.             // Preemptively replace the spaces in a more controlled fashion.
  34.             $text = self::tabsToSpaces( $text );
  35.         }
  36.  
  37.         // Validate language
  38.         if( isset( $args['lang'] ) && $args['lang'] ) {
  39.             $lang = $args['lang'];
  40.         } else {
  41.             // language is not specified. Check if default exists, if yes, use it.
  42.             if ( !is_null( $wgSyntaxHighlightDefaultLang ) ) {
  43.                 $lang = $wgSyntaxHighlightDefaultLang;
  44.             } else {
  45.                 $error = self::formatLanguageError( $text );
  46.                 wfProfileOut( __METHOD__ );
  47.                 return $error;
  48.             }
  49.         }
  50.         $lang = strtolower( $lang );
  51.         if( !preg_match( '/^[a-z_0-9-]*$/', $lang ) ) {
  52.             $error = self::formatLanguageError( $text );
  53.             wfProfileOut( __METHOD__ );
  54.             return $error;
  55.         }
  56.         $geshi = self::prepare( $text, $lang );
  57.         if( !$geshi instanceof GeSHi ) {
  58.             $error = self::formatLanguageError( $text );
  59.             wfProfileOut( __METHOD__ );
  60.             return $error;
  61.         }
  62.  
  63.         $enclose = self::getEncloseType( $args );
  64.  
  65.         // Line numbers
  66.         if( isset( $args['line'] ) ) {
  67.             $geshi->enable_line_numbers( GESHI_FANCY_LINE_NUMBERS );
  68.         }
  69.         // Highlighting specific lines
  70.         if( isset( $args['highlight'] ) ) {
  71.             $lines = self::parseHighlightLines( $args['highlight'] );
  72.             if ( count($lines) ) {
  73.                 $geshi->highlight_lines_extra( $lines );
  74.             }
  75.         }
  76.         // Starting line number
  77.         if( isset( $args['start'] ) ) {
  78.             $geshi->start_line_numbers_at( $args['start'] );
  79.         }
  80.         $geshi->set_header_type( $enclose );
  81.         // Strict mode
  82.         if( isset( $args['strict'] ) ) {
  83.             $geshi->enable_strict_mode();
  84.         }
  85.         // Format
  86.         $out = $geshi->parse_code();
  87.         if ( $geshi->error == GESHI_ERROR_NO_SUCH_LANG ) {
  88.             // Common error :D
  89.             $error = self::formatLanguageError( $text );
  90.             wfProfileOut( __METHOD__ );
  91.             return $error;
  92.         }
  93.         $err = $geshi->error();
  94.         if( $err ) {
  95.             // Other unknown error!
  96.             $error = self::formatError( $err );
  97.             wfProfileOut( __METHOD__ );
  98.             return $error;
  99.         }
  100.         // Armour for Parser::doBlockLevels()
  101.         if( $enclose === GESHI_HEADER_DIV )
  102.             $out = str_replace( "\n", '', $out );
  103.         // Register CSS
  104.         $parser->mOutput->addHeadItem( self::buildHeadItem( $geshi ), "source-{$lang}" );
  105.  
  106.         if( $wgUseSiteCss ) {
  107.                     $parser->mOutput->addModuleStyles( 'ext.geshi.local' );
  108.             }
  109.  
  110.  
  111.         $encloseTag = $enclose === GESHI_HEADER_NONE ? 'span' : 'div';
  112.         $attribs = Sanitizer::validateTagAttributes( $args, $encloseTag );
  113.  
  114.         //lang is valid in HTML context, but also used on GeSHi
  115.         unset( $attribs['lang'] );
  116.  
  117.         if ( $enclose === GESHI_HEADER_NONE ) {
  118.             $attribs = self::addAttribute( $attribs, 'class', 'mw-geshi ' . $lang . ' source-' . $lang );
  119.         } else {
  120.             if ( !isset( $attribs['dir'] ) ) {
  121.                 $attribs = self::addAttribute( $attribs, 'dir', 'ltr' );
  122.             }
  123.  
  124.             $attribs = self::addAttribute( $attribs, 'class', 'mw-geshi' );
  125.             $attribs = self::addAttribute( $attribs, 'style', 'text-align: left;' );
  126.         }
  127.         $out = Xml::tags( $encloseTag, $attribs, $out );
  128.  
  129.         wfProfileOut( __METHOD__ );
  130.         return $out;
  131.     }
  132.  
  133.     private static function addAttribute( $attribs, $name, $value ) {
  134.         if( isset( $attribs[$name] ) ) {
  135.             $attribs[$name] = $value . ' ' . $attribs[$name];
  136.         } else {
  137.             $attribs[$name] = $value;
  138.         }
  139.         return $attribs;
  140.     }
  141.  
  142.     /**
  143.      * Take an input specifying a list of lines to highlight, returning
  144.      * a raw list of matching line numbers.
  145.      *
  146.      * Input is comma-separated list of lines or line ranges.
  147.      *
  148.      * @input string
  149.      * @return array of ints
  150.      */
  151.     protected static function parseHighlightLines( $arg ) {
  152.         $lines = array();
  153.         $values = array_map( 'trim', explode( ',', $arg ) );
  154.         foreach ( $values as $value ) {
  155.             if ( ctype_digit($value) ) {
  156.                 $lines[] = (int) $value;
  157.             } elseif ( strpos( $value, '-' ) !== false ) {
  158.                 list( $start, $end ) = array_map( 'trim', explode( '-', $value ) );
  159.                 if ( self::validHighlightRange( $start, $end ) ) {
  160.                     for ($i = intval( $start ); $i <= $end; $i++ ) {
  161.                         $lines[] = $i;
  162.                     }
  163.                 } else {
  164.                     wfDebugLog( 'geshi', "Invalid range: $value\n" );
  165.                 }
  166.             } else {
  167.                 wfDebugLog( 'geshi', "Invalid line: $value\n" );
  168.             }
  169.         }
  170.         return $lines;
  171.     }
  172.  
  173.     /**
  174.      * Validate a provided input range
  175.      */
  176.     protected static function validHighlightRange( $start, $end ) {
  177.         // Since we're taking this tiny range and producing a an
  178.         // array of every integer between them, it would be trivial
  179.         // to DoS the system by asking for a huge range.
  180.         // Impose an arbitrary limit on the number of lines in a
  181.         // given range to reduce the impact.
  182.         $arbitrarilyLargeConstant = 10000;
  183.         return
  184.             ctype_digit($start) &&
  185.             ctype_digit($end) &&
  186.             $start > 0 &&
  187.             $start < $end &&
  188.             $end - $start < $arbitrarilyLargeConstant;
  189.     }
  190.  
  191.     static function getEncloseType( $args ) {
  192.         // Since version 1.0.8 geshi can produce valid pre, but we need to check for it
  193.         if ( defined('GESHI_HEADER_PRE_VALID') ) {
  194.             $pre = GESHI_HEADER_PRE_VALID;
  195.         } else {
  196.             $pre = GESHI_HEADER_PRE;
  197.         }
  198.  
  199.         // "Enclose" parameter
  200.         $enclose = $pre;
  201.         if ( isset( $args['enclose'] ) ) {
  202.             if ( $args['enclose'] === 'div' ) {
  203.                 $enclose = GESHI_HEADER_DIV;
  204.             } elseif ( $args['enclose'] === 'none' ) {
  205.                 $enclose = GESHI_HEADER_NONE;
  206.             }
  207.         }
  208.  
  209.         if( isset( $args['line'] ) && $pre === GESHI_HEADER_PRE ) {
  210.             // Force <div> mode to maintain valid XHTML, see
  211.             // http://sourceforge.net/tracker/index.php?func=detail&aid=1201963&group_id=114997&atid=670231
  212.             $enclose = GESHI_HEADER_DIV;
  213.         }
  214.  
  215.         return $enclose;
  216.     }
  217.  
  218.     /**
  219.      * Hook into Article::view() to provide syntax highlighting for
  220.      * custom CSS and JavaScript pages
  221.      *
  222.      * @param string $text
  223.      * @param Title $title
  224.      * @param OutputPage $output
  225.      * @return bool
  226.      */
  227.     public static function viewHook( $text, $title, $output ) {
  228.         global $wgUseSiteCSS;
  229.         // Determine the language
  230.         $matches = array();
  231.         preg_match( '!\.(css|js)$!u', $title->getText(), $matches );
  232.         $lang = $matches[1] == 'css' ? 'css' : 'javascript';
  233.         // Attempt to format
  234.         $geshi = self::prepare( $text, $lang );
  235.         if( $geshi instanceof GeSHi ) {
  236.             $out = $geshi->parse_code();
  237.             if( !$geshi->error() ) {
  238.                 // Done
  239.                 $output->addHeadItem( "source-$lang", self::buildHeadItem( $geshi ) );
  240.                 $output->addHTML( "<div dir=\"ltr\">{$out}</div>" );
  241.                 if( $wgUseSiteCss ) {
  242.                                         $output->addModuleStyles( 'ext.geshi.local' );
  243.                                 }
  244.  
  245.             return false;
  246.             }
  247.         }
  248.         // Bottle out
  249.         return true;
  250.     }
  251.  
  252.     /**
  253.      * Initialise a GeSHi object to format some code, performing
  254.      * common setup for all our uses of it
  255.      *
  256.      * @param string $text
  257.      * @param string $lang
  258.      * @return GeSHi
  259.      */
  260.     public static function prepare( $text, $lang ) {
  261.         self::initialise();
  262.         $geshi = new GeSHi( $text, $lang );
  263.         if( $geshi->error() == GESHI_ERROR_NO_SUCH_LANG ) {
  264.             return null;
  265.         }
  266.         $geshi->set_encoding( 'UTF-8' );
  267.         $geshi->enable_classes();
  268.         $geshi->set_overall_class( "source-$lang" );
  269.         $geshi->enable_keyword_links( false );
  270.         return $geshi;
  271.     }
  272.  
  273.  
  274.     /**
  275.      * Format an 'unknown language' error message and append formatted
  276.      * plain text to it.
  277.      *
  278.      * @param string $text
  279.      * @return string HTML fragment
  280.      */
  281.     private static function formatLanguageError( $text ) {
  282.         $error = self::formatError( htmlspecialchars( wfMsgForContent( 'syntaxhighlight-err-language' ) ), $text );
  283.         return $error . '<pre>' . htmlspecialchars( $text ) . '</pre>';
  284.     }
  285.  
  286.     /**
  287.      * Format an error message
  288.      *
  289.      * @param string $error
  290.      * @return string
  291.      */
  292.     private static function formatError( $error = '' ) {
  293.         $html = '';
  294.         if( $error ) {
  295.             $html .= "<p>{$error}</p>";
  296.         }
  297.         $html .= '<p>' . htmlspecialchars( wfMsgForContent( 'syntaxhighlight-specify' ) )
  298.             . ' <samp>&lt;source lang=&quot;html4strict&quot;&gt;...&lt;/source&gt;</samp></p>'
  299.             . '<p>' . htmlspecialchars( wfMsgForContent( 'syntaxhighlight-supported' ) ) . '</p>'
  300.             . self::formatLanguages();
  301.         return "<div style=\"border: solid red 1px; padding: .5em;\">{$html}</div>";
  302.     }
  303.  
  304.     /**
  305.      * Format the list of supported languages
  306.      *
  307.      * @return string
  308.      */
  309.     private static function formatLanguages() {
  310.         $langs = self::getSupportedLanguages();
  311.         $list = array();
  312.         if( count( $langs ) > 0 ) {
  313.             foreach( $langs as $lang ) {
  314.                 $list[] = '<samp>' . htmlspecialchars( $lang ) . '</samp>';
  315.             }
  316.             return '<p class="mw-collapsible mw-collapsed" style="padding: 0em 1em;">' . implode( ', ', $list ) . '</p><br style="clear: all"/>';
  317.         } else {
  318.             return '<p>' . htmlspecialchars( wfMsgForContent( 'syntaxhighlight-err-loading' ) ) . '</p>';
  319.         }
  320.     }
  321.  
  322.     /**
  323.      * Get the list of supported languages
  324.      *
  325.      * @return array
  326.      */
  327.     private static function getSupportedLanguages() {
  328.         if( !is_array( self::$languages ) ) {
  329.             self::initialise();
  330.             self::$languages = array();
  331.             foreach( glob( GESHI_LANG_ROOT . "/*.php" ) as $file ) {
  332.                 self::$languages[] = basename( $file, '.php' );
  333.             }
  334.             sort( self::$languages );
  335.         }
  336.         return self::$languages;
  337.     }
  338.  
  339.     /**
  340.      * Initialise messages and ensure the GeSHi class is loaded
  341.      */
  342.     private static function initialise() {
  343.         if( !self::$initialised ) {
  344.             if( !class_exists( 'GeSHi' ) ) {
  345.                 require( 'geshi/geshi.php' );
  346.             }
  347.             self::$initialised = true;
  348.         }
  349.         return true;
  350.     }
  351.  
  352.     /**
  353.      * Get the GeSHI's version information while Special:Version is read.
  354.      */
  355.     public static function hSpecialVersion_GeSHi( &$extensionTypes ) {
  356.         global $wgExtensionCredits;
  357.         self::initialise();
  358.         $wgExtensionCredits['parserhook']['SyntaxHighlight_GeSHi']['version'] = GESHI_VERSION;
  359.         return true;
  360.     }
  361.  
  362.     /**
  363.      * @see SyntaxHighlight_GeSHi::hSpecialVersion_GeSHi
  364.      */
  365.     public static function hOldSpecialVersion_GeSHi( &$sp, &$extensionTypes ) {
  366.         return self::hSpecialVersion_GeSHi( $extensionTypes );
  367.     }
  368.  
  369.     /**
  370.      * Convert tabs to spaces
  371.      *
  372.      * @param string $text
  373.      * @return string
  374.      */
  375.     private static function tabsToSpaces( $text ) {
  376.         $lines = explode( "\n", $text );
  377.         $lines = array_map( array( __CLASS__, 'tabsToSpacesLine' ), $lines );
  378.         return implode( "\n", $lines );
  379.     }
  380.  
  381.     /**
  382.      * Convert tabs to spaces for a single line
  383.      *
  384.      * @param string $text
  385.      * @return string
  386.      */
  387.     private static function tabsToSpacesLine( $line ) {
  388.         $parts = explode( "\t", $line );
  389.         $width = 8; // To match tidy's config & typical browser defaults
  390.         $out = $parts[0];
  391.         foreach( array_slice( $parts, 1 ) as $chunk ) {
  392.             $spaces = $width - (strlen( $out ) % $width);
  393.             $out .= str_repeat( ' ', $spaces );
  394.             $out .= $chunk;
  395.         }
  396.         return $out;
  397.     }
  398. }
  399.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement