source: spip-zone/_plugins_/coloration_code/trunk/geshi/geshi.php @ 95058

Last change on this file since 95058 was 95058, checked in by marcimat@…, 4 years ago

Mea Culpa dans r93266 : geshi.php avait une petite modification qui n'avait pas été remise ! Corrige https://core.spip.net/issues/3685

File size: 201.3 KB
Line 
1<?php
2/**
3 * GeSHi - Generic Syntax Highlighter
4 *
5 * The GeSHi class for Generic Syntax Highlighting. Please refer to the
6 * documentation at http://qbnz.com/highlighter/documentation.php for more
7 * information about how to use this class.
8 *
9 * For changes, release notes, TODOs etc, see the relevant files in the docs/
10 * directory.
11 *
12 *   This file is part of GeSHi.
13 *
14 *  GeSHi is free software; you can redistribute it and/or modify
15 *  it under the terms of the GNU General Public License as published by
16 *  the Free Software Foundation; either version 2 of the License, or
17 *  (at your option) any later version.
18 *
19 *  GeSHi is distributed in the hope that it will be useful,
20 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 *  GNU General Public License for more details.
23 *
24 *  You should have received a copy of the GNU General Public License
25 *  along with GeSHi; if not, write to the Free Software
26 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
27 *
28 * @package    geshi
29 * @subpackage core
30 * @author     Nigel McNie <nigel@geshi.org>, Benny Baumann <BenBE@omorphia.de>
31 * @copyright  (C) 2004 - 2007 Nigel McNie, (C) 2007 - 2014 Benny Baumann
32 * @license    http://gnu.org/copyleft/gpl.html GNU GPL
33 *
34 */
35
36//
37// GeSHi Constants
38// You should use these constant names in your programs instead of
39// their values - you never know when a value may change in a future
40// version
41//
42
43/** The version of this GeSHi file */
44define('GESHI_VERSION', '1.0.8.12');
45
46// Define the root directory for the GeSHi code tree
47if (!defined('GESHI_ROOT')) {
48    /** The root directory for GeSHi */
49    define('GESHI_ROOT', dirname(__FILE__) . DIRECTORY_SEPARATOR);
50}
51/** The language file directory for GeSHi
52    @access private */
53define('GESHI_LANG_ROOT', GESHI_ROOT . 'geshi' . DIRECTORY_SEPARATOR);
54
55// Define if GeSHi should be paranoid about security
56if (!defined('GESHI_SECURITY_PARANOID')) {
57    /** Tells GeSHi to be paranoid about security settings */
58    define('GESHI_SECURITY_PARANOID', false);
59}
60
61// Line numbers - use with enable_line_numbers()
62/** Use no line numbers when building the result */
63define('GESHI_NO_LINE_NUMBERS', 0);
64/** Use normal line numbers when building the result */
65define('GESHI_NORMAL_LINE_NUMBERS', 1);
66/** Use fancy line numbers when building the result */
67define('GESHI_FANCY_LINE_NUMBERS', 2);
68
69// Container HTML type
70/** Use nothing to surround the source */
71define('GESHI_HEADER_NONE', 0);
72/** Use a "div" to surround the source */
73define('GESHI_HEADER_DIV', 1);
74/** Use a "pre" to surround the source */
75define('GESHI_HEADER_PRE', 2);
76/** Use a pre to wrap lines when line numbers are enabled or to wrap the whole code. */
77define('GESHI_HEADER_PRE_VALID', 3);
78/**
79 * Use a "table" to surround the source:
80 *
81 *  <table>
82 *    <thead><tr><td colspan="2">$header</td></tr></thead>
83 *    <tbody><tr><td><pre>$linenumbers</pre></td><td><pre>$code></pre></td></tr></tbody>
84 *    <tfooter><tr><td colspan="2">$footer</td></tr></tfoot>
85 *  </table>
86 *
87 * this is essentially only a workaround for Firefox, see sf#1651996 or take a look at
88 * https://bugzilla.mozilla.org/show_bug.cgi?id=365805
89 * @note when linenumbers are disabled this is essentially the same as GESHI_HEADER_PRE
90 */
91define('GESHI_HEADER_PRE_TABLE', 4);
92
93// Capatalisation constants
94/** Lowercase keywords found */
95define('GESHI_CAPS_NO_CHANGE', 0);
96/** Uppercase keywords found */
97define('GESHI_CAPS_UPPER', 1);
98/** Leave keywords found as the case that they are */
99define('GESHI_CAPS_LOWER', 2);
100
101// Link style constants
102/** Links in the source in the :link state */
103define('GESHI_LINK', 0);
104/** Links in the source in the :hover state */
105define('GESHI_HOVER', 1);
106/** Links in the source in the :active state */
107define('GESHI_ACTIVE', 2);
108/** Links in the source in the :visited state */
109define('GESHI_VISITED', 3);
110
111// Important string starter/finisher
112// Note that if you change these, they should be as-is: i.e., don't
113// write them as if they had been run through htmlentities()
114/** The starter for important parts of the source */
115define('GESHI_START_IMPORTANT', '<BEGIN GeSHi>');
116/** The ender for important parts of the source */
117define('GESHI_END_IMPORTANT', '<END GeSHi>');
118
119/**#@+
120 *  @access private
121 */
122// When strict mode applies for a language
123/** Strict mode never applies (this is the most common) */
124define('GESHI_NEVER', 0);
125/** Strict mode *might* apply, and can be enabled or
126    disabled by {@link GeSHi->enable_strict_mode()} */
127define('GESHI_MAYBE', 1);
128/** Strict mode always applies */
129define('GESHI_ALWAYS', 2);
130
131// Advanced regexp handling constants, used in language files
132/** The key of the regex array defining what to search for */
133define('GESHI_SEARCH', 0);
134/** The key of the regex array defining what bracket group in a
135    matched search to use as a replacement */
136define('GESHI_REPLACE', 1);
137/** The key of the regex array defining any modifiers to the regular expression */
138define('GESHI_MODIFIERS', 2);
139/** The key of the regex array defining what bracket group in a
140    matched search to put before the replacement */
141define('GESHI_BEFORE', 3);
142/** The key of the regex array defining what bracket group in a
143    matched search to put after the replacement */
144define('GESHI_AFTER', 4);
145/** The key of the regex array defining a custom keyword to use
146    for this regexp's html tag class */
147define('GESHI_CLASS', 5);
148
149/** Used in language files to mark comments */
150define('GESHI_COMMENTS', 0);
151
152/** Used to work around missing PHP features **/
153define('GESHI_PHP_PRE_433', !(version_compare(PHP_VERSION, '4.3.3') === 1));
154
155/** make sure we can call stripos **/
156if (!function_exists('stripos')) {
157    // the offset param of preg_match is not supported below PHP 4.3.3
158    if (GESHI_PHP_PRE_433) {
159        /**
160         * @ignore
161         */
162        function stripos($haystack, $needle, $offset = null) {
163            if (!is_null($offset)) {
164                $haystack = substr($haystack, $offset);
165            }
166            if (preg_match('/'. preg_quote($needle, '/') . '/', $haystack, $match, PREG_OFFSET_CAPTURE)) {
167                return $match[0][1];
168            }
169            return false;
170        }
171    }
172    else {
173        /**
174         * @ignore
175         */
176        function stripos($haystack, $needle, $offset = null) {
177            if (preg_match('/'. preg_quote($needle, '/') . '/', $haystack, $match, PREG_OFFSET_CAPTURE, $offset)) {
178                return $match[0][1];
179            }
180            return false;
181        }
182    }
183}
184
185/** some old PHP / PCRE subpatterns only support up to xxx subpatterns in
186    regular expressions. Set this to false if your PCRE lib is up to date
187    @see GeSHi->optimize_regexp_list()
188    **/
189define('GESHI_MAX_PCRE_SUBPATTERNS', 500);
190/** it's also important not to generate too long regular expressions
191    be generous here... but keep in mind, that when reaching this limit we
192    still have to close open patterns. 12k should do just fine on a 16k limit.
193    @see GeSHi->optimize_regexp_list()
194    **/
195define('GESHI_MAX_PCRE_LENGTH', 12288);
196
197//Number format specification
198/** Basic number format for integers */
199define('GESHI_NUMBER_INT_BASIC', 1);        //Default integers \d+
200/** Enhanced number format for integers like seen in C */
201define('GESHI_NUMBER_INT_CSTYLE', 2);       //Default C-Style \d+[lL]?
202/** Number format to highlight binary numbers with a suffix "b" */
203define('GESHI_NUMBER_BIN_SUFFIX', 16);           //[01]+[bB]
204/** Number format to highlight binary numbers with a prefix % */
205define('GESHI_NUMBER_BIN_PREFIX_PERCENT', 32);   //%[01]+
206/** Number format to highlight binary numbers with a prefix 0b (C) */
207define('GESHI_NUMBER_BIN_PREFIX_0B', 64);        //0b[01]+
208/** Number format to highlight octal numbers with a leading zero */
209define('GESHI_NUMBER_OCT_PREFIX', 256);           //0[0-7]+
210/** Number format to highlight octal numbers with a prefix 0o (logtalk) */
211define('GESHI_NUMBER_OCT_PREFIX_0O', 512);           //0[0-7]+
212/** Number format to highlight octal numbers with a leading @ (Used in HiSofts Devpac series). */
213define('GESHI_NUMBER_OCT_PREFIX_AT', 1024);           //@[0-7]+
214/** Number format to highlight octal numbers with a suffix of o */
215define('GESHI_NUMBER_OCT_SUFFIX', 2048);           //[0-7]+[oO]
216/** Number format to highlight hex numbers with a prefix 0x */
217define('GESHI_NUMBER_HEX_PREFIX', 4096);           //0x[0-9a-fA-F]+
218/** Number format to highlight hex numbers with a prefix $ */
219define('GESHI_NUMBER_HEX_PREFIX_DOLLAR', 8192);           //$[0-9a-fA-F]+
220/** Number format to highlight hex numbers with a suffix of h */
221define('GESHI_NUMBER_HEX_SUFFIX', 16384);           //[0-9][0-9a-fA-F]*h
222/** Number format to highlight floating-point numbers without support for scientific notation */
223define('GESHI_NUMBER_FLT_NONSCI', 65536);          //\d+\.\d+
224/** Number format to highlight floating-point numbers without support for scientific notation */
225define('GESHI_NUMBER_FLT_NONSCI_F', 131072);       //\d+(\.\d+)?f
226/** Number format to highlight floating-point numbers with support for scientific notation (E) and optional leading zero */
227define('GESHI_NUMBER_FLT_SCI_SHORT', 262144);      //\.\d+e\d+
228/** Number format to highlight floating-point numbers with support for scientific notation (E) and required leading digit */
229define('GESHI_NUMBER_FLT_SCI_ZERO', 524288);       //\d+(\.\d+)?e\d+
230//Custom formats are passed by RX array
231
232// Error detection - use these to analyse faults
233/** No sourcecode to highlight was specified
234 * @deprecated
235 */
236define('GESHI_ERROR_NO_INPUT', 1);
237/** The language specified does not exist */
238define('GESHI_ERROR_NO_SUCH_LANG', 2);
239/** GeSHi could not open a file for reading (generally a language file) */
240define('GESHI_ERROR_FILE_NOT_READABLE', 3);
241/** The header type passed to {@link GeSHi->set_header_type()} was invalid */
242define('GESHI_ERROR_INVALID_HEADER_TYPE', 4);
243/** The line number type passed to {@link GeSHi->enable_line_numbers()} was invalid */
244define('GESHI_ERROR_INVALID_LINE_NUMBER_TYPE', 5);
245/**#@-*/
246
247
248/**
249 * The GeSHi Class.
250 *
251 * Please refer to the documentation for GeSHi 1.0.X that is available
252 * at http://qbnz.com/highlighter/documentation.php for more information
253 * about how to use this class.
254 *
255 * @package   geshi
256 * @author    Nigel McNie <nigel@geshi.org>, Benny Baumann <BenBE@omorphia.de>
257 * @copyright (C) 2004 - 2007 Nigel McNie, (C) 2007 - 2014 Benny Baumann
258 */
259class GeSHi {
260    /**#@+
261     * @access private
262     */
263    /**
264     * The source code to highlight
265     * @var string
266     */
267    var $source = '';
268
269    /**
270     * The language to use when highlighting
271     * @var string
272     */
273    var $language = '';
274
275    /**
276     * The data for the language used
277     * @var array
278     */
279    var $language_data = array();
280
281    /**
282     * The path to the language files
283     * @var string
284     */
285    var $language_path = GESHI_LANG_ROOT;
286
287    /**
288     * The error message associated with an error
289     * @var string
290     * @todo check err reporting works
291     */
292    var $error = false;
293
294    /**
295     * Possible error messages
296     * @var array
297     */
298    var $error_messages = array(
299        GESHI_ERROR_NO_SUCH_LANG => 'GeSHi could not find the language {LANGUAGE} (using path {PATH})',
300        GESHI_ERROR_FILE_NOT_READABLE => 'The file specified for load_from_file was not readable',
301        GESHI_ERROR_INVALID_HEADER_TYPE => 'The header type specified is invalid',
302        GESHI_ERROR_INVALID_LINE_NUMBER_TYPE => 'The line number type specified is invalid'
303    );
304
305    /**
306     * Whether highlighting is strict or not
307     * @var boolean
308     */
309    var $strict_mode = false;
310
311    /**
312     * Whether to use CSS classes in output
313     * @var boolean
314     */
315    var $use_classes = false;
316
317    /**
318     * The type of header to use. Can be one of the following
319     * values:
320     *
321     * - GESHI_HEADER_PRE: Source is outputted in a "pre" HTML element.
322     * - GESHI_HEADER_DIV: Source is outputted in a "div" HTML element.
323     * - GESHI_HEADER_NONE: No header is outputted.
324     *
325     * @var int
326     */
327    var $header_type = GESHI_HEADER_PRE;
328
329    /**
330     * Array of permissions for which lexics should be highlighted
331     * @var array
332     */
333    var $lexic_permissions = array(
334        'KEYWORDS' =>    array(),
335        'COMMENTS' =>    array('MULTI' => true),
336        'REGEXPS' =>     array(),
337        'ESCAPE_CHAR' => true,
338        'BRACKETS' =>    true,
339        'SYMBOLS' =>     false,
340        'STRINGS' =>     true,
341        'NUMBERS' =>     true,
342        'METHODS' =>     true,
343        'SCRIPT' =>      true
344    );
345
346    /**
347     * The time it took to parse the code
348     * @var double
349     */
350    var $time = 0;
351
352    /**
353     * The content of the header block
354     * @var string
355     */
356    var $header_content = '';
357
358    /**
359     * The content of the footer block
360     * @var string
361     */
362    var $footer_content = '';
363
364    /**
365     * The style of the header block
366     * @var string
367     */
368    var $header_content_style = '';
369
370    /**
371     * The style of the footer block
372     * @var string
373     */
374    var $footer_content_style = '';
375
376    /**
377     * Tells if a block around the highlighted source should be forced
378     * if not using line numbering
379     * @var boolean
380     */
381    var $force_code_block = false;
382
383    /**
384     * The styles for hyperlinks in the code
385     * @var array
386     */
387    var $link_styles = array();
388
389    /**
390     * Whether important blocks should be recognised or not
391     * @var boolean
392     * @deprecated
393     * @todo REMOVE THIS FUNCTIONALITY!
394     */
395    var $enable_important_blocks = false;
396
397    /**
398     * Styles for important parts of the code
399     * @var string
400     * @deprecated
401     * @todo As above - rethink the whole idea of important blocks as it is buggy and
402     * will be hard to implement in 1.2
403     */
404    var $important_styles = 'font-weight: bold; color: red;'; // Styles for important parts of the code
405
406    /**
407     * Whether CSS IDs should be added to the code
408     * @var boolean
409     */
410    var $add_ids = false;
411
412    /**
413     * Lines that should be highlighted extra
414     * @var array
415     */
416    var $highlight_extra_lines = array();
417
418    /**
419     * Styles of lines that should be highlighted extra
420     * @var array
421     */
422    var $highlight_extra_lines_styles = array();
423
424    /**
425     * Styles of extra-highlighted lines
426     * @var string
427     */
428    var $highlight_extra_lines_style = 'background-color: #ffc;';
429
430    /**
431     * The line ending
432     * If null, nl2br() will be used on the result string.
433     * Otherwise, all instances of \n will be replaced with $line_ending
434     * @var string
435     */
436    var $line_ending = null;
437
438    /**
439     * Number at which line numbers should start at
440     * @var int
441     */
442    var $line_numbers_start = 1;
443
444    /**
445     * The overall style for this code block
446     * @var string
447     */
448    var $overall_style = 'font-family:monospace;';
449
450    /**
451     *  The style for the actual code
452     * @var string
453     */
454    var $code_style = 'font: normal normal 1em/1.2em monospace; margin:0; padding:0; background:none; vertical-align:top;';
455
456    /**
457     * The overall class for this code block
458     * @var string
459     */
460    var $overall_class = '';
461
462    /**
463     * The overall ID for this code block
464     * @var string
465     */
466    var $overall_id = '';
467
468    /**
469     * Line number styles
470     * @var string
471     */
472    var $line_style1 = 'font-weight: normal; vertical-align:top;';
473
474    /**
475     * Line number styles for fancy lines
476     * @var string
477     */
478    var $line_style2 = 'font-weight: bold; vertical-align:top;';
479
480    /**
481     * Style for line numbers when GESHI_HEADER_PRE_TABLE is chosen
482     * @var string
483     */
484    var $table_linenumber_style = 'width:1px;text-align:right;margin:0;padding:0 2px;vertical-align:top;';
485
486    /**
487     * Flag for how line numbers are displayed
488     * @var boolean
489     */
490    var $line_numbers = GESHI_NO_LINE_NUMBERS;
491
492    /**
493     * Flag to decide if multi line spans are allowed. Set it to false to make sure
494     * each tag is closed before and reopened after each linefeed.
495     * @var boolean
496     */
497    var $allow_multiline_span = true;
498
499    /**
500     * The "nth" value for fancy line highlighting
501     * @var int
502     */
503    var $line_nth_row = 0;
504
505    /**
506     * The size of tab stops
507     * @var int
508     */
509    var $tab_width = 8;
510
511    /**
512     * Should we use language-defined tab stop widths?
513     * @var int
514     */
515    var $use_language_tab_width = false;
516
517    /**
518     * Default target for keyword links
519     * @var string
520     */
521    var $link_target = '';
522
523    /**
524     * The encoding to use for entity encoding
525     * NOTE: Used with Escape Char Sequences to fix UTF-8 handling (cf. SF#2037598)
526     * @var string
527     */
528    var $encoding = 'utf-8';
529
530    /**
531     * Should keywords be linked?
532     * @var boolean
533     */
534    var $keyword_links = true;
535
536    /**
537     * Currently loaded language file
538     * @var string
539     * @since 1.0.7.22
540     */
541    var $loaded_language = '';
542
543    /**
544     * Wether the caches needed for parsing are built or not
545     *
546     * @var bool
547     * @since 1.0.8
548     */
549    var $parse_cache_built = false;
550
551    /**
552     * Work around for Suhosin Patch with disabled /e modifier
553     *
554     * Note from suhosins author in config file:
555     * <blockquote>
556     *   The /e modifier inside <code>preg_replace()</code> allows code execution.
557     *   Often it is the cause for remote code execution exploits. It is wise to
558     *   deactivate this feature and test where in the application it is used.
559     *   The developer using the /e modifier should be made aware that he should
560     *   use <code>preg_replace_callback()</code> instead
561     * </blockquote>
562     *
563     * @var array
564     * @since 1.0.8
565     */
566    var $_kw_replace_group = 0;
567    var $_rx_key = 0;
568
569    /**
570     * some "callback parameters" for handle_multiline_regexps
571     *
572     * @since 1.0.8
573     * @access private
574     * @var string
575     */
576    var $_hmr_before = '';
577    var $_hmr_replace = '';
578    var $_hmr_after = '';
579    var $_hmr_key = 0;
580
581    /**#@-*/
582
583    /**
584     * Creates a new GeSHi object, with source and language
585     *
586     * @param string The source code to highlight
587     * @param string The language to highlight the source with
588     * @param string The path to the language file directory. <b>This
589     *               is deprecated!</b> I've backported the auto path
590     *               detection from the 1.1.X dev branch, so now it
591     *               should be automatically set correctly. If you have
592     *               renamed the language directory however, you will
593     *               still need to set the path using this parameter or
594     *               {@link GeSHi->set_language_path()}
595     * @since 1.0.0
596     */
597    function __construct($source = '', $language = '', $path = '') {
598        if (!empty($source)) {
599            $this->set_source($source);
600        }
601        if (!empty($language)) {
602            $this->set_language($language);
603        }
604        $this->set_language_path($path);
605    }
606
607    /**
608     * Returns the version of GeSHi
609     *
610     * @return string
611     * @since 1 0.8.11
612     */
613    function get_version()
614    {
615        return GESHI_VERSION;
616    }
617
618    /**
619     * Returns an error message associated with the last GeSHi operation,
620     * or false if no error has occurred
621     *
622     * @return string|false An error message if there has been an error, else false
623     * @since  1.0.0
624     */
625    function error() {
626        if ($this->error) {
627            //Put some template variables for debugging here ...
628            $debug_tpl_vars = array(
629                '{LANGUAGE}' => $this->language,
630                '{PATH}' => $this->language_path
631            );
632            $msg = str_replace(
633                array_keys($debug_tpl_vars),
634                array_values($debug_tpl_vars),
635                $this->error_messages[$this->error]);
636
637            return "<br /><strong>GeSHi Error:</strong> $msg (code {$this->error})<br />";
638        }
639        return false;
640    }
641
642    /**
643     * Gets a human-readable language name (thanks to Simon Patterson
644     * for the idea :))
645     *
646     * @return string The name for the current language
647     * @since  1.0.2
648     */
649    function get_language_name() {
650        if (GESHI_ERROR_NO_SUCH_LANG == $this->error) {
651            return $this->language_data['LANG_NAME'] . ' (Unknown Language)';
652        }
653        return $this->language_data['LANG_NAME'];
654    }
655
656    /**
657     * Sets the source code for this object
658     *
659     * @param string The source code to highlight
660     * @since 1.0.0
661     */
662    function set_source($source) {
663        $this->source = $source;
664        $this->highlight_extra_lines = array();
665    }
666
667    /**
668     * Sets the language for this object
669     *
670     * @note since 1.0.8 this function won't reset language-settings by default anymore!
671     *       if you need this set $force_reset = true
672     *
673     * @param string The name of the language to use
674     * @since 1.0.0
675     */
676    function set_language($language, $force_reset = false) {
677        if ($force_reset) {
678            $this->loaded_language = false;
679        }
680
681        //Clean up the language name to prevent malicious code injection
682        $language = preg_replace('#[^a-zA-Z0-9\-_]#', '', $language);
683
684        $language = strtolower($language);
685
686        //Retreive the full filename
687        $file_name = $this->language_path . $language . '.php';
688        if ($file_name == $this->loaded_language) {
689            // this language is already loaded!
690            return;
691        }
692
693        $this->language = $language;
694
695        $this->error = false;
696        $this->strict_mode = GESHI_NEVER;
697
698        //Check if we can read the desired file
699        if (!is_readable($file_name)) {
700            $this->error = GESHI_ERROR_NO_SUCH_LANG;
701            return;
702        }
703
704        // Load the language for parsing
705        $this->load_language($file_name);
706    }
707
708    /**
709     * Sets the path to the directory containing the language files. Note
710     * that this path is relative to the directory of the script that included
711     * geshi.php, NOT geshi.php itself.
712     *
713     * @param string The path to the language directory
714     * @since 1.0.0
715     * @deprecated The path to the language files should now be automatically
716     *             detected, so this method should no longer be needed. The
717     *             1.1.X branch handles manual setting of the path differently
718     *             so this method will disappear in 1.2.0.
719     */
720    function set_language_path($path) {
721        if(strpos($path,':')) {
722            //Security Fix to prevent external directories using fopen wrappers.
723            if(DIRECTORY_SEPARATOR == "\\") {
724                if(!preg_match('#^[a-zA-Z]:#', $path) || false !== strpos($path, ':', 2)) {
725                    return;
726                }
727            } else {
728                return;
729            }
730        }
731        if(preg_match('#[^/a-zA-Z0-9_\.\-\\\s:]#', $path)) {
732            //Security Fix to prevent external directories using fopen wrappers.
733            return;
734        }
735        if(GESHI_SECURITY_PARANOID && false !== strpos($path, '/.')) {
736            //Security Fix to prevent external directories using fopen wrappers.
737            return;
738        }
739        if(GESHI_SECURITY_PARANOID && false !== strpos($path, '..')) {
740            //Security Fix to prevent external directories using fopen wrappers.
741            return;
742        }
743        if ($path) {
744            $this->language_path = ('/' == $path[strlen($path) - 1]) ? $path : $path . '/';
745            $this->set_language($this->language); // otherwise set_language_path has no effect
746        }
747    }
748
749    /**
750     * Get supported langs or an associative array lang=>full_name.
751     * @param boolean $longnames
752     * @return array
753     */
754    function get_supported_languages($full_names=false)
755    {
756        // return array
757        $back = array();
758
759        // we walk the lang root
760        $dir = dir($this->language_path);
761
762        // foreach entry
763        while (false !== ($entry = $dir->read()))
764        {
765            $full_path = $this->language_path.$entry;
766
767            // Skip all dirs
768            if (is_dir($full_path)) {
769                continue;
770            }
771
772            // we only want lang.php files
773            if (!preg_match('/^([^.]+)\.php$/', $entry, $matches)) {
774                continue;
775            }
776
777            // Raw lang name is here
778            $langname = $matches[1];
779
780            // We want the fullname too?
781            if ($full_names === true)
782            {
783                if (false !== ($fullname = $this->get_language_fullname($langname)))
784                {
785                    $back[$langname] = $fullname; // we go associative
786                }
787            }
788            else
789            {
790                // just store raw langname
791                $back[] = $langname;
792            }
793        }
794
795        $dir->close();
796
797        return $back;
798    }
799
800    /**
801     * Get full_name for a lang or false.
802     * @param string $language short langname (html4strict for example)
803     * @return mixed
804     */
805    function get_language_fullname($language)
806    {
807        //Clean up the language name to prevent malicious code injection
808        $language = preg_replace('#[^a-zA-Z0-9\-_]#', '', $language);
809
810        $language = strtolower($language);
811
812        // get fullpath-filename for a langname
813        $fullpath = $this->language_path.$language.'.php';
814
815        // we need to get contents :S
816        if (false === ($data = file_get_contents($fullpath))) {
817            $this->error = sprintf('Geshi::get_lang_fullname() Unknown Language: %s', $language);
818            return false;
819        }
820
821        // match the langname
822        if (!preg_match('/\'LANG_NAME\'\s*=>\s*\'((?:[^\']|\\\')+?)\'/', $data, $matches)) {
823            $this->error = sprintf('Geshi::get_lang_fullname(%s): Regex can not detect language', $language);
824            return false;
825        }
826
827        // return fullname for langname
828        return stripcslashes($matches[1]);
829    }
830
831    /**
832     * Sets the type of header to be used.
833     *
834     * If GESHI_HEADER_DIV is used, the code is surrounded in a "div".This
835     * means more source code but more control over tab width and line-wrapping.
836     * GESHI_HEADER_PRE means that a "pre" is used - less source, but less
837     * control. Default is GESHI_HEADER_PRE.
838     *
839     * From 1.0.7.2, you can use GESHI_HEADER_NONE to specify that no header code
840     * should be outputted.
841     *
842     * @param int The type of header to be used
843     * @since 1.0.0
844     */
845    function set_header_type($type) {
846        //Check if we got a valid header type
847        if (!in_array($type, array(GESHI_HEADER_NONE, GESHI_HEADER_DIV,
848            GESHI_HEADER_PRE, GESHI_HEADER_PRE_VALID, GESHI_HEADER_PRE_TABLE))) {
849            $this->error = GESHI_ERROR_INVALID_HEADER_TYPE;
850            return;
851        }
852
853        //Set that new header type
854        $this->header_type = $type;
855    }
856
857    /**
858     * Sets the styles for the code that will be outputted
859     * when this object is parsed. The style should be a
860     * string of valid stylesheet declarations
861     *
862     * @param string  The overall style for the outputted code block
863     * @param boolean Whether to merge the styles with the current styles or not
864     * @since 1.0.0
865     */
866    function set_overall_style($style, $preserve_defaults = false) {
867        if (!$preserve_defaults) {
868            $this->overall_style = $style;
869        } else {
870            $this->overall_style .= $style;
871        }
872    }
873
874    /**
875     * Sets the overall classname for this block of code. This
876     * class can then be used in a stylesheet to style this object's
877     * output
878     *
879     * @param string The class name to use for this block of code
880     * @since 1.0.0
881     */
882    function set_overall_class($class) {
883        $this->overall_class = $class;
884    }
885
886    /**
887     * Sets the overall id for this block of code. This id can then
888     * be used in a stylesheet to style this object's output
889     *
890     * @param string The ID to use for this block of code
891     * @since 1.0.0
892     */
893    function set_overall_id($id) {
894        $this->overall_id = $id;
895    }
896
897    /**
898     * Sets whether CSS classes should be used to highlight the source. Default
899     * is off, calling this method with no arguments will turn it on
900     *
901     * @param boolean Whether to turn classes on or not
902     * @since 1.0.0
903     */
904    function enable_classes($flag = true) {
905        $this->use_classes = ($flag) ? true : false;
906    }
907
908    /**
909     * Sets the style for the actual code. This should be a string
910     * containing valid stylesheet declarations. If $preserve_defaults is
911     * true, then styles are merged with the default styles, with the
912     * user defined styles having priority
913     *
914     * Note: Use this method to override any style changes you made to
915     * the line numbers if you are using line numbers, else the line of
916     * code will have the same style as the line number! Consult the
917     * GeSHi documentation for more information about this.
918     *
919     * @param string  The style to use for actual code
920     * @param boolean Whether to merge the current styles with the new styles
921     * @since 1.0.2
922     */
923    function set_code_style($style, $preserve_defaults = false) {
924        if (!$preserve_defaults) {
925            $this->code_style = $style;
926        } else {
927            $this->code_style .= $style;
928        }
929    }
930
931    /**
932     * Sets the styles for the line numbers.
933     *
934     * @param string The style for the line numbers that are "normal"
935     * @param string|boolean If a string, this is the style of the line
936     *        numbers that are "fancy", otherwise if boolean then this
937     *        defines whether the normal styles should be merged with the
938     *        new normal styles or not
939     * @param boolean If set, is the flag for whether to merge the "fancy"
940     *        styles with the current styles or not
941     * @since 1.0.2
942     */
943    function set_line_style($style1, $style2 = '', $preserve_defaults = false) {
944        //Check if we got 2 or three parameters
945        if (is_bool($style2)) {
946            $preserve_defaults = $style2;
947            $style2 = '';
948        }
949
950        //Actually set the new styles
951        if (!$preserve_defaults) {
952            $this->line_style1 = $style1;
953            $this->line_style2 = $style2;
954        } else {
955            $this->line_style1 .= $style1;
956            $this->line_style2 .= $style2;
957        }
958    }
959
960    /**
961     * Sets whether line numbers should be displayed.
962     *
963     * Valid values for the first parameter are:
964     *
965     *  - GESHI_NO_LINE_NUMBERS: Line numbers will not be displayed
966     *  - GESHI_NORMAL_LINE_NUMBERS: Line numbers will be displayed
967     *  - GESHI_FANCY_LINE_NUMBERS: Fancy line numbers will be displayed
968     *
969     * For fancy line numbers, the second parameter is used to signal which lines
970     * are to be fancy. For example, if the value of this parameter is 5 then every
971     * 5th line will be fancy.
972     *
973     * @param int How line numbers should be displayed
974     * @param int Defines which lines are fancy
975     * @since 1.0.0
976     */
977    function enable_line_numbers($flag, $nth_row = 5) {
978        if (GESHI_NO_LINE_NUMBERS != $flag && GESHI_NORMAL_LINE_NUMBERS != $flag
979            && GESHI_FANCY_LINE_NUMBERS != $flag) {
980            $this->error = GESHI_ERROR_INVALID_LINE_NUMBER_TYPE;
981        }
982        $this->line_numbers = $flag;
983        $this->line_nth_row = $nth_row;
984    }
985
986    /**
987     * Sets wether spans and other HTML markup generated by GeSHi can
988     * span over multiple lines or not. Defaults to true to reduce overhead.
989     * Set it to false if you want to manipulate the output or manually display
990     * the code in an ordered list.
991     *
992     * @param boolean Wether multiline spans are allowed or not
993     * @since 1.0.7.22
994     */
995    function enable_multiline_span($flag) {
996        $this->allow_multiline_span = (bool) $flag;
997    }
998
999    /**
1000     * Get current setting for multiline spans, see GeSHi->enable_multiline_span().
1001     *
1002     * @see enable_multiline_span
1003     * @return bool
1004     */
1005    function get_multiline_span() {
1006        return $this->allow_multiline_span;
1007    }
1008
1009    /**
1010     * Sets the style for a keyword group. If $preserve_defaults is
1011     * true, then styles are merged with the default styles, with the
1012     * user defined styles having priority
1013     *
1014     * @param int     The key of the keyword group to change the styles of
1015     * @param string  The style to make the keywords
1016     * @param boolean Whether to merge the new styles with the old or just
1017     *                to overwrite them
1018     * @since 1.0.0
1019     */
1020    function set_keyword_group_style($key, $style, $preserve_defaults = false) {
1021        //Set the style for this keyword group
1022        if('*' == $key) {
1023            foreach($this->language_data['STYLES']['KEYWORDS'] as $_key => $_value) {
1024                if (!$preserve_defaults) {
1025                    $this->language_data['STYLES']['KEYWORDS'][$_key] = $style;
1026                } else {
1027                    $this->language_data['STYLES']['KEYWORDS'][$_key] .= $style;
1028                }
1029            }
1030        } else {
1031            if (!$preserve_defaults) {
1032                $this->language_data['STYLES']['KEYWORDS'][$key] = $style;
1033            } else {
1034                $this->language_data['STYLES']['KEYWORDS'][$key] .= $style;
1035            }
1036        }
1037
1038        //Update the lexic permissions
1039        if (!isset($this->lexic_permissions['KEYWORDS'][$key])) {
1040            $this->lexic_permissions['KEYWORDS'][$key] = true;
1041        }
1042    }
1043
1044    /**
1045     * Turns highlighting on/off for a keyword group
1046     *
1047     * @param int     The key of the keyword group to turn on or off
1048     * @param boolean Whether to turn highlighting for that group on or off
1049     * @since 1.0.0
1050     */
1051    function set_keyword_group_highlighting($key, $flag = true) {
1052        $this->lexic_permissions['KEYWORDS'][$key] = ($flag) ? true : false;
1053    }
1054
1055    /**
1056     * Sets the styles for comment groups.  If $preserve_defaults is
1057     * true, then styles are merged with the default styles, with the
1058     * user defined styles having priority
1059     *
1060     * @param int     The key of the comment group to change the styles of
1061     * @param string  The style to make the comments
1062     * @param boolean Whether to merge the new styles with the old or just
1063     *                to overwrite them
1064     * @since 1.0.0
1065     */
1066    function set_comments_style($key, $style, $preserve_defaults = false) {
1067        if('*' == $key) {
1068            foreach($this->language_data['STYLES']['COMMENTS'] as $_key => $_value) {
1069                if (!$preserve_defaults) {
1070                    $this->language_data['STYLES']['COMMENTS'][$_key] = $style;
1071                } else {
1072                    $this->language_data['STYLES']['COMMENTS'][$_key] .= $style;
1073                }
1074            }
1075        } else {
1076            if (!$preserve_defaults) {
1077                $this->language_data['STYLES']['COMMENTS'][$key] = $style;
1078            } else {
1079                $this->language_data['STYLES']['COMMENTS'][$key] .= $style;
1080            }
1081        }
1082    }
1083
1084    /**
1085     * Turns highlighting on/off for comment groups
1086     *
1087     * @param int     The key of the comment group to turn on or off
1088     * @param boolean Whether to turn highlighting for that group on or off
1089     * @since 1.0.0
1090     */
1091    function set_comments_highlighting($key, $flag = true) {
1092        $this->lexic_permissions['COMMENTS'][$key] = ($flag) ? true : false;
1093    }
1094
1095    /**
1096     * Sets the styles for escaped characters. If $preserve_defaults is
1097     * true, then styles are merged with the default styles, with the
1098     * user defined styles having priority
1099     *
1100     * @param string  The style to make the escape characters
1101     * @param boolean Whether to merge the new styles with the old or just
1102     *                to overwrite them
1103     * @since 1.0.0
1104     */
1105    function set_escape_characters_style($style, $preserve_defaults = false, $group = 0) {
1106        if (!$preserve_defaults) {
1107            $this->language_data['STYLES']['ESCAPE_CHAR'][$group] = $style;
1108        } else {
1109            $this->language_data['STYLES']['ESCAPE_CHAR'][$group] .= $style;
1110        }
1111    }
1112
1113    /**
1114     * Turns highlighting on/off for escaped characters
1115     *
1116     * @param boolean Whether to turn highlighting for escape characters on or off
1117     * @since 1.0.0
1118     */
1119    function set_escape_characters_highlighting($flag = true) {
1120        $this->lexic_permissions['ESCAPE_CHAR'] = ($flag) ? true : false;
1121    }
1122
1123    /**
1124     * Sets the styles for brackets. If $preserve_defaults is
1125     * true, then styles are merged with the default styles, with the
1126     * user defined styles having priority
1127     *
1128     * This method is DEPRECATED: use set_symbols_style instead.
1129     * This method will be removed in 1.2.X
1130     *
1131     * @param string  The style to make the brackets
1132     * @param boolean Whether to merge the new styles with the old or just
1133     *                to overwrite them
1134     * @since 1.0.0
1135     * @deprecated In favour of set_symbols_style
1136     */
1137    function set_brackets_style($style, $preserve_defaults = false) {
1138        if (!$preserve_defaults) {
1139            $this->language_data['STYLES']['BRACKETS'][0] = $style;
1140        } else {
1141            $this->language_data['STYLES']['BRACKETS'][0] .= $style;
1142        }
1143    }
1144
1145    /**
1146     * Turns highlighting on/off for brackets
1147     *
1148     * This method is DEPRECATED: use set_symbols_highlighting instead.
1149     * This method will be remove in 1.2.X
1150     *
1151     * @param boolean Whether to turn highlighting for brackets on or off
1152     * @since 1.0.0
1153     * @deprecated In favour of set_symbols_highlighting
1154     */
1155    function set_brackets_highlighting($flag) {
1156        $this->lexic_permissions['BRACKETS'] = ($flag) ? true : false;
1157    }
1158
1159    /**
1160     * Sets the styles for symbols. If $preserve_defaults is
1161     * true, then styles are merged with the default styles, with the
1162     * user defined styles having priority
1163     *
1164     * @param string  The style to make the symbols
1165     * @param boolean Whether to merge the new styles with the old or just
1166     *                to overwrite them
1167     * @param int     Tells the group of symbols for which style should be set.
1168     * @since 1.0.1
1169     */
1170    function set_symbols_style($style, $preserve_defaults = false, $group = 0) {
1171        // Update the style of symbols
1172        if (!$preserve_defaults) {
1173            $this->language_data['STYLES']['SYMBOLS'][$group] = $style;
1174        } else {
1175            $this->language_data['STYLES']['SYMBOLS'][$group] .= $style;
1176        }
1177
1178        // For backward compatibility
1179        if (0 == $group) {
1180            $this->set_brackets_style ($style, $preserve_defaults);
1181        }
1182    }
1183
1184    /**
1185     * Turns highlighting on/off for symbols
1186     *
1187     * @param boolean Whether to turn highlighting for symbols on or off
1188     * @since 1.0.0
1189     */
1190    function set_symbols_highlighting($flag) {
1191        // Update lexic permissions for this symbol group
1192        $this->lexic_permissions['SYMBOLS'] = ($flag) ? true : false;
1193
1194        // For backward compatibility
1195        $this->set_brackets_highlighting ($flag);
1196    }
1197
1198    /**
1199     * Sets the styles for strings. If $preserve_defaults is
1200     * true, then styles are merged with the default styles, with the
1201     * user defined styles having priority
1202     *
1203     * @param string  The style to make the escape characters
1204     * @param boolean Whether to merge the new styles with the old or just
1205     *                to overwrite them
1206     * @param int     Tells the group of strings for which style should be set.
1207     * @since 1.0.0
1208     */
1209    function set_strings_style($style, $preserve_defaults = false, $group = 0) {
1210        if (!$preserve_defaults) {
1211            $this->language_data['STYLES']['STRINGS'][$group] = $style;
1212        } else {
1213            $this->language_data['STYLES']['STRINGS'][$group] .= $style;
1214        }
1215    }
1216
1217    /**
1218     * Turns highlighting on/off for strings
1219     *
1220     * @param boolean Whether to turn highlighting for strings on or off
1221     * @since 1.0.0
1222     */
1223    function set_strings_highlighting($flag) {
1224        $this->lexic_permissions['STRINGS'] = ($flag) ? true : false;
1225    }
1226
1227    /**
1228     * Sets the styles for strict code blocks. If $preserve_defaults is
1229     * true, then styles are merged with the default styles, with the
1230     * user defined styles having priority
1231     *
1232     * @param string  The style to make the script blocks
1233     * @param boolean Whether to merge the new styles with the old or just
1234     *                to overwrite them
1235     * @param int     Tells the group of script blocks for which style should be set.
1236     * @since 1.0.8.4
1237     */
1238    function set_script_style($style, $preserve_defaults = false, $group = 0) {
1239        // Update the style of symbols
1240        if (!$preserve_defaults) {
1241            $this->language_data['STYLES']['SCRIPT'][$group] = $style;
1242        } else {
1243            $this->language_data['STYLES']['SCRIPT'][$group] .= $style;
1244        }
1245    }
1246
1247    /**
1248     * Sets the styles for numbers. If $preserve_defaults is
1249     * true, then styles are merged with the default styles, with the
1250     * user defined styles having priority
1251     *
1252     * @param string  The style to make the numbers
1253     * @param boolean Whether to merge the new styles with the old or just
1254     *                to overwrite them
1255     * @param int     Tells the group of numbers for which style should be set.
1256     * @since 1.0.0
1257     */
1258    function set_numbers_style($style, $preserve_defaults = false, $group = 0) {
1259        if (!$preserve_defaults) {
1260            $this->language_data['STYLES']['NUMBERS'][$group] = $style;
1261        } else {
1262            $this->language_data['STYLES']['NUMBERS'][$group] .= $style;
1263        }
1264    }
1265
1266    /**
1267     * Turns highlighting on/off for numbers
1268     *
1269     * @param boolean Whether to turn highlighting for numbers on or off
1270     * @since 1.0.0
1271     */
1272    function set_numbers_highlighting($flag) {
1273        $this->lexic_permissions['NUMBERS'] = ($flag) ? true : false;
1274    }
1275
1276    /**
1277     * Sets the styles for methods. $key is a number that references the
1278     * appropriate "object splitter" - see the language file for the language
1279     * you are highlighting to get this number. If $preserve_defaults is
1280     * true, then styles are merged with the default styles, with the
1281     * user defined styles having priority
1282     *
1283     * @param int     The key of the object splitter to change the styles of
1284     * @param string  The style to make the methods
1285     * @param boolean Whether to merge the new styles with the old or just
1286     *                to overwrite them
1287     * @since 1.0.0
1288     */
1289    function set_methods_style($key, $style, $preserve_defaults = false) {
1290        if (!$preserve_defaults) {
1291            $this->language_data['STYLES']['METHODS'][$key] = $style;
1292        } else {
1293            $this->language_data['STYLES']['METHODS'][$key] .= $style;
1294        }
1295    }
1296
1297    /**
1298     * Turns highlighting on/off for methods
1299     *
1300     * @param boolean Whether to turn highlighting for methods on or off
1301     * @since 1.0.0
1302     */
1303    function set_methods_highlighting($flag) {
1304        $this->lexic_permissions['METHODS'] = ($flag) ? true : false;
1305    }
1306
1307    /**
1308     * Sets the styles for regexps. If $preserve_defaults is
1309     * true, then styles are merged with the default styles, with the
1310     * user defined styles having priority
1311     *
1312     * @param string  The style to make the regular expression matches
1313     * @param boolean Whether to merge the new styles with the old or just
1314     *                to overwrite them
1315     * @since 1.0.0
1316     */
1317    function set_regexps_style($key, $style, $preserve_defaults = false) {
1318        if (!$preserve_defaults) {
1319            $this->language_data['STYLES']['REGEXPS'][$key] = $style;
1320        } else {
1321            $this->language_data['STYLES']['REGEXPS'][$key] .= $style;
1322        }
1323    }
1324
1325    /**
1326     * Turns highlighting on/off for regexps
1327     *
1328     * @param int     The key of the regular expression group to turn on or off
1329     * @param boolean Whether to turn highlighting for the regular expression group on or off
1330     * @since 1.0.0
1331     */
1332    function set_regexps_highlighting($key, $flag) {
1333        $this->lexic_permissions['REGEXPS'][$key] = ($flag) ? true : false;
1334    }
1335
1336    /**
1337     * Sets whether a set of keywords are checked for in a case sensitive manner
1338     *
1339     * @param int The key of the keyword group to change the case sensitivity of
1340     * @param boolean Whether to check in a case sensitive manner or not
1341     * @since 1.0.0
1342     */
1343    function set_case_sensitivity($key, $case) {
1344        $this->language_data['CASE_SENSITIVE'][$key] = ($case) ? true : false;
1345    }
1346
1347    /**
1348     * Sets the case that keywords should use when found. Use the constants:
1349     *
1350     *  - GESHI_CAPS_NO_CHANGE: leave keywords as-is
1351     *  - GESHI_CAPS_UPPER: convert all keywords to uppercase where found
1352     *  - GESHI_CAPS_LOWER: convert all keywords to lowercase where found
1353     *
1354     * @param int A constant specifying what to do with matched keywords
1355     * @since 1.0.1
1356     */
1357    function set_case_keywords($case) {
1358        if (in_array($case, array(
1359            GESHI_CAPS_NO_CHANGE, GESHI_CAPS_UPPER, GESHI_CAPS_LOWER))) {
1360            $this->language_data['CASE_KEYWORDS'] = $case;
1361        }
1362    }
1363
1364    /**
1365     * Sets how many spaces a tab is substituted for
1366     *
1367     * Widths below zero are ignored
1368     *
1369     * @param int The tab width
1370     * @since 1.0.0
1371     */
1372    function set_tab_width($width) {
1373        $this->tab_width = intval($width);
1374
1375        //Check if it fit's the constraints:
1376        if ($this->tab_width < 1) {
1377            //Return it to the default
1378            $this->tab_width = 8;
1379        }
1380    }
1381
1382    /**
1383     * Sets whether or not to use tab-stop width specifed by language
1384     *
1385     * @param boolean Whether to use language-specific tab-stop widths
1386     * @since 1.0.7.20
1387     */
1388    function set_use_language_tab_width($use) {
1389        $this->use_language_tab_width = (bool) $use;
1390    }
1391
1392    /**
1393     * Returns the tab width to use, based on the current language and user
1394     * preference
1395     *
1396     * @return int Tab width
1397     * @since 1.0.7.20
1398     */
1399    function get_real_tab_width() {
1400        if (!$this->use_language_tab_width ||
1401            !isset($this->language_data['TAB_WIDTH'])) {
1402            return $this->tab_width;
1403        } else {
1404            return $this->language_data['TAB_WIDTH'];
1405        }
1406    }
1407
1408    /**
1409     * Enables/disables strict highlighting. Default is off, calling this
1410     * method without parameters will turn it on. See documentation
1411     * for more details on strict mode and where to use it.
1412     *
1413     * @param boolean Whether to enable strict mode or not
1414     * @since 1.0.0
1415     */
1416    function enable_strict_mode($mode = true) {
1417        if (GESHI_MAYBE == $this->language_data['STRICT_MODE_APPLIES']) {
1418            $this->strict_mode = ($mode) ? GESHI_ALWAYS : GESHI_NEVER;
1419        }
1420    }
1421
1422    /**
1423     * Disables all highlighting
1424     *
1425     * @since 1.0.0
1426     * @todo  Rewrite with array traversal
1427     * @deprecated In favour of enable_highlighting
1428     */
1429    function disable_highlighting() {
1430        $this->enable_highlighting(false);
1431    }
1432
1433    /**
1434     * Enables all highlighting
1435     *
1436     * The optional flag parameter was added in version 1.0.7.21 and can be used
1437     * to enable (true) or disable (false) all highlighting.
1438     *
1439     * @since 1.0.0
1440     * @param boolean A flag specifying whether to enable or disable all highlighting
1441     * @todo  Rewrite with array traversal
1442     */
1443    function enable_highlighting($flag = true) {
1444        $flag = $flag ? true : false;
1445        foreach ($this->lexic_permissions as $key => $value) {
1446            if (is_array($value)) {
1447                foreach ($value as $k => $v) {
1448                    $this->lexic_permissions[$key][$k] = $flag;
1449                }
1450            } else {
1451                $this->lexic_permissions[$key] = $flag;
1452            }
1453        }
1454
1455        // Context blocks
1456        $this->enable_important_blocks = $flag;
1457    }
1458
1459    /**
1460     * Given a file extension, this method returns either a valid geshi language
1461     * name, or the empty string if it couldn't be found
1462     *
1463     * @param string The extension to get a language name for
1464     * @param array  A lookup array to use instead of the default one
1465     * @since 1.0.5
1466     * @todo Re-think about how this method works (maybe make it private and/or make it
1467     *       a extension->lang lookup?)
1468     */
1469    static function get_language_name_from_extension( $extension, $lookup = array() ) {
1470        $extension = strtolower($extension);
1471
1472        if ( !is_array($lookup) || empty($lookup)) {
1473            $lookup = array(
1474                '6502acme' => array( 'a', 's', 'asm', 'inc' ),
1475                '6502tasm' => array( 'a', 's', 'asm', 'inc' ),
1476                '6502kickass' => array( 'a', 's', 'asm', 'inc' ),
1477                '68000devpac' => array( 'a', 's', 'asm', 'inc' ),
1478                'abap' => array('abap'),
1479                'actionscript' => array('as'),
1480                'ada' => array('a', 'ada', 'adb', 'ads'),
1481                'apache' => array('conf'),
1482                'asm' => array('ash', 'asm', 'inc'),
1483                'asp' => array('asp'),
1484                'bash' => array('sh'),
1485                'bf' => array('bf'),
1486                'c' => array('c', 'h'),
1487                'c_mac' => array('c', 'h'),
1488                'caddcl' => array(),
1489                'cadlisp' => array(),
1490                'cdfg' => array('cdfg'),
1491                'cobol' => array('cbl'),
1492                'cpp' => array('cpp', 'hpp', 'C', 'H', 'CPP', 'HPP'),
1493                'csharp' => array('cs'),
1494                'css' => array('css'),
1495                'd' => array('d'),
1496                'delphi' => array('dpk', 'dpr', 'pp', 'pas'),
1497                'diff' => array('diff', 'patch'),
1498                'dos' => array('bat', 'cmd'),
1499                'gdb' => array('kcrash', 'crash', 'bt'),
1500                'gettext' => array('po', 'pot'),
1501                'gml' => array('gml'),
1502                'gnuplot' => array('plt'),
1503                'groovy' => array('groovy'),
1504                'haskell' => array('hs'),
1505                'haxe' => array('hx'),
1506                'html4strict' => array('html', 'htm'),
1507                'ini' => array('ini', 'desktop'),
1508                'java' => array('java'),
1509                'javascript' => array('js'),
1510                'klonec' => array('kl1'),
1511                'klonecpp' => array('klx'),
1512                'latex' => array('tex'),
1513                'lisp' => array('lisp'),
1514                'lua' => array('lua'),
1515                'matlab' => array('m'),
1516                'mpasm' => array(),
1517                'mysql' => array('sql'),
1518                'nsis' => array(),
1519                'objc' => array(),
1520                'oobas' => array(),
1521                'oracle8' => array(),
1522                'oracle10' => array(),
1523                'pascal' => array('pas'),
1524                'perl' => array('pl', 'pm'),
1525                'php' => array('php', 'php5', 'phtml', 'phps'),
1526                'povray' => array('pov'),
1527                'providex' => array('pvc', 'pvx'),
1528                'prolog' => array('pl'),
1529                'python' => array('py'),
1530                'qbasic' => array('bi'),
1531                'reg' => array('reg'),
1532                'ruby' => array('rb'),
1533                'sas' => array('sas'),
1534                'scala' => array('scala'),
1535                'scheme' => array('scm'),
1536                'scilab' => array('sci'),
1537                'smalltalk' => array('st'),
1538                'smarty' => array(),
1539                'tcl' => array('tcl'),
1540                'text' => array('txt'),
1541                'vb' => array('bas'),
1542                'vbnet' => array(),
1543                'visualfoxpro' => array(),
1544                'whitespace' => array('ws'),
1545                'xml' => array('xml', 'svg', 'xrc'),
1546                'z80' => array('z80', 'asm', 'inc')
1547            );
1548        }
1549
1550        foreach ($lookup as $lang => $extensions) {
1551            if (in_array($extension, $extensions)) {
1552                return $lang;
1553            }
1554        }
1555
1556        return 'text';
1557    }
1558
1559    /**
1560     * Given a file name, this method loads its contents in, and attempts
1561     * to set the language automatically. An optional lookup table can be
1562     * passed for looking up the language name. If not specified a default
1563     * table is used
1564     *
1565     * The language table is in the form
1566     * <pre>array(
1567     *   'lang_name' => array('extension', 'extension', ...),
1568     *   'lang_name' ...
1569     * );</pre>
1570     *
1571     * @param string The filename to load the source from
1572     * @param array  A lookup array to use instead of the default one
1573     * @todo Complete rethink of this and above method
1574     * @since 1.0.5
1575     */
1576    function load_from_file($file_name, $lookup = array()) {
1577        if (is_readable($file_name)) {
1578            $this->set_source(file_get_contents($file_name));
1579            $this->set_language(self::get_language_name_from_extension(substr(strrchr($file_name, '.'), 1), $lookup));
1580        } else {
1581            $this->error = GESHI_ERROR_FILE_NOT_READABLE;
1582        }
1583    }
1584
1585    /**
1586     * Adds a keyword to a keyword group for highlighting
1587     *
1588     * @param int    The key of the keyword group to add the keyword to
1589     * @param string The word to add to the keyword group
1590     * @since 1.0.0
1591     */
1592    function add_keyword($key, $word) {
1593        if (!is_array($this->language_data['KEYWORDS'][$key])) {
1594            $this->language_data['KEYWORDS'][$key] = array();
1595        }
1596        if (!in_array($word, $this->language_data['KEYWORDS'][$key])) {
1597            $this->language_data['KEYWORDS'][$key][] = $word;
1598
1599            //NEW in 1.0.8 don't recompile the whole optimized regexp, simply append it
1600            if ($this->parse_cache_built) {
1601                $subkey = count($this->language_data['CACHED_KEYWORD_LISTS'][$key]) - 1;
1602                $this->language_data['CACHED_KEYWORD_LISTS'][$key][$subkey] .= '|' . preg_quote($word, '/');
1603            }
1604        }
1605    }
1606
1607    /**
1608     * Removes a keyword from a keyword group
1609     *
1610     * @param int    The key of the keyword group to remove the keyword from
1611     * @param string The word to remove from the keyword group
1612     * @param bool   Wether to automatically recompile the optimized regexp list or not.
1613     *               Note: if you set this to false and @see GeSHi->parse_code() was already called once,
1614     *               for the current language, you have to manually call @see GeSHi->optimize_keyword_group()
1615     *               or the removed keyword will stay in cache and still be highlighted! On the other hand
1616     *               it might be too expensive to recompile the regexp list for every removal if you want to
1617     *               remove a lot of keywords.
1618     * @since 1.0.0
1619     */
1620    function remove_keyword($key, $word, $recompile = true) {
1621        $key_to_remove = array_search($word, $this->language_data['KEYWORDS'][$key]);
1622        if ($key_to_remove !== false) {
1623            unset($this->language_data['KEYWORDS'][$key][$key_to_remove]);
1624
1625            //NEW in 1.0.8, optionally recompile keyword group
1626            if ($recompile && $this->parse_cache_built) {
1627                $this->optimize_keyword_group($key);
1628            }
1629        }
1630    }
1631
1632    /**
1633     * Creates a new keyword group
1634     *
1635     * @param int    The key of the keyword group to create
1636     * @param string The styles for the keyword group
1637     * @param boolean Whether the keyword group is case sensitive ornot
1638     * @param array  The words to use for the keyword group
1639     * @since 1.0.0
1640     */
1641    function add_keyword_group($key, $styles, $case_sensitive = true, $words = array()) {
1642        $words = (array) $words;
1643        if  (empty($words)) {
1644            // empty word lists mess up highlighting
1645            return false;
1646        }
1647
1648        //Add the new keyword group internally
1649        $this->language_data['KEYWORDS'][$key] = $words;
1650        $this->lexic_permissions['KEYWORDS'][$key] = true;
1651        $this->language_data['CASE_SENSITIVE'][$key] = $case_sensitive;
1652        $this->language_data['STYLES']['KEYWORDS'][$key] = $styles;
1653
1654        //NEW in 1.0.8, cache keyword regexp
1655        if ($this->parse_cache_built) {
1656            $this->optimize_keyword_group($key);
1657        }
1658    }
1659
1660    /**
1661     * Removes a keyword group
1662     *
1663     * @param int    The key of the keyword group to remove
1664     * @since 1.0.0
1665     */
1666    function remove_keyword_group ($key) {
1667        //Remove the keyword group internally
1668        unset($this->language_data['KEYWORDS'][$key]);
1669        unset($this->lexic_permissions['KEYWORDS'][$key]);
1670        unset($this->language_data['CASE_SENSITIVE'][$key]);
1671        unset($this->language_data['STYLES']['KEYWORDS'][$key]);
1672
1673        //NEW in 1.0.8
1674        unset($this->language_data['CACHED_KEYWORD_LISTS'][$key]);
1675    }
1676
1677    /**
1678     * compile optimized regexp list for keyword group
1679     *
1680     * @param int   The key of the keyword group to compile & optimize
1681     * @since 1.0.8
1682     */
1683    function optimize_keyword_group($key) {
1684        $this->language_data['CACHED_KEYWORD_LISTS'][$key] =
1685            $this->optimize_regexp_list($this->language_data['KEYWORDS'][$key]);
1686        $space_as_whitespace = false;
1687        if(isset($this->language_data['PARSER_CONTROL'])) {
1688            if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS'])) {
1689                if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS']['SPACE_AS_WHITESPACE'])) {
1690                    $space_as_whitespace = $this->language_data['PARSER_CONTROL']['KEYWORDS']['SPACE_AS_WHITESPACE'];
1691                }
1692                if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$key]['SPACE_AS_WHITESPACE'])) {
1693                    if(isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$key]['SPACE_AS_WHITESPACE'])) {
1694                        $space_as_whitespace = $this->language_data['PARSER_CONTROL']['KEYWORDS'][$key]['SPACE_AS_WHITESPACE'];
1695                    }
1696                }
1697            }
1698        }
1699        if($space_as_whitespace) {
1700            foreach($this->language_data['CACHED_KEYWORD_LISTS'][$key] as $rxk => $rxv) {
1701                $this->language_data['CACHED_KEYWORD_LISTS'][$key][$rxk] =
1702                    str_replace(" ", "\\s+", $rxv);
1703            }
1704        }
1705    }
1706
1707    /**
1708     * Sets the content of the header block
1709     *
1710     * @param string The content of the header block
1711     * @since 1.0.2
1712     */
1713    function set_header_content($content) {
1714        $this->header_content = $content;
1715    }
1716
1717    /**
1718     * Sets the content of the footer block
1719     *
1720     * @param string The content of the footer block
1721     * @since 1.0.2
1722     */
1723    function set_footer_content($content) {
1724        $this->footer_content = $content;
1725    }
1726
1727    /**
1728     * Sets the style for the header content
1729     *
1730     * @param string The style for the header content
1731     * @since 1.0.2
1732     */
1733    function set_header_content_style($style) {
1734        $this->header_content_style = $style;
1735    }
1736
1737    /**
1738     * Sets the style for the footer content
1739     *
1740     * @param string The style for the footer content
1741     * @since 1.0.2
1742     */
1743    function set_footer_content_style($style) {
1744        $this->footer_content_style = $style;
1745    }
1746
1747    /**
1748     * Sets whether to force a surrounding block around
1749     * the highlighted code or not
1750     *
1751     * @param boolean Tells whether to enable or disable this feature
1752     * @since 1.0.7.20
1753     */
1754    function enable_inner_code_block($flag) {
1755        $this->force_code_block = (bool)$flag;
1756    }
1757
1758    /**
1759     * Sets the base URL to be used for keywords
1760     *
1761     * @param int The key of the keyword group to set the URL for
1762     * @param string The URL to set for the group. If {FNAME} is in
1763     *               the url somewhere, it is replaced by the keyword
1764     *               that the URL is being made for
1765     * @since 1.0.2
1766     */
1767    function set_url_for_keyword_group($group, $url) {
1768        $this->language_data['URLS'][$group] = $url;
1769    }
1770
1771    /**
1772     * Sets styles for links in code
1773     *
1774     * @param int A constant that specifies what state the style is being
1775     *            set for - e.g. :hover or :visited
1776     * @param string The styles to use for that state
1777     * @since 1.0.2
1778     */
1779    function set_link_styles($type, $styles) {
1780        $this->link_styles[$type] = $styles;
1781    }
1782
1783    /**
1784     * Sets the target for links in code
1785     *
1786     * @param string The target for links in the code, e.g. _blank
1787     * @since 1.0.3
1788     */
1789    function set_link_target($target) {
1790        if (!$target) {
1791            $this->link_target = '';
1792        } else {
1793            $this->link_target = ' target="' . $target . '"';
1794        }
1795    }
1796
1797    /**
1798     * Sets styles for important parts of the code
1799     *
1800     * @param string The styles to use on important parts of the code
1801     * @since 1.0.2
1802     */
1803    function set_important_styles($styles) {
1804        $this->important_styles = $styles;
1805    }
1806
1807    /**
1808     * Sets whether context-important blocks are highlighted
1809     *
1810     * @param boolean Tells whether to enable or disable highlighting of important blocks
1811     * @todo REMOVE THIS SHIZ FROM GESHI!
1812     * @deprecated
1813     * @since 1.0.2
1814     */
1815    function enable_important_blocks($flag) {
1816        $this->enable_important_blocks = ( $flag ) ? true : false;
1817    }
1818
1819    /**
1820     * Whether CSS IDs should be added to each line
1821     *
1822     * @param boolean If true, IDs will be added to each line.
1823     * @since 1.0.2
1824     */
1825    function enable_ids($flag = true) {
1826        $this->add_ids = ($flag) ? true : false;
1827    }
1828
1829    /**
1830     * Specifies which lines to highlight extra
1831     *
1832     * The extra style parameter was added in 1.0.7.21.
1833     *
1834     * @param mixed An array of line numbers to highlight, or just a line
1835     *              number on its own.
1836     * @param string A string specifying the style to use for this line.
1837     *              If null is specified, the default style is used.
1838     *              If false is specified, the line will be removed from
1839     *              special highlighting
1840     * @since 1.0.2
1841     * @todo  Some data replication here that could be cut down on
1842     */
1843    function highlight_lines_extra($lines, $style = null) {
1844        if (is_array($lines)) {
1845            //Split up the job using single lines at a time
1846            foreach ($lines as $line) {
1847                $this->highlight_lines_extra($line, $style);
1848            }
1849        } else {
1850            //Mark the line as being highlighted specially
1851            $lines = intval($lines);
1852            $this->highlight_extra_lines[$lines] = $lines;
1853
1854            //Decide on which style to use
1855            if ($style === null) { //Check if we should use default style
1856                unset($this->highlight_extra_lines_styles[$lines]);
1857            } elseif ($style === false) { //Check if to remove this line
1858                unset($this->highlight_extra_lines[$lines]);
1859                unset($this->highlight_extra_lines_styles[$lines]);
1860            } else {
1861                $this->highlight_extra_lines_styles[$lines] = $style;
1862            }
1863        }
1864    }
1865
1866    /**
1867     * Sets the style for extra-highlighted lines
1868     *
1869     * @param string The style for extra-highlighted lines
1870     * @since 1.0.2
1871     */
1872    function set_highlight_lines_extra_style($styles) {
1873        $this->highlight_extra_lines_style = $styles;
1874    }
1875
1876    /**
1877     * Sets the line-ending
1878     *
1879     * @param string The new line-ending
1880     * @since 1.0.2
1881     */
1882    function set_line_ending($line_ending) {
1883        $this->line_ending = (string)$line_ending;
1884    }
1885
1886    /**
1887     * Sets what number line numbers should start at. Should
1888     * be a positive integer, and will be converted to one.
1889     *
1890     * <b>Warning:</b> Using this method will add the "start"
1891     * attribute to the &lt;ol&gt; that is used for line numbering.
1892     * This is <b>not</b> valid XHTML strict, so if that's what you
1893     * care about then don't use this method. Firefox is getting
1894     * support for the CSS method of doing this in 1.1 and Opera
1895     * has support for the CSS method, but (of course) IE doesn't
1896     * so it's not worth doing it the CSS way yet.
1897     *
1898     * @param int The number to start line numbers at
1899     * @since 1.0.2
1900     */
1901    function start_line_numbers_at($number) {
1902        $this->line_numbers_start = abs(intval($number));
1903    }
1904
1905    /**
1906     * Sets the encoding used for htmlspecialchars(), for international
1907     * support.
1908     *
1909     * NOTE: This is not needed for now because htmlspecialchars() is not
1910     * being used (it has a security hole in PHP4 that has not been patched).
1911     * Maybe in a future version it may make a return for speed reasons, but
1912     * I doubt it.
1913     *
1914     * @param string The encoding to use for the source
1915     * @since 1.0.3
1916     */
1917    function set_encoding($encoding) {
1918        if ($encoding) {
1919          $this->encoding = strtolower($encoding);
1920        }
1921    }
1922
1923    /**
1924     * Turns linking of keywords on or off.
1925     *
1926     * @param boolean If true, links will be added to keywords
1927     * @since 1.0.2
1928     */
1929    function enable_keyword_links($enable = true) {
1930        $this->keyword_links = (bool) $enable;
1931    }
1932
1933    /**
1934     * Setup caches needed for styling. This is automatically called in
1935     * parse_code() and get_stylesheet() when appropriate. This function helps
1936     * stylesheet generators as they rely on some style information being
1937     * preprocessed
1938     *
1939     * @since 1.0.8
1940     * @access private
1941     */
1942    function build_style_cache() {
1943        //Build the style cache needed to highlight numbers appropriate
1944        if($this->lexic_permissions['NUMBERS']) {
1945            //First check what way highlighting information for numbers are given
1946            if(!isset($this->language_data['NUMBERS'])) {
1947                $this->language_data['NUMBERS'] = 0;
1948            }
1949
1950            if(is_array($this->language_data['NUMBERS'])) {
1951                $this->language_data['NUMBERS_CACHE'] = $this->language_data['NUMBERS'];
1952            } else {
1953                $this->language_data['NUMBERS_CACHE'] = array();
1954                if(!$this->language_data['NUMBERS']) {
1955                    $this->language_data['NUMBERS'] =
1956                        GESHI_NUMBER_INT_BASIC |
1957                        GESHI_NUMBER_FLT_NONSCI;
1958                }
1959
1960                for($i = 0, $j = $this->language_data['NUMBERS']; $j > 0; ++$i, $j>>=1) {
1961                    //Rearrange style indices if required ...
1962                    if(isset($this->language_data['STYLES']['NUMBERS'][1<<$i])) {
1963                        $this->language_data['STYLES']['NUMBERS'][$i] =
1964                            $this->language_data['STYLES']['NUMBERS'][1<<$i];
1965                        unset($this->language_data['STYLES']['NUMBERS'][1<<$i]);
1966                    }
1967
1968                    //Check if this bit is set for highlighting
1969                    if($j&1) {
1970                        //So this bit is set ...
1971                        //Check if it belongs to group 0 or the actual stylegroup
1972                        if(isset($this->language_data['STYLES']['NUMBERS'][$i])) {
1973                            $this->language_data['NUMBERS_CACHE'][$i] = 1 << $i;
1974                        } else {
1975                            if(!isset($this->language_data['NUMBERS_CACHE'][0])) {
1976                                $this->language_data['NUMBERS_CACHE'][0] = 0;
1977                            }
1978                            $this->language_data['NUMBERS_CACHE'][0] |= 1 << $i;
1979                        }
1980                    }
1981                }
1982            }
1983        }
1984    }
1985
1986    /**
1987     * Setup caches needed for parsing. This is automatically called in parse_code() when appropriate.
1988     * This function makes stylesheet generators much faster as they do not need these caches.
1989     *
1990     * @since 1.0.8
1991     * @access private
1992     */
1993    function build_parse_cache() {
1994        // cache symbol regexp
1995        //As this is a costy operation, we avoid doing it for multiple groups ...
1996        //Instead we perform it for all symbols at once.
1997        //
1998        //For this to work, we need to reorganize the data arrays.
1999        if ($this->lexic_permissions['SYMBOLS'] && !empty($this->language_data['SYMBOLS'])) {
2000            $this->language_data['MULTIPLE_SYMBOL_GROUPS'] = count($this->language_data['STYLES']['SYMBOLS']) > 1;
2001
2002            $this->language_data['SYMBOL_DATA'] = array();
2003            $symbol_preg_multi = array(); // multi char symbols
2004            $symbol_preg_single = array(); // single char symbols
2005            foreach ($this->language_data['SYMBOLS'] as $key => $symbols) {
2006                if (is_array($symbols)) {
2007                    foreach ($symbols as $sym) {
2008                        $sym = $this->hsc($sym);
2009                        if (!isset($this->language_data['SYMBOL_DATA'][$sym])) {
2010                            $this->language_data['SYMBOL_DATA'][$sym] = $key;
2011                            if (isset($sym[1])) { // multiple chars
2012                                $symbol_preg_multi[] = preg_quote($sym, '/');
2013                            } else { // single char
2014                                if ($sym == '-') {
2015                                    // don't trigger range out of order error
2016                                    $symbol_preg_single[] = '\-';
2017                                } else {
2018                                    $symbol_preg_single[] = preg_quote($sym, '/');
2019                                }
2020                            }
2021                        }
2022                    }
2023                } else {
2024                    $symbols = $this->hsc($symbols);
2025                    if (!isset($this->language_data['SYMBOL_DATA'][$symbols])) {
2026                        $this->language_data['SYMBOL_DATA'][$symbols] = 0;
2027                        if (isset($symbols[1])) { // multiple chars
2028                            $symbol_preg_multi[] = preg_quote($symbols, '/');
2029                        } elseif ($symbols == '-') {
2030                            // don't trigger range out of order error
2031                            $symbol_preg_single[] = '\-';
2032                        } else { // single char
2033                            $symbol_preg_single[] = preg_quote($symbols, '/');
2034                        }
2035                    }
2036                }
2037            }
2038
2039            //Now we have an array with each possible symbol as the key and the style as the actual data.
2040            //This way we can set the correct style just the moment we highlight ...
2041            //
2042            //Now we need to rewrite our array to get a search string that
2043            $symbol_preg = array();
2044            if (!empty($symbol_preg_multi)) {
2045                rsort($symbol_preg_multi);
2046                $symbol_preg[] = implode('|', $symbol_preg_multi);
2047            }
2048            if (!empty($symbol_preg_single)) {
2049                rsort($symbol_preg_single);
2050                $symbol_preg[] = '[' . implode('', $symbol_preg_single) . ']';
2051            }
2052            $this->language_data['SYMBOL_SEARCH'] = implode("|", $symbol_preg);
2053        }
2054
2055        // cache optimized regexp for keyword matching
2056        // remove old cache
2057        $this->language_data['CACHED_KEYWORD_LISTS'] = array();
2058        foreach (array_keys($this->language_data['KEYWORDS']) as $key) {
2059            if (!isset($this->lexic_permissions['KEYWORDS'][$key]) ||
2060                    $this->lexic_permissions['KEYWORDS'][$key]) {
2061                $this->optimize_keyword_group($key);
2062            }
2063        }
2064
2065        // brackets
2066        if ($this->lexic_permissions['BRACKETS']) {
2067            $this->language_data['CACHE_BRACKET_MATCH'] = array('[', ']', '(', ')', '{', '}');
2068            if (!$this->use_classes && isset($this->language_data['STYLES']['BRACKETS'][0])) {
2069                $this->language_data['CACHE_BRACKET_REPLACE'] = array(
2070                    '<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">&#91;|>',
2071                    '<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">&#93;|>',
2072                    '<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">&#40;|>',
2073                    '<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">&#41;|>',
2074                    '<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">&#123;|>',
2075                    '<| style="' . $this->language_data['STYLES']['BRACKETS'][0] . '">&#125;|>',
2076                );
2077            }
2078            else {
2079                $this->language_data['CACHE_BRACKET_REPLACE'] = array(
2080                    '<| class="br0">&#91;|>',
2081                    '<| class="br0">&#93;|>',
2082                    '<| class="br0">&#40;|>',
2083                    '<| class="br0">&#41;|>',
2084                    '<| class="br0">&#123;|>',
2085                    '<| class="br0">&#125;|>',
2086                );
2087            }
2088        }
2089
2090        //Build the parse cache needed to highlight numbers appropriate
2091        if($this->lexic_permissions['NUMBERS']) {
2092            //Check if the style rearrangements have been processed ...
2093            //This also does some preprocessing to check which style groups are useable ...
2094            if(!isset($this->language_data['NUMBERS_CACHE'])) {
2095                $this->build_style_cache();
2096            }
2097
2098            //Number format specification
2099            //All this formats are matched case-insensitively!
2100            static $numbers_format = array(
2101                GESHI_NUMBER_INT_BASIC =>
2102                    '(?:(?<![0-9a-z_\.%$@])|(?<=\.\.))(?<![\d\.]e[+\-])([1-9]\d*?|0)(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2103                GESHI_NUMBER_INT_CSTYLE =>
2104                    '(?<![0-9a-z_\.%])(?<![\d\.]e[+\-])([1-9]\d*?|0)l(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2105                GESHI_NUMBER_BIN_SUFFIX =>
2106                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])[01]+?[bB](?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2107                GESHI_NUMBER_BIN_PREFIX_PERCENT =>
2108                    '(?<![0-9a-z_\.%])(?<![\d\.]e[+\-])%[01]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2109                GESHI_NUMBER_BIN_PREFIX_0B =>
2110                    '(?<![0-9a-z_\.%])(?<![\d\.]e[+\-])0b[01]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2111                GESHI_NUMBER_OCT_PREFIX =>
2112                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])0[0-7]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2113                GESHI_NUMBER_OCT_PREFIX_0O =>
2114                    '(?<![0-9a-z_\.%])(?<![\d\.]e[+\-])0o[0-7]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2115                GESHI_NUMBER_OCT_PREFIX_AT =>
2116                    '(?<![0-9a-z_\.%])(?<![\d\.]e[+\-])\@[0-7]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2117                GESHI_NUMBER_OCT_SUFFIX =>
2118                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])[0-7]+?o(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2119                GESHI_NUMBER_HEX_PREFIX =>
2120                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])0x[0-9a-fA-F]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2121                GESHI_NUMBER_HEX_PREFIX_DOLLAR =>
2122                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])\$[0-9a-fA-F]+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2123                GESHI_NUMBER_HEX_SUFFIX =>
2124                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])\d[0-9a-fA-F]*?[hH](?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2125                GESHI_NUMBER_FLT_NONSCI =>
2126                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])\d+?\.\d+?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2127                GESHI_NUMBER_FLT_NONSCI_F =>
2128                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])(?:\d+?(?:\.\d*?)?|\.\d+?)f(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2129                GESHI_NUMBER_FLT_SCI_SHORT =>
2130                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])\.\d+?(?:e[+\-]?\d+?)?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)',
2131                GESHI_NUMBER_FLT_SCI_ZERO =>
2132                    '(?<![0-9a-z_\.])(?<![\d\.]e[+\-])(?:\d+?(?:\.\d*?)?|\.\d+?)(?:e[+\-]?\d+?)?(?![0-9a-z]|\.(?:[eE][+\-]?)?\d)'
2133                );
2134
2135            //At this step we have an associative array with flag groups for a
2136            //specific style or an string denoting a regexp given its index.
2137            $this->language_data['NUMBERS_RXCACHE'] = array();
2138            foreach($this->language_data['NUMBERS_CACHE'] as $key => $rxdata) {
2139                if(is_string($rxdata)) {
2140                    $regexp = $rxdata;
2141                } else {
2142                    //This is a bitfield of number flags to highlight:
2143                    //Build an array, implode them together and make this the actual RX
2144                    $rxuse = array();
2145                    for($i = 1; $i <= $rxdata; $i<<=1) {
2146                        if($rxdata & $i) {
2147                            $rxuse[] = $numbers_format[$i];
2148                        }
2149                    }
2150                    $regexp = implode("|", $rxuse);
2151                }
2152
2153                $this->language_data['NUMBERS_RXCACHE'][$key] =
2154                    "/(?<!<\|\/)(?<!<\|!REG3XP)(?<!<\|\/NUM!)(?<!\d\/>)($regexp)(?!(?:<DOT>|(?>[^\<]))+>)(?![^<]*>)(?!\|>)(?!\/>)/i"; //
2155            }
2156
2157            if(!isset($this->language_data['PARSER_CONTROL']['NUMBERS']['PRECHECK_RX'])) {
2158                $this->language_data['PARSER_CONTROL']['NUMBERS']['PRECHECK_RX'] = '#\d#';
2159            }
2160        }
2161
2162        $this->parse_cache_built = true;
2163    }
2164
2165    /**
2166     * Returns the code in $this->source, highlighted and surrounded by the
2167     * nessecary HTML.
2168     *
2169     * This should only be called ONCE, cos it's SLOW! If you want to highlight
2170     * the same source multiple times, you're better off doing a whole lot of
2171     * str_replaces to replace the &lt;span&gt;s
2172     *
2173     * @since 1.0.0
2174     */
2175    function parse_code () {
2176        // Start the timer
2177        $start_time = microtime();
2178
2179        // Replace all newlines to a common form.
2180        $code = str_replace("\r\n", "\n", $this->source);
2181        $code = str_replace("\r", "\n", $code);
2182
2183        // Firstly, if there is an error, we won't highlight
2184        if ($this->error) {
2185            //Escape the source for output
2186            $result = $this->hsc($this->source);
2187
2188            //This fix is related to SF#1923020, but has to be applied regardless of
2189            //actually highlighting symbols.
2190            $result = str_replace(array('<SEMI>', '<PIPE>'), array(';', '|'), $result);
2191
2192            // Timing is irrelevant
2193            $this->set_time($start_time, $start_time);
2194            $this->finalise($result);
2195            return $result;
2196        }
2197
2198        // make sure the parse cache is up2date
2199        if (!$this->parse_cache_built) {
2200            $this->build_parse_cache();
2201        }
2202
2203        // Initialise various stuff
2204        $length           = strlen($code);
2205        $COMMENT_MATCHED  = false;
2206        $stuff_to_parse   = '';
2207        $endresult        = '';
2208
2209        // "Important" selections are handled like multiline comments
2210        // @todo GET RID OF THIS SHIZ
2211        if ($this->enable_important_blocks) {
2212            $this->language_data['COMMENT_MULTI'][GESHI_START_IMPORTANT] = GESHI_END_IMPORTANT;
2213        }
2214
2215        if ($this->strict_mode) {
2216            // Break the source into bits. Each bit will be a portion of the code
2217            // within script delimiters - for example, HTML between < and >
2218            $k = 0;
2219            $parts = array();
2220            $matches = array();
2221            $next_match_pointer = null;
2222            // we use a copy to unset delimiters on demand (when they are not found)
2223            $delim_copy = $this->language_data['SCRIPT_DELIMITERS'];
2224            $i = 0;
2225            while ($i < $length) {
2226                $next_match_pos = $length + 1; // never true
2227                foreach ($delim_copy as $dk => $delimiters) {
2228                    if(is_array($delimiters)) {
2229                        foreach ($delimiters as $open => $close) {
2230                            // make sure the cache is setup properly
2231                            if (!isset($matches[$dk][$open])) {
2232                                $matches[$dk][$open] = array(
2233                                    'next_match' => -1,
2234                                    'dk' => $dk,
2235
2236                                    'open' => $open, // needed for grouping of adjacent code blocks (see below)
2237                                    'open_strlen' => strlen($open),
2238
2239                                    'close' => $close,
2240                                    'close_strlen' => strlen($close),
2241                                );
2242                            }
2243                            // Get the next little bit for this opening string
2244                            if ($matches[$dk][$open]['next_match'] < $i) {
2245                                // only find the next pos if it was not already cached
2246                                $open_pos = strpos($code, $open, $i);
2247                                if ($open_pos === false) {
2248                                    // no match for this delimiter ever
2249                                    unset($delim_copy[$dk][$open]);
2250                                    continue;
2251                                }
2252                                $matches[$dk][$open]['next_match'] = $open_pos;
2253                            }
2254                            if ($matches[$dk][$open]['next_match'] < $next_match_pos) {
2255                                //So we got a new match, update the close_pos
2256                                $matches[$dk][$open]['close_pos'] =
2257                                    strpos($code, $close, $matches[$dk][$open]['next_match']+1);
2258
2259                                $next_match_pointer =& $matches[$dk][$open];
2260                                $next_match_pos = $matches[$dk][$open]['next_match'];
2261                            }
2262                        }
2263                    } else {
2264                        //So we should match an RegExp as Strict Block ...
2265                        /**
2266                         * The value in $delimiters is expected to be an RegExp
2267                         * containing exactly 2 matching groups:
2268                         *  - Group 1 is the opener
2269                         *  - Group 2 is the closer
2270                         */
2271                        if(!GESHI_PHP_PRE_433 && //Needs proper rewrite to work with PHP >=4.3.0; 4.3.3 is guaranteed to work.
2272                            preg_match($delimiters, $code, $matches_rx, PREG_OFFSET_CAPTURE, $i)) {
2273                            //We got a match ...
2274                            if(isset($matches_rx['start']) && isset($matches_rx['end']))
2275                            {
2276                                $matches[$dk] = array(
2277                                    'next_match' => $matches_rx['start'][1],
2278                                    'dk' => $dk,
2279
2280                                    'close_strlen' => strlen($matches_rx['end'][0]),
2281                                    'close_pos' => $matches_rx['end'][1],
2282                                    );
2283                            } else {
2284                                $matches[$dk] = array(
2285                                    'next_match' => $matches_rx[1][1],
2286                                    'dk' => $dk,
2287
2288                                    'close_strlen' => strlen($matches_rx[2][0]),
2289                                    'close_pos' => $matches_rx[2][1],
2290                                    );
2291                            }
2292                        } else {
2293                            // no match for this delimiter ever
2294                            unset($delim_copy[$dk]);
2295                            continue;
2296                        }
2297
2298                        if ($matches[$dk]['next_match'] <= $next_match_pos) {
2299                            $next_match_pointer =& $matches[$dk];
2300                            $next_match_pos = $matches[$dk]['next_match'];
2301                        }
2302                    }
2303                }
2304
2305                // non-highlightable text
2306                $parts[$k] = array(
2307                    1 => substr($code, $i, $next_match_pos - $i)
2308                );
2309                ++$k;
2310
2311                if ($next_match_pos > $length) {
2312                    // out of bounds means no next match was found
2313                    break;
2314                }
2315
2316                // highlightable code
2317                $parts[$k][0] = $next_match_pointer['dk'];
2318
2319                //Only combine for non-rx script blocks
2320                if(is_array($delim_copy[$next_match_pointer['dk']])) {
2321                    // group adjacent script blocks, e.g. <foobar><asdf> should be one block, not three!
2322                    $i = $next_match_pos + $next_match_pointer['open_strlen'];
2323                    while (true) {
2324                        $close_pos = strpos($code, $next_match_pointer['close'], $i);
2325                        if ($close_pos == false) {
2326                            break;
2327                        }
2328                        $i = $close_pos + $next_match_pointer['close_strlen'];
2329                        if ($i == $length) {
2330                            break;
2331                        }
2332                        if ($code[$i] == $next_match_pointer['open'][0] && ($next_match_pointer['open_strlen'] == 1 ||
2333                            substr($code, $i, $next_match_pointer['open_strlen']) == $next_match_pointer['open'])) {
2334                            // merge adjacent but make sure we don't merge things like <tag><!-- comment -->
2335                            foreach ($matches as $submatches) {
2336                                foreach ($submatches as $match) {
2337                                    if ($match['next_match'] == $i) {
2338                                        // a different block already matches here!
2339                                        break 3;
2340                                    }
2341                                }
2342                            }
2343                        } else {
2344                            break;
2345                        }
2346                    }
2347                } else {
2348                    $close_pos = $next_match_pointer['close_pos'] + $next_match_pointer['close_strlen'];
2349                    $i = $close_pos;
2350                }
2351
2352                if ($close_pos === false) {
2353                    // no closing delimiter found!
2354                    $parts[$k][1] = substr($code, $next_match_pos);
2355                    ++$k;
2356                    break;
2357                } else {
2358                    $parts[$k][1] = substr($code, $next_match_pos, $i - $next_match_pos);
2359                    ++$k;
2360                }
2361            }
2362            unset($delim_copy, $next_match_pointer, $next_match_pos, $matches);
2363            $num_parts = $k;
2364
2365            if ($num_parts == 1 && $this->strict_mode == GESHI_MAYBE) {
2366                // when we have only one part, we don't have anything to highlight at all.
2367                // if we have a "maybe" strict language, this should be handled as highlightable code
2368                $parts = array(
2369                    0 => array(
2370                        0 => '',
2371                        1 => ''
2372                    ),
2373                    1 => array(
2374                        0 => null,
2375                        1 => $parts[0][1]
2376                    )
2377                );
2378                $num_parts = 2;
2379            }
2380
2381        } else {
2382            // Not strict mode - simply dump the source into
2383            // the array at index 1 (the first highlightable block)
2384            $parts = array(
2385                0 => array(
2386                    0 => '',
2387                    1 => ''
2388                ),
2389                1 => array(
2390                    0 => null,
2391                    1 => $code
2392                )
2393            );
2394            $num_parts = 2;
2395        }
2396
2397        //Unset variables we won't need any longer
2398        unset($code);
2399
2400        //Preload some repeatedly used values regarding hardquotes ...
2401        $hq = isset($this->language_data['HARDQUOTE']) ? $this->language_data['HARDQUOTE'][0] : false;
2402        $hq_strlen = strlen($hq);
2403
2404        //Preload if line numbers are to be generated afterwards
2405        //Added a check if line breaks should be forced even without line numbers, fixes SF#1727398
2406        $check_linenumbers = $this->line_numbers != GESHI_NO_LINE_NUMBERS ||
2407            !empty($this->highlight_extra_lines) || !$this->allow_multiline_span;
2408
2409        //preload the escape char for faster checking ...
2410        $escaped_escape_char = $this->hsc($this->language_data['ESCAPE_CHAR']);
2411
2412        // this is used for single-line comments
2413        $sc_disallowed_before = "";
2414        $sc_disallowed_after = "";
2415
2416        if (isset($this->language_data['PARSER_CONTROL'])) {
2417            if (isset($this->language_data['PARSER_CONTROL']['COMMENTS'])) {
2418                if (isset($this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_BEFORE'])) {
2419                    $sc_disallowed_before = $this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_BEFORE'];
2420                }
2421                if (isset($this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_AFTER'])) {
2422                    $sc_disallowed_after = $this->language_data['PARSER_CONTROL']['COMMENTS']['DISALLOWED_AFTER'];
2423                }
2424            }
2425        }
2426
2427        //Fix for SF#1932083: Multichar Quotemarks unsupported
2428        $is_string_starter = array();
2429        if ($this->lexic_permissions['STRINGS']) {
2430            foreach ($this->language_data['QUOTEMARKS'] as $quotemark) {
2431                if (!isset($is_string_starter[$quotemark[0]])) {
2432                    $is_string_starter[$quotemark[0]] = (string)$quotemark;
2433                } elseif (is_string($is_string_starter[$quotemark[0]])) {
2434                    $is_string_starter[$quotemark[0]] = array(
2435                        $is_string_starter[$quotemark[0]],
2436                        $quotemark);
2437                } else {
2438                    $is_string_starter[$quotemark[0]][] = $quotemark;
2439                }
2440            }
2441        }
2442
2443        // Now we go through each part. We know that even-indexed parts are
2444        // code that shouldn't be highlighted, and odd-indexed parts should
2445        // be highlighted
2446        for ($key = 0; $key < $num_parts; ++$key) {
2447            $STRICTATTRS = '';
2448
2449            // If this block should be highlighted...
2450            if (!($key & 1)) {
2451                // Else not a block to highlight
2452                $endresult .= $this->hsc($parts[$key][1]);
2453                unset($parts[$key]);
2454                continue;
2455            }
2456
2457            $result = '';
2458            $part = $parts[$key][1];
2459
2460            $highlight_part = true;
2461            if ($this->strict_mode && !is_null($parts[$key][0])) {
2462                // get the class key for this block of code
2463                $script_key = $parts[$key][0];
2464                $highlight_part = $this->language_data['HIGHLIGHT_STRICT_BLOCK'][$script_key];
2465                if ($this->language_data['STYLES']['SCRIPT'][$script_key] != '' &&
2466                    $this->lexic_permissions['SCRIPT']) {
2467                    // Add a span element around the source to
2468                    // highlight the overall source block
2469                    if (!$this->use_classes &&
2470                        $this->language_data['STYLES']['SCRIPT'][$script_key] != '') {
2471                        $attributes = ' style="' . $this->language_data['STYLES']['SCRIPT'][$script_key] . '"';
2472                    } else {
2473                        $attributes = ' class="sc' . $script_key . '"';
2474                    }
2475                    $result .= "<span$attributes>";
2476                    $STRICTATTRS = $attributes;
2477                }
2478            }
2479
2480            if ($highlight_part) {
2481                // Now, highlight the code in this block. This code
2482                // is really the engine of GeSHi (along with the method
2483                // parse_non_string_part).
2484
2485                // cache comment regexps incrementally
2486                $next_comment_regexp_key = '';
2487                $next_comment_regexp_pos = -1;
2488                $next_comment_multi_pos = -1;
2489                $next_comment_single_pos = -1;
2490                $comment_regexp_cache_per_key = array();
2491                $comment_multi_cache_per_key = array();
2492                $comment_single_cache_per_key = array();
2493                $next_open_comment_multi = '';
2494                $next_comment_single_key = '';
2495                $escape_regexp_cache_per_key = array();
2496                $next_escape_regexp_key = '';
2497                $next_escape_regexp_pos = -1;
2498
2499                $length = strlen($part);
2500                for ($i = 0; $i < $length; ++$i) {
2501                    // Get the next char
2502                    $char = $part[$i];
2503                    $char_len = 1;
2504
2505                    // update regexp comment cache if needed
2506                    if (isset($this->language_data['COMMENT_REGEXP']) && $next_comment_regexp_pos < $i) {
2507                        $next_comment_regexp_pos = $length;
2508                        foreach ($this->language_data['COMMENT_REGEXP'] as $comment_key => $regexp) {
2509                            $match_i = false;
2510                            if (isset($comment_regexp_cache_per_key[$comment_key]) &&
2511                                ($comment_regexp_cache_per_key[$comment_key]['pos'] >= $i ||
2512                                 $comment_regexp_cache_per_key[$comment_key]['pos'] === false)) {
2513                                // we have already matched something
2514                                if ($comment_regexp_cache_per_key[$comment_key]['pos'] === false) {
2515                                    // this comment is never matched
2516                                    continue;
2517                                }
2518                                $match_i = $comment_regexp_cache_per_key[$comment_key]['pos'];
2519                            } elseif (
2520                                //This is to allow use of the offset parameter in preg_match and stay as compatible with older PHP versions as possible
2521                                (GESHI_PHP_PRE_433 && preg_match($regexp, substr($part, $i), $match, PREG_OFFSET_CAPTURE)) ||
2522                                (!GESHI_PHP_PRE_433 && preg_match($regexp, $part, $match, PREG_OFFSET_CAPTURE, $i))
2523                                ) {
2524                                $match_i = $match[0][1];
2525                                if (GESHI_PHP_PRE_433) {
2526                                    $match_i += $i;
2527                                }
2528
2529                                $comment_regexp_cache_per_key[$comment_key] = array(
2530                                    'key' => $comment_key,
2531                                    'length' => strlen($match[0][0]),
2532                                    'pos' => $match_i
2533                                );
2534                            } else {
2535                                $comment_regexp_cache_per_key[$comment_key]['pos'] = false;
2536                                continue;
2537                            }
2538
2539                            if ($match_i !== false && $match_i < $next_comment_regexp_pos) {
2540                                $next_comment_regexp_pos = $match_i;
2541                                $next_comment_regexp_key = $comment_key;
2542                                if ($match_i === $i) {
2543                                    break;
2544                                }
2545                            }
2546                        }
2547                    }
2548
2549                    $string_started = false;
2550
2551                    if (isset($is_string_starter[$char])) {
2552                        // Possibly the start of a new string ...
2553
2554                        //Check which starter it was ...
2555                        //Fix for SF#1932083: Multichar Quotemarks unsupported
2556                        if (is_array($is_string_starter[$char])) {
2557                            $char_new = '';
2558                            foreach ($is_string_starter[$char] as $testchar) {
2559                                if ($testchar === substr($part, $i, strlen($testchar)) &&
2560                                    strlen($testchar) > strlen($char_new)) {
2561                                    $char_new = $testchar;
2562                                    $string_started = true;
2563                                }
2564                            }
2565                            if ($string_started) {
2566                                $char = $char_new;
2567                            }
2568                        } else {
2569                            $testchar = $is_string_starter[$char];
2570                            if ($testchar === substr($part, $i, strlen($testchar))) {
2571                                $char = $testchar;
2572                                $string_started = true;
2573                            }
2574                        }
2575                        $char_len = strlen($char);
2576                    }
2577
2578                    if ($string_started && ($i != $next_comment_regexp_pos)) {
2579                        // Hand out the correct style information for this string
2580                        $string_key = array_search($char, $this->language_data['QUOTEMARKS']);
2581                        if (!isset($this->language_data['STYLES']['STRINGS'][$string_key]) ||
2582                            !isset($this->language_data['STYLES']['ESCAPE_CHAR'][$string_key])) {
2583                            $string_key = 0;
2584                        }
2585
2586                        // parse the stuff before this
2587                        $result .= $this->parse_non_string_part($stuff_to_parse);
2588                        $stuff_to_parse = '';
2589
2590                        if (!$this->use_classes) {
2591                            $string_attributes = ' style="' . $this->language_data['STYLES']['STRINGS'][$string_key] . '"';
2592                        } else {
2593                            $string_attributes = ' class="st'.$string_key.'"';
2594                        }
2595
2596                        // now handle the string
2597                        $string = "<span$string_attributes>" . GeSHi::hsc($char);
2598                        $start = $i + $char_len;
2599                        $string_open = true;
2600
2601                        if(empty($this->language_data['ESCAPE_REGEXP'])) {
2602                            $next_escape_regexp_pos = $length;
2603                        }
2604
2605                        do {
2606                            //Get the regular ending pos ...
2607                            $close_pos = strpos($part, $char, $start);
2608                            if(false === $close_pos) {
2609                                $close_pos = $length;
2610                            }
2611
2612                            if($this->lexic_permissions['ESCAPE_CHAR']) {
2613                                // update escape regexp cache if needed
2614                                if (isset($this->language_data['ESCAPE_REGEXP']) && $next_escape_regexp_pos < $start) {
2615                                    $next_escape_regexp_pos = $length;
2616                                    foreach ($this->language_data['ESCAPE_REGEXP'] as $escape_key => $regexp) {
2617                                        $match_i = false;
2618                                        if (isset($escape_regexp_cache_per_key[$escape_key]) &&
2619                                            ($escape_regexp_cache_per_key[$escape_key]['pos'] >= $start ||
2620                                             $escape_regexp_cache_per_key[$escape_key]['pos'] === false)) {
2621                                            // we have already matched something
2622                                            if ($escape_regexp_cache_per_key[$escape_key]['pos'] === false) {
2623                                                // this comment is never matched
2624                                                continue;
2625                                            }
2626                                            $match_i = $escape_regexp_cache_per_key[$escape_key]['pos'];
2627                                        } elseif (
2628                                            //This is to allow use of the offset parameter in preg_match and stay as compatible with older PHP versions as possible
2629                                            (GESHI_PHP_PRE_433 && preg_match($regexp, substr($part, $start), $match, PREG_OFFSET_CAPTURE)) ||
2630                                            (!GESHI_PHP_PRE_433 && preg_match($regexp, $part, $match, PREG_OFFSET_CAPTURE, $start))
2631                                            ) {
2632                                            $match_i = $match[0][1];
2633                                            if (GESHI_PHP_PRE_433) {
2634                                                $match_i += $start;
2635                                            }
2636
2637                                            $escape_regexp_cache_per_key[$escape_key] = array(
2638                                                'key' => $escape_key,
2639                                                'length' => strlen($match[0][0]),
2640                                                'pos' => $match_i
2641                                            );
2642                                        } else {
2643                                            $escape_regexp_cache_per_key[$escape_key]['pos'] = false;
2644                                            continue;
2645                                        }
2646
2647                                        if ($match_i !== false && $match_i < $next_escape_regexp_pos) {
2648                                            $next_escape_regexp_pos = $match_i;
2649                                            $next_escape_regexp_key = $escape_key;
2650                                            if ($match_i === $start) {
2651                                                break;
2652                                            }
2653                                        }
2654                                    }
2655                                }
2656
2657                                //Find the next simple escape position
2658                                if('' != $this->language_data['ESCAPE_CHAR']) {
2659                                    $simple_escape = strpos($part, $this->language_data['ESCAPE_CHAR'], $start);
2660                                    if(false === $simple_escape) {
2661                                        $simple_escape = $length;
2662                                    }
2663                                } else {
2664                                    $simple_escape = $length;
2665                                }
2666                            } else {
2667                                $next_escape_regexp_pos = $length;
2668                                $simple_escape = $length;
2669                            }
2670
2671                            if($simple_escape < $next_escape_regexp_pos &&
2672                                $simple_escape < $length &&
2673                                $simple_escape < $close_pos) {
2674                                //The nexxt escape sequence is a simple one ...
2675                                $es_pos = $simple_escape;
2676
2677                                //Add the stuff not in the string yet ...
2678                                $string .= $this->hsc(substr($part, $start, $es_pos - $start));
2679
2680                                //Get the style for this escaped char ...
2681                                if (!$this->use_classes) {
2682                                    $escape_char_attributes = ' style="' . $this->language_data['STYLES']['ESCAPE_CHAR'][0] . '"';
2683                                } else {
2684                                    $escape_char_attributes = ' class="es0"';
2685                                }
2686
2687                                //Add the style for the escape char ...
2688                                $string .= "<span$escape_char_attributes>" .
2689                                    GeSHi::hsc($this->language_data['ESCAPE_CHAR']);
2690
2691                                //Get the byte AFTER the ESCAPE_CHAR we just found
2692                                $es_char = $part[$es_pos + 1];
2693                                if ($es_char == "\n") {
2694                                    // don't put a newline around newlines
2695                                    $string .= "</span>\n";
2696                                    $start = $es_pos + 2;
2697                                } elseif (ord($es_char) >= 128) {
2698                                    //This is an non-ASCII char (UTF8 or single byte)
2699                                    //This code tries to work around SF#2037598 ...
2700                                    if(function_exists('mb_substr')) {
2701                                        $es_char_m = mb_substr(substr($part, $es_pos+1, 16), 0, 1, $this->encoding);
2702                                        $string .= $es_char_m . '</span>';
2703                                    } elseif (!GESHI_PHP_PRE_433 && 'utf-8' == $this->encoding) {
2704                                        if(preg_match("/[\xC2-\xDF][\x80-\xBF]".
2705                                            "|\xE0[\xA0-\xBF][\x80-\xBF]".
2706                                            "|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}".
2707                                            "|\xED[\x80-\x9F][\x80-\xBF]".
2708                                            "|\xF0[\x90-\xBF][\x80-\xBF]{2}".
2709                                            "|[\xF1-\xF3][\x80-\xBF]{3}".
2710                                            "|\xF4[\x80-\x8F][\x80-\xBF]{2}/s",
2711                                            $part, $es_char_m, null, $es_pos + 1)) {
2712                                            $es_char_m = $es_char_m[0];
2713                                        } else {
2714                                            $es_char_m = $es_char;
2715                                        }
2716                                        $string .= $this->hsc($es_char_m) . '</span>';
2717                                    } else {
2718                                        $es_char_m = $this->hsc($es_char);
2719                                    }
2720                                    $start = $es_pos + strlen($es_char_m) + 1;
2721                                } else {
2722                                    $string .= $this->hsc($es_char) . '</span>';
2723                                    $start = $es_pos + 2;
2724                                }
2725                            } elseif ($next_escape_regexp_pos < $length &&
2726                                $next_escape_regexp_pos < $close_pos) {
2727                                $es_pos = $next_escape_regexp_pos;
2728                                //Add the stuff not in the string yet ...
2729                                $string .= $this->hsc(substr($part, $start, $es_pos - $start));
2730
2731                                //Get the key and length of this match ...
2732                                $escape = $escape_regexp_cache_per_key[$next_escape_regexp_key];
2733                                $escape_str = substr($part, $es_pos, $escape['length']);
2734                                $escape_key = $escape['key'];
2735
2736                                //Get the style for this escaped char ...
2737                                if (!$this->use_classes) {
2738                                    $escape_char_attributes = ' style="' . $this->language_data['STYLES']['ESCAPE_CHAR'][$escape_key] . '"';
2739                                } else {
2740                                    $escape_char_attributes = ' class="es' . $escape_key . '"';
2741                                }
2742
2743                                //Add the style for the escape char ...
2744                                $string .= "<span$escape_char_attributes>" .
2745                                    $this->hsc($escape_str) . '</span>';
2746
2747                                $start = $es_pos + $escape['length'];
2748                            } else {
2749                                //Copy the remainder of the string ...
2750                                $string .= $this->hsc(substr($part, $start, $close_pos - $start + $char_len)) . '</span>';
2751                                $start = $close_pos + $char_len;
2752                                $string_open = false;
2753                            }
2754                        } while($string_open);
2755
2756                        if ($check_linenumbers) {
2757                            // Are line numbers used? If, we should end the string before
2758                            // the newline and begin it again (so when <li>s are put in the source
2759                            // remains XHTML compliant)
2760                            // note to self: This opens up possibility of config files specifying
2761                            // that languages can/cannot have multiline strings???
2762                            $string = str_replace("\n", "</span>\n<span$string_attributes>", $string);
2763                        }
2764
2765                        $result .= $string;
2766                        $string = '';
2767                        $i = $start - 1;
2768                        continue;
2769                    } elseif ($this->lexic_permissions['STRINGS'] && $hq && $hq[0] == $char &&
2770                        substr($part, $i, $hq_strlen) == $hq && ($i != $next_comment_regexp_pos)) {
2771                        // The start of a hard quoted string
2772                        if (!$this->use_classes) {
2773                            $string_attributes = ' style="' . $this->language_data['STYLES']['STRINGS']['HARD'] . '"';
2774                            $escape_char_attributes = ' style="' . $this->language_data['STYLES']['ESCAPE_CHAR']['HARD'] . '"';
2775                        } else {
2776                            $string_attributes = ' class="st_h"';
2777                            $escape_char_attributes = ' class="es_h"';
2778                        }
2779                        // parse the stuff before this
2780                        $result .= $this->parse_non_string_part($stuff_to_parse);
2781                        $stuff_to_parse = '';
2782
2783                        // now handle the string
2784                        $string = '';
2785
2786                        // look for closing quote
2787                        $start = $i + $hq_strlen;
2788                        while ($close_pos = strpos($part, $this->language_data['HARDQUOTE'][1], $start)) {
2789                            $start = $close_pos + 1;
2790                            if ($this->lexic_permissions['ESCAPE_CHAR'] && $part[$close_pos - 1] == $this->language_data['HARDCHAR'] &&
2791                                (($i + $hq_strlen) != ($close_pos))) { //Support empty string for HQ escapes if Starter = Escape
2792                                // make sure this quote is not escaped
2793                                foreach ($this->language_data['HARDESCAPE'] as $hardescape) {
2794                                    if (substr($part, $close_pos - 1, strlen($hardescape)) == $hardescape) {
2795                                        // check wether this quote is escaped or if it is something like '\\'
2796                                        $escape_char_pos = $close_pos - 1;
2797                                        while ($escape_char_pos > 0
2798                                                && $part[$escape_char_pos - 1] == $this->language_data['HARDCHAR']) {
2799                                            --$escape_char_pos;
2800                                        }
2801                                        if (($close_pos - $escape_char_pos) & 1) {
2802                                            // uneven number of escape chars => this quote is escaped
2803                                            continue 2;
2804                                        }
2805                                    }
2806                                }
2807                            }
2808
2809                            // found closing quote
2810                            break;
2811                        }
2812
2813                        //Found the closing delimiter?
2814                        if (!$close_pos) {
2815                            // span till the end of this $part when no closing delimiter is found
2816                            $close_pos = $length;
2817                        }
2818
2819                        //Get the actual string
2820                        $string = substr($part, $i, $close_pos - $i + 1);
2821                        $i = $close_pos;
2822
2823                        // handle escape chars and encode html chars
2824                        // (special because when we have escape chars within our string they may not be escaped)
2825                        if ($this->lexic_permissions['ESCAPE_CHAR'] && $this->language_data['ESCAPE_CHAR']) {
2826                            $start = 0;
2827                            $new_string = '';
2828                            while ($es_pos = strpos($string, $this->language_data['ESCAPE_CHAR'], $start)) {
2829                                // hmtl escape stuff before
2830                                $new_string .= $this->hsc(substr($string, $start, $es_pos - $start));
2831                                // check if this is a hard escape
2832                                foreach ($this->language_data['HARDESCAPE'] as $hardescape) {
2833                                    if (substr($string, $es_pos, strlen($hardescape)) == $hardescape) {
2834                                        // indeed, this is a hardescape
2835                                        $new_string .= "<span$escape_char_attributes>" .
2836                                            $this->hsc($hardescape) . '</span>';
2837                                        $start = $es_pos + strlen($hardescape);
2838                                        continue 2;
2839                                    }
2840                                }
2841                                // not a hard escape, but a normal escape
2842                                // they come in pairs of two
2843                                $c = 0;
2844                                while (isset($string[$es_pos + $c]) && isset($string[$es_pos + $c + 1])
2845                                    && $string[$es_pos + $c] == $this->language_data['ESCAPE_CHAR']
2846                                    && $string[$es_pos + $c + 1] == $this->language_data['ESCAPE_CHAR']) {
2847                                    $c += 2;
2848                                }
2849                                if ($c) {
2850                                    $new_string .= "<span$escape_char_attributes>" .
2851                                        str_repeat($escaped_escape_char, $c) .
2852                                        '</span>';
2853                                    $start = $es_pos + $c;
2854                                } else {
2855                                    // this is just a single lonely escape char...
2856                                    $new_string .= $escaped_escape_char;
2857                                    $start = $es_pos + 1;
2858                                }
2859                            }
2860                            $string = $new_string . $this->hsc(substr($string, $start));
2861                        } else {
2862                            $string = $this->hsc($string);
2863                        }
2864
2865                        if ($check_linenumbers) {
2866                            // Are line numbers used? If, we should end the string before
2867                            // the newline and begin it again (so when <li>s are put in the source
2868                            // remains XHTML compliant)
2869                            // note to self: This opens up possibility of config files specifying
2870                            // that languages can/cannot have multiline strings???
2871                            $string = str_replace("\n", "</span>\n<span$string_attributes>", $string);
2872                        }
2873
2874                        $result .= "<span$string_attributes>" . $string . '</span>';
2875                        $string = '';
2876                        continue;
2877                    } else {
2878                        //Have a look for regexp comments
2879                        if ($i == $next_comment_regexp_pos) {
2880                            $COMMENT_MATCHED = true;
2881                            $comment = $comment_regexp_cache_per_key[$next_comment_regexp_key];
2882                            $test_str = $this->hsc(substr($part, $i, $comment['length']));
2883
2884                            //@todo If remove important do remove here
2885                            if ($this->lexic_permissions['COMMENTS']['MULTI']) {
2886                                if (!$this->use_classes) {
2887                                    $attributes = ' style="' . $this->language_data['STYLES']['COMMENTS'][$comment['key']] . '"';
2888                                } else {
2889                                    $attributes = ' class="co' . $comment['key'] . '"';
2890                                }
2891
2892                                $test_str = "<span$attributes>" . $test_str . "</span>";
2893
2894                                // Short-cut through all the multiline code
2895                                if ($check_linenumbers) {
2896                                    // strreplace to put close span and open span around multiline newlines
2897                                    $test_str = str_replace(
2898                                        "\n", "</span>\n<span$attributes>",
2899                                        str_replace("\n ", "\n&nbsp;", $test_str)
2900                                    );
2901                                }
2902                            }
2903
2904                            $i += $comment['length'] - 1;
2905
2906                            // parse the rest
2907                            $result .= $this->parse_non_string_part($stuff_to_parse);
2908                            $stuff_to_parse = '';
2909                        }
2910
2911                        // If we haven't matched a regexp comment, try multi-line comments
2912                        if (!$COMMENT_MATCHED) {
2913                            // Is this a multiline comment?
2914                            if (!empty($this->language_data['COMMENT_MULTI']) && $next_comment_multi_pos < $i) {
2915                                $next_comment_multi_pos = $length;
2916                                foreach ($this->language_data['COMMENT_MULTI'] as $open => $close) {
2917                                    $match_i = false;
2918                                    if (isset($comment_multi_cache_per_key[$open]) &&
2919                                        ($comment_multi_cache_per_key[$open] >= $i ||
2920                                         $comment_multi_cache_per_key[$open] === false)) {
2921                                        // we have already matched something
2922                                        if ($comment_multi_cache_per_key[$open] === false) {
2923                                            // this comment is never matched
2924                                            continue;
2925                                        }
2926                                        $match_i = $comment_multi_cache_per_key[$open];
2927                                    } elseif (($match_i = stripos($part, $open, $i)) !== false) {
2928                                        $comment_multi_cache_per_key[$open] = $match_i;
2929                                    } else {
2930                                        $comment_multi_cache_per_key[$open] = false;
2931                                        continue;
2932                                    }
2933                                    if ($match_i !== false && $match_i < $next_comment_multi_pos) {
2934                                        $next_comment_multi_pos = $match_i;
2935                                        $next_open_comment_multi = $open;
2936                                        if ($match_i === $i) {
2937                                            break;
2938                                        }
2939                                    }
2940                                }
2941                            }
2942                            if ($i == $next_comment_multi_pos) {
2943                                $open = $next_open_comment_multi;
2944                                $close = $this->language_data['COMMENT_MULTI'][$open];
2945                                $open_strlen = strlen($open);
2946                                $close_strlen = strlen($close);
2947                                $COMMENT_MATCHED = true;
2948                                $test_str_match = $open;
2949                                //@todo If remove important do remove here
2950                                if ($this->lexic_permissions['COMMENTS']['MULTI'] ||
2951                                    $open == GESHI_START_IMPORTANT) {
2952                                    if ($open != GESHI_START_IMPORTANT) {
2953                                        if (!$this->use_classes) {
2954                                            $attributes = ' style="' . $this->language_data['STYLES']['COMMENTS']['MULTI'] . '"';
2955                                        } else {
2956                                            $attributes = ' class="coMULTI"';
2957                                        }
2958                                        $test_str = "<span$attributes>" . $this->hsc($open);
2959                                    } else {
2960                                        if (!$this->use_classes) {
2961                                            $attributes = ' style="' . $this->important_styles . '"';
2962                                        } else {
2963                                            $attributes = ' class="imp"';
2964                                        }
2965
2966                                        // We don't include the start of the comment if it's an
2967                                        // "important" part
2968                                        $test_str = "<span$attributes>";
2969                                    }
2970                                } else {
2971                                    $test_str = $this->hsc($open);
2972                                }
2973
2974                                $close_pos = strpos( $part, $close, $i + $open_strlen );
2975
2976                                if ($close_pos === false) {
2977                                    $close_pos = $length;
2978                                }
2979
2980                                // Short-cut through all the multiline code
2981                                $rest_of_comment = $this->hsc(substr($part, $i + $open_strlen, $close_pos - $i - $open_strlen + $close_strlen));
2982                                if (($this->lexic_permissions['COMMENTS']['MULTI'] ||
2983                                    $test_str_match == GESHI_START_IMPORTANT) &&
2984                                    $check_linenumbers) {
2985
2986                                    // strreplace to put close span and open span around multiline newlines
2987                                    $test_str .= str_replace(
2988                                        "\n", "</span>\n<span$attributes>",
2989                                        str_replace("\n ", "\n&nbsp;", $rest_of_comment)
2990                                    );
2991                                } else {
2992                                    $test_str .= $rest_of_comment;
2993                                }
2994
2995                                if ($this->lexic_permissions['COMMENTS']['MULTI'] ||
2996                                    $test_str_match == GESHI_START_IMPORTANT) {
2997                                    $test_str .= '</span>';
2998                                }
2999
3000                                $i = $close_pos + $close_strlen - 1;
3001
3002                                // parse the rest
3003                                $result .= $this->parse_non_string_part($stuff_to_parse);
3004                                $stuff_to_parse = '';
3005                            }
3006                        }
3007
3008                        // If we haven't matched a multiline comment, try single-line comments
3009                        if (!$COMMENT_MATCHED) {
3010                            // cache potential single line comment occurances
3011                            if (!empty($this->language_data['COMMENT_SINGLE']) && $next_comment_single_pos < $i) {
3012                                $next_comment_single_pos = $length;
3013                                foreach ($this->language_data['COMMENT_SINGLE'] as $comment_key => $comment_mark) {
3014                                    $match_i = false;
3015                                    if (isset($comment_single_cache_per_key[$comment_key]) &&
3016                                        ($comment_single_cache_per_key[$comment_key] >= $i ||
3017                                         $comment_single_cache_per_key[$comment_key] === false)) {
3018                                        // we have already matched something
3019                                        if ($comment_single_cache_per_key[$comment_key] === false) {
3020                                            // this comment is never matched
3021                                            continue;
3022                                        }
3023                                        $match_i = $comment_single_cache_per_key[$comment_key];
3024                                    } elseif (
3025                                        // case sensitive comments
3026                                        ($this->language_data['CASE_SENSITIVE'][GESHI_COMMENTS] &&
3027                                        ($match_i = stripos($part, $comment_mark, $i)) !== false) ||
3028                                        // non case sensitive
3029                                        (!$this->language_data['CASE_SENSITIVE'][GESHI_COMMENTS] &&
3030                                          (($match_i = strpos($part, $comment_mark, $i)) !== false))) {
3031                                        $comment_single_cache_per_key[$comment_key] = $match_i;
3032                                    } else {
3033                                        $comment_single_cache_per_key[$comment_key] = false;
3034                                        continue;
3035                                    }
3036                                    if ($match_i !== false && $match_i < $next_comment_single_pos) {
3037                                        $next_comment_single_pos = $match_i;
3038                                        $next_comment_single_key = $comment_key;
3039                                        if ($match_i === $i) {
3040                                            break;
3041                                        }
3042                                    }
3043                                }
3044                            }
3045                            if ($next_comment_single_pos == $i) {
3046                                $comment_key = $next_comment_single_key;
3047                                $comment_mark = $this->language_data['COMMENT_SINGLE'][$comment_key];
3048                                $com_len = strlen($comment_mark);
3049
3050                                // This check will find special variables like $# in bash
3051                                // or compiler directives of Delphi beginning {$
3052                                if ((empty($sc_disallowed_before) || ($i == 0) ||
3053                                    (false === strpos($sc_disallowed_before, $part[$i-1]))) &&
3054                                    (empty($sc_disallowed_after) || ($length <= $i + $com_len) ||
3055                                    (false === strpos($sc_disallowed_after, $part[$i + $com_len]))))
3056                                {
3057                                    // this is a valid comment
3058                                    $COMMENT_MATCHED = true;
3059                                    if ($this->lexic_permissions['COMMENTS'][$comment_key]) {
3060                                        if (!$this->use_classes) {
3061                                            $attributes = ' style="' . $this->language_data['STYLES']['COMMENTS'][$comment_key] . '"';
3062                                        } else {
3063                                            $attributes = ' class="co' . $comment_key . '"';
3064                                        }
3065                                        $test_str = "<span$attributes>" . $this->hsc($this->change_case($comment_mark));
3066                                    } else {
3067                                        $test_str = $this->hsc($comment_mark);
3068                                    }
3069
3070                                    //Check if this comment is the last in the source
3071                                    $close_pos = strpos($part, "\n", $i);
3072                                    $oops = false;
3073                                    if ($close_pos === false) {
3074                                        $close_pos = $length;
3075                                        $oops = true;
3076                                    }
3077                                    $test_str .= $this->hsc(substr($part, $i + $com_len, $close_pos - $i - $com_len));
3078                                    if ($this->lexic_permissions['COMMENTS'][$comment_key]) {
3079                                        $test_str .= "</span>";
3080                                    }
3081
3082                                    // Take into account that the comment might be the last in the source
3083                                    if (!$oops) {
3084                                      $test_str .= "\n";
3085                                    }
3086
3087                                    $i = $close_pos;
3088
3089                                    // parse the rest
3090                                    $result .= $this->parse_non_string_part($stuff_to_parse);
3091                                    $stuff_to_parse = '';
3092                                }
3093                            }
3094                        }
3095                    }
3096
3097                    // Where are we adding this char?
3098                    if (!$COMMENT_MATCHED) {
3099                        $stuff_to_parse .= $char;
3100                    } else {
3101                        $result .= $test_str;
3102                        unset($test_str);
3103                        $COMMENT_MATCHED = false;
3104                    }
3105                }
3106                // Parse the last bit
3107                $result .= $this->parse_non_string_part($stuff_to_parse);
3108                $stuff_to_parse = '';
3109            } else {
3110                $result .= $this->hsc($part);
3111            }
3112            // Close the <span> that surrounds the block
3113            if ($STRICTATTRS != '') {
3114                $result = str_replace("\n", "</span>\n<span$STRICTATTRS>", $result);
3115                $result .= '</span>';
3116            }
3117
3118            $endresult .= $result;
3119            unset($part, $parts[$key], $result);
3120        }
3121
3122        //This fix is related to SF#1923020, but has to be applied regardless of
3123        //actually highlighting symbols.
3124        /** NOTE: memorypeak #3 */
3125        $endresult = str_replace(array('<SEMI>', '<PIPE>'), array(';', '|'), $endresult);
3126
3127//        // Parse the last stuff (redundant?)
3128//        $result .= $this->parse_non_string_part($stuff_to_parse);
3129
3130        // Lop off the very first and last spaces
3131//        $result = substr($result, 1, -1);
3132
3133        // We're finished: stop timing
3134        $this->set_time($start_time, microtime());
3135
3136        $this->finalise($endresult);
3137        return $endresult;
3138    }
3139
3140    /**
3141     * Swaps out spaces and tabs for HTML indentation. Not needed if
3142     * the code is in a pre block...
3143     *
3144     * @param  string The source to indent (reference!)
3145     * @since  1.0.0
3146     * @access private
3147     */
3148    function indent(&$result) {
3149        /// Replace tabs with the correct number of spaces
3150        if (false !== strpos($result, "\t")) {
3151            $lines = explode("\n", $result);
3152            $result = null;//Save memory while we process the lines individually
3153            $tab_width = $this->get_real_tab_width();
3154            $tab_string = '&nbsp;' . str_repeat(' ', $tab_width);
3155
3156            for ($key = 0, $n = count($lines); $key < $n; $key++) {
3157                $line = $lines[$key];
3158                if (false === strpos($line, "\t")) {
3159                    continue;
3160                }
3161
3162                $pos = 0;
3163                $length = strlen($line);
3164                $lines[$key] = ''; // reduce memory
3165
3166                $IN_TAG = false;
3167                for ($i = 0; $i < $length; ++$i) {
3168                    $char = $line[$i];
3169                    // Simple engine to work out whether we're in a tag.
3170                    // If we are we modify $pos. This is so we ignore HTML
3171                    // in the line and only workout the tab replacement
3172                    // via the actual content of the string
3173                    // This test could be improved to include strings in the
3174                    // html so that < or > would be allowed in user's styles
3175                    // (e.g. quotes: '<' '>'; or similar)
3176                    if ($IN_TAG) {
3177                        if ('>' == $char) {
3178                            $IN_TAG = false;
3179                        }
3180                        $lines[$key] .= $char;
3181                    } elseif ('<' == $char) {
3182                        $IN_TAG = true;
3183                        $lines[$key] .= '<';
3184                    } elseif ('&' == $char) {
3185                        $substr = substr($line, $i + 3, 5);
3186                        $posi = strpos($substr, ';');
3187                        if (false === $posi) {
3188                            ++$pos;
3189                        } else {
3190                            $pos -= $posi+2;
3191                        }
3192                        $lines[$key] .= $char;
3193                    } elseif ("\t" == $char) {
3194                        $str = '';
3195                        // OPTIMISE - move $strs out. Make an array:
3196                        // $tabs = array(
3197                        //  1 => '&nbsp;',
3198                        //  2 => '&nbsp; ',
3199                        //  3 => '&nbsp; &nbsp;' etc etc
3200                        // to use instead of building a string every time
3201                        $tab_end_width = $tab_width - ($pos % $tab_width); //Moved out of the look as it doesn't change within the loop
3202                        if (($pos & 1) || 1 == $tab_end_width) {
3203                            $str .= substr($tab_string, 6, $tab_end_width);
3204                        } else {
3205                            $str .= substr($tab_string, 0, $tab_end_width+5);
3206                        }
3207                        $lines[$key] .= $str;
3208                        $pos += $tab_end_width;
3209
3210                        if (false === strpos($line, "\t", $i + 1)) {
3211                            $lines[$key] .= substr($line, $i + 1);
3212                            break;
3213                        }
3214                    } elseif (0 == $pos && ' ' == $char) {
3215                        $lines[$key] .= '&nbsp;';
3216                        ++$pos;
3217                    } else {
3218                        $lines[$key] .= $char;
3219                        ++$pos;
3220                    }
3221                }
3222            }
3223            $result = implode("\n", $lines);
3224            unset($lines);//We don't need the lines separated beyond this --- free them!
3225        }
3226        // Other whitespace
3227        // BenBE: Fix to reduce the number of replacements to be done
3228        $result = preg_replace('/^ /m', '&nbsp;', $result);
3229        $result = str_replace('  ', ' &nbsp;', $result);
3230
3231        if ($this->line_numbers == GESHI_NO_LINE_NUMBERS && $this->header_type != GESHI_HEADER_PRE_TABLE) {
3232            if ($this->line_ending === null) {
3233                $result = nl2br($result);
3234            } else {
3235                $result = str_replace("\n", $this->line_ending, $result);
3236            }
3237        }
3238    }
3239
3240    /**
3241     * Changes the case of a keyword for those languages where a change is asked for
3242     *
3243     * @param  string The keyword to change the case of
3244     * @return string The keyword with its case changed
3245     * @since  1.0.0
3246     * @access private
3247     */
3248    function change_case($instr) {
3249        switch ($this->language_data['CASE_KEYWORDS']) {
3250            case GESHI_CAPS_UPPER:
3251                return strtoupper($instr);
3252            case GESHI_CAPS_LOWER:
3253                return strtolower($instr);
3254            default:
3255                return $instr;
3256        }
3257    }
3258
3259    /**
3260     * Handles replacements of keywords to include markup and links if requested
3261     *
3262     * @param  string The keyword to add the Markup to
3263     * @return The HTML for the match found
3264     * @since  1.0.8
3265     * @access private
3266     *
3267     * @todo   Get rid of ender in keyword links
3268     */
3269    function handle_keyword_replace($match) {
3270        $k = $this->_kw_replace_group;
3271        $keyword = $match[0];
3272        $keyword_match = $match[1];
3273
3274        $before = '';
3275        $after = '';
3276
3277        if ($this->keyword_links) {
3278            // Keyword links have been ebabled
3279
3280            if (isset($this->language_data['URLS'][$k]) &&
3281                $this->language_data['URLS'][$k] != '') {
3282                // There is a base group for this keyword
3283
3284                // Old system: strtolower
3285                //$keyword = ( $this->language_data['CASE_SENSITIVE'][$group] ) ? $keyword : strtolower($keyword);
3286                // New system: get keyword from language file to get correct case
3287                if (!$this->language_data['CASE_SENSITIVE'][$k] &&
3288                    strpos($this->language_data['URLS'][$k], '{FNAME}') !== false) {
3289                    foreach ($this->language_data['KEYWORDS'][$k] as $word) {
3290                        if (strcasecmp($word, $keyword_match) == 0) {
3291                            break;
3292                        }
3293                    }
3294                } else {
3295                    $word = $keyword_match;
3296                }
3297
3298                $before = '<|UR1|"' .
3299                    str_replace(
3300                        array(
3301                            '{FNAME}',
3302                            '{FNAMEL}',
3303                            '{FNAMEU}',
3304                            '.'),
3305                        array(
3306                            str_replace('+', '%20', urlencode($this->hsc($word))),
3307                            str_replace('+', '%20', urlencode($this->hsc(strtolower($word)))),
3308                            str_replace('+', '%20', urlencode($this->hsc(strtoupper($word)))),
3309                            '<DOT>'),
3310                        $this->language_data['URLS'][$k]
3311                    ) . '">';
3312                $after = '</a>';
3313            }
3314        }
3315
3316        return $before . '<|/'. $k .'/>' . $this->change_case($keyword) . '|>' . $after;
3317    }
3318
3319    /**
3320    * [Surcharge de Geshi] 
3321    *
3322    **/ 
3323    function handle_singleline_regexps($stuff_to_parse, $regexp, $key) { 
3324        $stuff_to_parse = preg_replace( 
3325                '/' . $regexp[GESHI_SEARCH] . '/' . $regexp[GESHI_MODIFIERS], 
3326                $regexp[GESHI_BEFORE] . '<|!REG3XP'. $key .'!>' . $regexp[GESHI_REPLACE] . '|>' . $regexp[GESHI_AFTER], 
3327                $stuff_to_parse); 
3328        return $stuff_to_parse; 
3329    } 
3330
3331    /**
3332    * handles regular expressions highlighting-definitions with callback functions
3333     *
3334     * @note this is a callback, don't use it directly
3335     *
3336     * @param array the matches array
3337     * @return The highlighted string
3338     * @since 1.0.8
3339     * @access private
3340     */
3341    function handle_regexps_callback($matches) {
3342        // before: "' style=\"' . call_user_func(\"$func\", '\\1') . '\"\\1|>'",
3343        return  ' style="' . call_user_func($this->language_data['STYLES']['REGEXPS'][$this->_rx_key], $matches[1]) . '"'. $matches[1] . '|>';
3344    }
3345
3346    /**
3347     * handles newlines in REGEXPS matches. Set the _hmr_* vars before calling this
3348     *
3349     * @note this is a callback, don't use it directly
3350     *
3351     * @param array the matches array
3352     * @return string
3353     * @since 1.0.8
3354     * @access private
3355     */
3356    function handle_multiline_regexps($matches) {
3357        $before = $this->_hmr_before;
3358        $after = $this->_hmr_after;
3359        if ($this->_hmr_replace) {
3360            $replace = $this->_hmr_replace;
3361            $search = array();
3362
3363            foreach (array_keys($matches) as $k) {
3364                $search[] = '\\' . $k;
3365            }
3366
3367            $before = str_replace($search, $matches, $before);
3368            $after = str_replace($search, $matches, $after);
3369            $replace = str_replace($search, $matches, $replace);
3370        } else {
3371            $replace = $matches[0];
3372        }
3373        return $before
3374                    . '<|!REG3XP' . $this->_hmr_key .'!>'
3375                        . str_replace("\n", "|>\n<|!REG3XP" . $this->_hmr_key . '!>', $replace)
3376                    . '|>'
3377              . $after;
3378    }
3379
3380    /**
3381     * Takes a string that has no strings or comments in it, and highlights
3382     * stuff like keywords, numbers and methods.
3383     *
3384     * @param string The string to parse for keyword, numbers etc.
3385     * @since 1.0.0
3386     * @access private
3387     * @todo BUGGY! Why? Why not build string and return?
3388     */
3389    function parse_non_string_part($stuff_to_parse) {
3390        $stuff_to_parse = ' ' . $this->hsc($stuff_to_parse);
3391
3392        // Highlight keywords
3393        $disallowed_before = "(?<![a-zA-Z0-9\$_\|\#|^&";
3394        $disallowed_after = "(?![a-zA-Z0-9_\|%\\-&;";
3395        if ($this->lexic_permissions['STRINGS']) {
3396            $quotemarks = preg_quote(implode($this->language_data['QUOTEMARKS']), '/');
3397            $disallowed_before .= $quotemarks;
3398            $disallowed_after .= $quotemarks;
3399        }
3400        $disallowed_before .= "])";
3401        $disallowed_after .= "])";
3402
3403        $parser_control_pergroup = false;
3404        if (isset($this->language_data['PARSER_CONTROL'])) {
3405            if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS'])) {
3406                $x = 0; // check wether per-keyword-group parser_control is enabled
3407                if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_BEFORE'])) {
3408                    $disallowed_before = $this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_BEFORE'];
3409                    ++$x;
3410                }
3411                if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_AFTER'])) {
3412                    $disallowed_after = $this->language_data['PARSER_CONTROL']['KEYWORDS']['DISALLOWED_AFTER'];
3413                    ++$x;
3414                }
3415                $parser_control_pergroup = (count($this->language_data['PARSER_CONTROL']['KEYWORDS']) - $x) > 0;
3416            }
3417        }
3418
3419        foreach (array_keys($this->language_data['KEYWORDS']) as $k) {
3420            if (!isset($this->lexic_permissions['KEYWORDS'][$k]) ||
3421                $this->lexic_permissions['KEYWORDS'][$k]) {
3422
3423                $case_sensitive = $this->language_data['CASE_SENSITIVE'][$k];
3424                $modifiers = $case_sensitive ? '' : 'i';
3425
3426                // NEW in 1.0.8 - per-keyword-group parser control
3427                $disallowed_before_local = $disallowed_before;
3428                $disallowed_after_local = $disallowed_after;
3429                if ($parser_control_pergroup && isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$k])) {
3430                    if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_BEFORE'])) {
3431                        $disallowed_before_local =
3432                            $this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_BEFORE'];
3433                    }
3434
3435                    if (isset($this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_AFTER'])) {
3436                        $disallowed_after_local =
3437                            $this->language_data['PARSER_CONTROL']['KEYWORDS'][$k]['DISALLOWED_AFTER'];
3438                    }
3439                }
3440
3441                $this->_kw_replace_group = $k;
3442
3443                //NEW in 1.0.8, the cached regexp list
3444                // since we don't want PHP / PCRE to crash due to too large patterns we split them into smaller chunks
3445                for ($set = 0, $set_length = count($this->language_data['CACHED_KEYWORD_LISTS'][$k]); $set <  $set_length; ++$set) {
3446                    $keywordset =& $this->language_data['CACHED_KEYWORD_LISTS'][$k][$set];
3447                    // Might make a more unique string for putting the number in soon
3448                    // Basically, we don't put the styles in yet because then the styles themselves will
3449                    // get highlighted if the language has a CSS keyword in it (like CSS, for example ;))
3450                    $stuff_to_parse = preg_replace_callback(
3451                        "/$disallowed_before_local({$keywordset})(?!\<DOT\>(?:htm|php|aspx?))$disallowed_after_local/$modifiers",
3452                        array($this, 'handle_keyword_replace'),
3453                        $stuff_to_parse
3454                        );
3455                }
3456            }
3457        }
3458
3459        // Regular expressions
3460        foreach ($this->language_data['REGEXPS'] as $key => $regexp) {
3461            if ($this->lexic_permissions['REGEXPS'][$key]) {
3462                if (is_array($regexp)) {
3463                    if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
3464                        // produce valid HTML when we match multiple lines
3465                        $this->_hmr_replace = $regexp[GESHI_REPLACE];
3466                        $this->_hmr_before = $regexp[GESHI_BEFORE];
3467                        $this->_hmr_key = $key;
3468                        $this->_hmr_after = $regexp[GESHI_AFTER];
3469                        $stuff_to_parse = preg_replace_callback(
3470                            "/" . $regexp[GESHI_SEARCH] . "/{$regexp[GESHI_MODIFIERS]}",
3471                            array($this, 'handle_multiline_regexps'),
3472                            $stuff_to_parse);
3473                        $this->_hmr_replace = false;
3474                        $this->_hmr_before = '';
3475                        $this->_hmr_after = '';
3476                    } else {
3477                        // [surcharge de GESHI]
3478                        // pour passer dans une methode
3479                        // (qui permet donc une surcharge de juste la methode)
3480                        /*
3481                            $stuff_to_parse = preg_replace(
3482                                '/' . $regexp[GESHI_SEARCH] . '/' . $regexp[GESHI_MODIFIERS],
3483                                $regexp[GESHI_BEFORE] . '<|!REG3XP'. $key .'!>' . $regexp[GESHI_REPLACE] . '|>' . $regexp[GESHI_AFTER],
3484                                $stuff_to_parse);
3485                        */ 
3486                        $stuff_to_parse = $this->handle_singleline_regexps($stuff_to_parse, $regexp, $key); 
3487                    }
3488                } else {
3489                    if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
3490                        // produce valid HTML when we match multiple lines
3491                        $this->_hmr_key = $key;
3492                        $stuff_to_parse = preg_replace_callback( "/(" . $regexp . ")/",
3493                                              array($this, 'handle_multiline_regexps'), $stuff_to_parse);
3494                        $this->_hmr_key = '';
3495                    } else {
3496                        $stuff_to_parse = preg_replace( "/(" . $regexp . ")/", "<|!REG3XP$key!>\\1|>", $stuff_to_parse);
3497                    }
3498                }
3499            }
3500        }
3501
3502        // Highlight numbers. As of 1.0.8 we support different types of numbers
3503        $numbers_found = false;
3504
3505        if ($this->lexic_permissions['NUMBERS'] && preg_match($this->language_data['PARSER_CONTROL']['NUMBERS']['PRECHECK_RX'], $stuff_to_parse )) {
3506            $numbers_found = true;
3507
3508            //For each of the formats ...
3509            foreach($this->language_data['NUMBERS_RXCACHE'] as $id => $regexp) {
3510                //Check if it should be highlighted ...
3511                $stuff_to_parse = preg_replace($regexp, "<|/NUM!$id/>\\1|>", $stuff_to_parse);
3512            }
3513        }
3514
3515        //
3516        // Now that's all done, replace /[number]/ with the correct styles
3517        //
3518        foreach (array_keys($this->language_data['KEYWORDS']) as $k) {
3519            if (!$this->use_classes) {
3520                $attributes = ' style="' .
3521                    (isset($this->language_data['STYLES']['KEYWORDS'][$k]) ?
3522                    $this->language_data['STYLES']['KEYWORDS'][$k] : "") . '"';
3523            } else {
3524                $attributes = ' class="kw' . $k . '"';
3525            }
3526            $stuff_to_parse = str_replace("<|/$k/>", "<|$attributes>", $stuff_to_parse);
3527        }
3528
3529        if ($numbers_found) {
3530            // Put number styles in
3531            foreach($this->language_data['NUMBERS_RXCACHE'] as $id => $regexp) {
3532                //Commented out for now, as this needs some review ...
3533                //                if ($numbers_permissions & $id) {
3534                //Get the appropriate style ...
3535                //Checking for unset styles is done by the style cache builder ...
3536                if (!$this->use_classes) {
3537                    $attributes = ' style="' . $this->language_data['STYLES']['NUMBERS'][$id] . '"';
3538                } else {
3539                    $attributes = ' class="nu'.$id.'"';
3540                }
3541
3542                //Set in the correct styles ...
3543                $stuff_to_parse = str_replace("/NUM!$id/", $attributes, $stuff_to_parse);
3544                //                }
3545            }
3546        }
3547
3548        // Highlight methods and fields in objects
3549        if ($this->lexic_permissions['METHODS'] && $this->language_data['OOLANG']) {
3550            $oolang_spaces = "[\s]*";
3551            $oolang_before = "";
3552            $oolang_after = "[a-zA-Z][a-zA-Z0-9_]*";
3553            if (isset($this->language_data['PARSER_CONTROL'])) {
3554                if (isset($this->language_data['PARSER_CONTROL']['OOLANG'])) {
3555                    if (isset($this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_BEFORE'])) {
3556                        $oolang_before = $this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_BEFORE'];
3557                    }
3558                    if (isset($this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_AFTER'])) {
3559                        $oolang_after = $this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_AFTER'];
3560                    }
3561                    if (isset($this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_SPACES'])) {
3562                        $oolang_spaces = $this->language_data['PARSER_CONTROL']['OOLANG']['MATCH_SPACES'];
3563                    }
3564                }
3565            }
3566
3567            foreach ($this->language_data['OBJECT_SPLITTERS'] as $key => $splitter) {
3568                if (false !== strpos($stuff_to_parse, $splitter)) {
3569                    if (!$this->use_classes) {
3570                        $attributes = ' style="' . $this->language_data['STYLES']['METHODS'][$key] . '"';
3571                    } else {
3572                        $attributes = ' class="me' . $key . '"';
3573                    }
3574                    $stuff_to_parse = preg_replace("/($oolang_before)(" . preg_quote($this->language_data['OBJECT_SPLITTERS'][$key], '/') . ")($oolang_spaces)($oolang_after)/", "\\1\\2\\3<|$attributes>\\4|>", $stuff_to_parse);
3575                }
3576            }
3577        }
3578
3579        //
3580        // Highlight brackets. Yes, I've tried adding a semi-colon to this list.
3581        // You try it, and see what happens ;)
3582        // TODO: Fix lexic permissions not converting entities if shouldn't
3583        // be highlighting regardless
3584        //
3585        if ($this->lexic_permissions['BRACKETS']) {
3586            $stuff_to_parse = str_replace( $this->language_data['CACHE_BRACKET_MATCH'],
3587                              $this->language_data['CACHE_BRACKET_REPLACE'], $stuff_to_parse );
3588        }
3589
3590
3591        //FIX for symbol highlighting ...
3592        if ($this->lexic_permissions['SYMBOLS'] && !empty($this->language_data['SYMBOLS'])) {
3593            //Get all matches and throw away those witin a block that is already highlighted... (i.e. matched by a regexp)
3594            $n_symbols = preg_match_all("/<\|(?:<DOT>|[^>])+>(?:(?!\|>).*?)\|>|<\/a>|(?:" . $this->language_data['SYMBOL_SEARCH'] . ")+(?![^<]+?>)/", $stuff_to_parse, $pot_symbols, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
3595            $global_offset = 0;
3596            for ($s_id = 0; $s_id < $n_symbols; ++$s_id) {
3597                $symbol_match = $pot_symbols[$s_id][0][0];
3598                if (strpos($symbol_match, '<') !== false || strpos($symbol_match, '>') !== false) {
3599                    // already highlighted blocks _must_ include either < or >
3600                    // so if this conditional applies, we have to skip this match
3601                    // BenBE: UNLESS the block contains <SEMI> or <PIPE>
3602                    if(strpos($symbol_match, '<SEMI>') === false &&
3603                        strpos($symbol_match, '<PIPE>') === false) {
3604                        continue;
3605                    }
3606                }
3607
3608                // if we reach this point, we have a valid match which needs to be highlighted
3609
3610                $symbol_length = strlen($symbol_match);
3611                $symbol_offset = $pot_symbols[$s_id][0][1];
3612                unset($pot_symbols[$s_id]);
3613                $symbol_hl = "";
3614
3615                // if we have multiple styles, we have to handle them properly
3616                if ($this->language_data['MULTIPLE_SYMBOL_GROUPS']) {
3617                    $old_sym = -1;
3618                    // Split the current stuff to replace into its atomic symbols ...
3619                    preg_match_all("/" . $this->language_data['SYMBOL_SEARCH'] . "/", $symbol_match, $sym_match_syms, PREG_PATTERN_ORDER);
3620                    foreach ($sym_match_syms[0] as $sym_ms) {
3621                        //Check if consequtive symbols belong to the same group to save output ...
3622                        if (isset($this->language_data['SYMBOL_DATA'][$sym_ms])
3623                            && ($this->language_data['SYMBOL_DATA'][$sym_ms] != $old_sym)) {
3624                            if (-1 != $old_sym) {
3625                                $symbol_hl .= "|>";
3626                            }
3627                            $old_sym = $this->language_data['SYMBOL_DATA'][$sym_ms];
3628                            if (!$this->use_classes) {
3629                                $symbol_hl .= '<| style="' . $this->language_data['STYLES']['SYMBOLS'][$old_sym] . '">';
3630                            } else {
3631                                $symbol_hl .= '<| class="sy' . $old_sym . '">';
3632                            }
3633                        }
3634                        $symbol_hl .= $sym_ms;
3635                    }
3636                    unset($sym_match_syms);
3637
3638                    //Close remaining tags and insert the replacement at the right position ...
3639                    //Take caution if symbol_hl is empty to avoid doubled closing spans.
3640                    if (-1 != $old_sym) {
3641                        $symbol_hl .= "|>";
3642                    }
3643                } else {
3644                    if (!$this->use_classes) {
3645                        $symbol_hl = '<| style="' . $this->language_data['STYLES']['SYMBOLS'][0] . '">';
3646                    } else {
3647                        $symbol_hl = '<| class="sy0">';
3648                    }
3649                    $symbol_hl .= $symbol_match . '|>';
3650                }
3651
3652                $stuff_to_parse = substr_replace($stuff_to_parse, $symbol_hl, $symbol_offset + $global_offset, $symbol_length);
3653
3654                // since we replace old text with something of different size,
3655                // we'll have to keep track of the differences
3656                $global_offset += strlen($symbol_hl) - $symbol_length;
3657            }
3658        }
3659        //FIX for symbol highlighting ...
3660
3661        // Add class/style for regexps
3662        foreach (array_keys($this->language_data['REGEXPS']) as $key) {
3663            if ($this->lexic_permissions['REGEXPS'][$key]) {
3664                if (is_callable($this->language_data['STYLES']['REGEXPS'][$key])) {
3665                    $this->_rx_key = $key;
3666                    $stuff_to_parse = preg_replace_callback("/!REG3XP$key!(.*)\|>/U",
3667                        array($this, 'handle_regexps_callback'),
3668                        $stuff_to_parse);
3669                } else {
3670                    if (!$this->use_classes) {
3671                        $attributes = ' style="' . $this->language_data['STYLES']['REGEXPS'][$key] . '"';
3672                    } else {
3673                        if (is_array($this->language_data['REGEXPS'][$key]) &&
3674                            array_key_exists(GESHI_CLASS, $this->language_data['REGEXPS'][$key])) {
3675                            $attributes = ' class="' .
3676                                $this->language_data['REGEXPS'][$key][GESHI_CLASS] . '"';
3677                        } else {
3678                           $attributes = ' class="re' . $key . '"';
3679                        }
3680                    }
3681                    $stuff_to_parse = str_replace("!REG3XP$key!", "$attributes", $stuff_to_parse);
3682                }
3683            }
3684        }
3685
3686        // Replace <DOT> with . for urls
3687        $stuff_to_parse = str_replace('<DOT>', '.', $stuff_to_parse);
3688        // Replace <|UR1| with <a href= for urls also
3689        if (isset($this->link_styles[GESHI_LINK])) {
3690            if ($this->use_classes) {
3691                $stuff_to_parse = str_replace('<|UR1|', '<a' . $this->link_target . ' href=', $stuff_to_parse);
3692            } else {
3693                $stuff_to_parse = str_replace('<|UR1|', '<a' . $this->link_target . ' style="' . $this->link_styles[GESHI_LINK] . '" href=', $stuff_to_parse);
3694            }
3695        } else {
3696            $stuff_to_parse = str_replace('<|UR1|', '<a' . $this->link_target . ' href=', $stuff_to_parse);
3697        }
3698
3699        //
3700        // NOW we add the span thingy ;)
3701        //
3702
3703        $stuff_to_parse = str_replace('<|', '<span', $stuff_to_parse);
3704        $stuff_to_parse = str_replace ( '|>', '</span>', $stuff_to_parse );
3705        return substr($stuff_to_parse, 1);
3706    }
3707
3708    /**
3709     * Sets the time taken to parse the code
3710     *
3711     * @param microtime The time when parsing started
3712     * @param microtime The time when parsing ended
3713     * @since 1.0.2
3714     * @access private
3715     */
3716    function set_time($start_time, $end_time) {
3717        $start = explode(' ', $start_time);
3718        $end = explode(' ', $end_time);
3719        $this->time = $end[0] + $end[1] - $start[0] - $start[1];
3720    }
3721
3722    /**
3723     * Gets the time taken to parse the code
3724     *
3725     * @return double The time taken to parse the code
3726     * @since  1.0.2
3727     */
3728    function get_time() {
3729        return $this->time;
3730    }
3731
3732    /**
3733     * Merges arrays recursively, overwriting values of the first array with values of later arrays
3734     *
3735     * @since 1.0.8
3736     * @access private
3737     */
3738    function merge_arrays() {
3739        $arrays = func_get_args();
3740        $narrays = count($arrays);
3741
3742        // check arguments
3743        // comment out if more performance is necessary (in this case the foreach loop will trigger a warning if the argument is not an array)
3744        for ($i = 0; $i < $narrays; $i ++) {
3745            if (!is_array($arrays[$i])) {
3746                // also array_merge_recursive returns nothing in this case
3747                trigger_error('Argument #' . ($i+1) . ' is not an array - trying to merge array with scalar! Returning false!', E_USER_WARNING);
3748                return false;
3749            }
3750        }
3751
3752        // the first array is in the output set in every case
3753        $ret = $arrays[0];
3754
3755        // merege $ret with the remaining arrays
3756        for ($i = 1; $i < $narrays; $i ++) {
3757            foreach ($arrays[$i] as $key => $value) {
3758                if (is_array($value) && isset($ret[$key])) {
3759                    // if $ret[$key] is not an array you try to merge an scalar value with an array - the result is not defined (incompatible arrays)
3760                    // in this case the call will trigger an E_USER_WARNING and the $ret[$key] will be false.
3761                    $ret[$key] = $this->merge_arrays($ret[$key], $value);
3762                } else {
3763                    $ret[$key] = $value;
3764                }
3765            }
3766        }
3767
3768        return $ret;
3769    }
3770
3771    /**
3772     * Gets language information and stores it for later use
3773     *
3774     * @param string The filename of the language file you want to load
3775     * @since 1.0.0
3776     * @access private
3777     * @todo Needs to load keys for lexic permissions for keywords, regexps etc
3778     */
3779    function load_language($file_name) {
3780        if ($file_name == $this->loaded_language) {
3781            // this file is already loaded!
3782            return;
3783        }
3784
3785        //Prepare some stuff before actually loading the language file
3786        $this->loaded_language = $file_name;
3787        $this->parse_cache_built = false;
3788        $this->enable_highlighting();
3789        $language_data = array();
3790
3791        //Load the language file
3792        require $file_name;
3793
3794        // Perhaps some checking might be added here later to check that
3795        // $language data is a valid thing but maybe not
3796        $this->language_data = $language_data;
3797
3798        // Set strict mode if should be set
3799        $this->strict_mode = $this->language_data['STRICT_MODE_APPLIES'];
3800
3801        // Set permissions for all lexics to true
3802        // so they'll be highlighted by default
3803        foreach (array_keys($this->language_data['KEYWORDS']) as $key) {
3804            if (!empty($this->language_data['KEYWORDS'][$key])) {
3805                $this->lexic_permissions['KEYWORDS'][$key] = true;
3806            } else {
3807                $this->lexic_permissions['KEYWORDS'][$key] = false;
3808            }
3809        }
3810
3811        foreach (array_keys($this->language_data['COMMENT_SINGLE']) as $key) {
3812            $this->lexic_permissions['COMMENTS'][$key] = true;
3813        }
3814        foreach (array_keys($this->language_data['REGEXPS']) as $key) {
3815            $this->lexic_permissions['REGEXPS'][$key] = true;
3816        }
3817
3818        // for BenBE and future code reviews:
3819        // we can use empty here since we only check for existance and emptiness of an array
3820        // if it is not an array at all but rather false or null this will work as intended as well
3821        // even if $this->language_data['PARSER_CONTROL'] is undefined this won't trigger a notice
3822        if (!empty($this->language_data['PARSER_CONTROL']['ENABLE_FLAGS'])) {
3823            foreach ($this->language_data['PARSER_CONTROL']['ENABLE_FLAGS'] as $flag => $value) {
3824                // it's either true or false and maybe is true as well
3825                $perm = $value !== GESHI_NEVER;
3826                if ($flag == 'ALL') {
3827                    $this->enable_highlighting($perm);
3828                    continue;
3829                }
3830                if (!isset($this->lexic_permissions[$flag])) {
3831                    // unknown lexic permission
3832                    continue;
3833                }
3834                if (is_array($this->lexic_permissions[$flag])) {
3835                    foreach ($this->lexic_permissions[$flag] as $key => $val) {
3836                        $this->lexic_permissions[$flag][$key] = $perm;
3837                    }
3838                } else {
3839                    $this->lexic_permissions[$flag] = $perm;
3840                }
3841            }
3842            unset($this->language_data['PARSER_CONTROL']['ENABLE_FLAGS']);
3843        }
3844
3845        //Fix: Problem where hardescapes weren't handled if no ESCAPE_CHAR was given
3846        //You need to set one for HARDESCAPES only in this case.
3847        if(!isset($this->language_data['HARDCHAR'])) {
3848            $this->language_data['HARDCHAR'] = $this->language_data['ESCAPE_CHAR'];
3849        }
3850
3851        //NEW in 1.0.8: Allow styles to be loaded from a separate file to override defaults
3852        $style_filename = substr($file_name, 0, -4) . '.style.php';
3853        if (is_readable($style_filename)) {
3854            //Clear any style_data that could have been set before ...
3855            if (isset($style_data)) {
3856                unset($style_data);
3857            }
3858
3859            //Read the Style Information from the style file
3860            include $style_filename;
3861
3862            //Apply the new styles to our current language styles
3863            if (isset($style_data) && is_array($style_data)) {
3864                $this->language_data['STYLES'] =
3865                    $this->merge_arrays($this->language_data['STYLES'], $style_data);
3866            }
3867        }
3868    }
3869
3870    /**
3871     * Takes the parsed code and various options, and creates the HTML
3872     * surrounding it to make it look nice.
3873     *
3874     * @param  string The code already parsed (reference!)
3875     * @since  1.0.0
3876     * @access private
3877     */
3878    function finalise(&$parsed_code) {
3879        // Remove end parts of important declarations
3880        // This is BUGGY!! My fault for bad code: fix coming in 1.2
3881        // @todo Remove this crap
3882        if ($this->enable_important_blocks &&
3883            (strpos($parsed_code, $this->hsc(GESHI_START_IMPORTANT)) === false)) {
3884            $parsed_code = str_replace($this->hsc(GESHI_END_IMPORTANT), '', $parsed_code);
3885        }
3886
3887        // Add HTML whitespace stuff if we're using the <div> header
3888        if ($this->header_type != GESHI_HEADER_PRE && $this->header_type != GESHI_HEADER_PRE_VALID) {
3889            $this->indent($parsed_code);
3890        }
3891
3892        // purge some unnecessary stuff
3893        /** NOTE: memorypeak #1 */
3894        $parsed_code = preg_replace('#<span[^>]+>(\s*)</span>#', '\\1', $parsed_code);
3895
3896        // If we are using IDs for line numbers, there needs to be an overall
3897        // ID set to prevent collisions.
3898        if ($this->add_ids && !$this->overall_id) {
3899            $this->overall_id = 'geshi-' . substr(md5(microtime()), 0, 4);
3900        }
3901
3902        // Get code into lines
3903        /** NOTE: memorypeak #2 */
3904        $code = explode("\n", $parsed_code);
3905        $parsed_code = $this->header();
3906
3907        // If we're using line numbers, we insert <li>s and appropriate
3908        // markup to style them (otherwise we don't need to do anything)
3909        if ($this->line_numbers != GESHI_NO_LINE_NUMBERS && $this->header_type != GESHI_HEADER_PRE_TABLE) {
3910            // If we're using the <pre> header, we shouldn't add newlines because
3911            // the <pre> will line-break them (and the <li>s already do this for us)
3912            $ls = ($this->header_type != GESHI_HEADER_PRE && $this->header_type != GESHI_HEADER_PRE_VALID) ? "\n" : '';
3913
3914            // Foreach line...
3915            for ($i = 0, $n = count($code); $i < $n;) {
3916                //Reset the attributes for a new line ...
3917                $attrs = array();
3918
3919                // Make lines have at least one space in them if they're empty
3920                // BenBE: Checking emptiness using trim instead of relying on blanks
3921                if ('' == trim($code[$i])) {
3922                    $code[$i] = '&nbsp;';
3923                }
3924
3925                // If this is a "special line"...
3926                if ($this->line_numbers == GESHI_FANCY_LINE_NUMBERS &&
3927                    $i % $this->line_nth_row == ($this->line_nth_row - 1)) {
3928                    // Set the attributes to style the line
3929                    if ($this->use_classes) {
3930                        //$attr = ' class="li2"';
3931                        $attrs['class'][] = 'li2';
3932                        $def_attr = ' class="de2"';
3933                    } else {
3934                        //$attr = ' style="' . $this->line_style2 . '"';
3935                        $attrs['style'][] = $this->line_style2;
3936                        // This style "covers up" the special styles set for special lines
3937                        // so that styles applied to special lines don't apply to the actual
3938                        // code on that line
3939                        $def_attr = ' style="' . $this->code_style . '"';
3940                    }
3941                } else {
3942                    if ($this->use_classes) {
3943                        //$attr = ' class="li1"';
3944                        $attrs['class'][] = 'li1';
3945                        $def_attr = ' class="de1"';
3946                    } else {
3947                        //$attr = ' style="' . $this->line_style1 . '"';
3948                        $attrs['style'][] = $this->line_style1;
3949                        $def_attr = ' style="' . $this->code_style . '"';
3950                    }
3951                }
3952
3953                //Check which type of tag to insert for this line
3954                if ($this->header_type == GESHI_HEADER_PRE_VALID) {
3955                    $start = "<pre$def_attr>";
3956                    $end = '</pre>';
3957                } else {
3958                    // Span or div?
3959                    $start = "<div$def_attr>";
3960                    $end = '</div>';
3961                }
3962
3963                ++$i;
3964
3965                // Are we supposed to use ids? If so, add them
3966                if ($this->add_ids) {
3967                    $attrs['id'][] = "$this->overall_id-$i";
3968                }
3969
3970                //Is this some line with extra styles???
3971                if (in_array($i, $this->highlight_extra_lines)) {
3972                    if ($this->use_classes) {
3973                        if (isset($this->highlight_extra_lines_styles[$i])) {
3974                            $attrs['class'][] = "lx$i";
3975                        } else {
3976                            $attrs['class'][] = "ln-xtra";
3977                        }
3978                    } else {
3979                        array_push($attrs['style'], $this->get_line_style($i));
3980                    }
3981                }
3982
3983                // Add in the line surrounded by appropriate list HTML
3984                $attr_string = '';
3985                foreach ($attrs as $key => $attr) {
3986                    $attr_string .= ' ' . $key . '="' . implode(' ', $attr) . '"';
3987                }
3988
3989                $parsed_code .= "<li$attr_string>$start{$code[$i-1]}$end</li>$ls";
3990                unset($code[$i - 1]);
3991            }
3992        } else {
3993            $n = count($code);
3994            if ($this->use_classes) {
3995                $attributes = ' class="de1"';
3996            } else {
3997                $attributes = ' style="'. $this->code_style .'"';
3998            }
3999            if ($this->header_type == GESHI_HEADER_PRE_VALID) {
4000                $parsed_code .= '<pre'. $attributes .'>';
4001            } elseif ($this->header_type == GESHI_HEADER_PRE_TABLE) {
4002                if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4003                    if ($this->use_classes) {
4004                        $attrs = ' class="ln"';
4005                    } else {
4006                        $attrs = ' style="'. $this->table_linenumber_style .'"';
4007                    }
4008                    $parsed_code .= '<td'.$attrs.'><pre'.$attributes.'>';
4009                    // get linenumbers
4010                    // we don't merge it with the for below, since it should be better for
4011                    // memory consumption this way
4012                    // @todo: but... actually it would still be somewhat nice to merge the two loops
4013                    //        the mem peaks are at different positions
4014                    for ($i = 0; $i < $n; ++$i) {
4015                        $close = 0;
4016                        // fancy lines
4017                        if ($this->line_numbers == GESHI_FANCY_LINE_NUMBERS &&
4018                            $i % $this->line_nth_row == ($this->line_nth_row - 1)) {
4019                            // Set the attributes to style the line
4020                            if ($this->use_classes) {
4021                                $parsed_code .= '<span class="xtra li2"><span class="de2">';
4022                            } else {
4023                                // This style "covers up" the special styles set for special lines
4024                                // so that styles applied to special lines don't apply to the actual
4025                                // code on that line
4026                                $parsed_code .= '<span style="display:block;' . $this->line_style2 . '">'
4027                                                  .'<span style="' . $this->code_style .'">';
4028                            }
4029                            $close += 2;
4030                        }
4031                        //Is this some line with extra styles???
4032                        if (in_array($i + 1, $this->highlight_extra_lines)) {
4033                            if ($this->use_classes) {
4034                                if (isset($this->highlight_extra_lines_styles[$i])) {
4035                                    $parsed_code .= "<span class=\"xtra lx$i\">";
4036                                } else {
4037                                    $parsed_code .= "<span class=\"xtra ln-xtra\">";
4038                                }
4039                            } else {
4040                                $parsed_code .= "<span style=\"display:block;" . $this->get_line_style($i) . "\">";
4041                            }
4042                            ++$close;
4043                        }
4044                        $parsed_code .= $this->line_numbers_start + $i;
4045                        if ($close) {
4046                            $parsed_code .= str_repeat('</span>', $close);
4047                        } elseif ($i != $n) {
4048                            $parsed_code .= "\n";
4049                        }
4050                    }
4051                    $parsed_code .= '</pre></td><td'.$attributes.'>';
4052                }
4053                $parsed_code .= '<pre'. $attributes .'>';
4054            }
4055            // No line numbers, but still need to handle highlighting lines extra.
4056            // Have to use divs so the full width of the code is highlighted
4057            $close = 0;
4058            for ($i = 0; $i < $n; ++$i) {
4059                // Make lines have at least one space in them if they're empty
4060                // BenBE: Checking emptiness using trim instead of relying on blanks
4061                if ('' == trim($code[$i])) {
4062                    $code[$i] = '&nbsp;';
4063                }
4064                // fancy lines
4065                if ($this->line_numbers == GESHI_FANCY_LINE_NUMBERS &&
4066                    $i % $this->line_nth_row == ($this->line_nth_row - 1)) {
4067                    // Set the attributes to style the line
4068                    if ($this->use_classes) {
4069                        $parsed_code .= '<span class="xtra li2"><span class="de2">';
4070                    } else {
4071                        // This style "covers up" the special styles set for special lines
4072                        // so that styles applied to special lines don't apply to the actual
4073                        // code on that line
4074                        $parsed_code .= '<span style="display:block;' . $this->line_style2 . '">'
4075                                          .'<span style="' . $this->code_style .'">';
4076                    }
4077                    $close += 2;
4078                }
4079                //Is this some line with extra styles???
4080                if (in_array($i + 1, $this->highlight_extra_lines)) {
4081                    if ($this->use_classes) {
4082                        if (isset($this->highlight_extra_lines_styles[$i])) {
4083                            $parsed_code .= "<span class=\"xtra lx$i\">";
4084                        } else {
4085                            $parsed_code .= "<span class=\"xtra ln-xtra\">";
4086                        }
4087                    } else {
4088                        $parsed_code .= "<span style=\"display:block;" . $this->get_line_style($i) . "\">";
4089                    }
4090                    ++$close;
4091                }
4092
4093                $parsed_code .= $code[$i];
4094
4095                if ($close) {
4096                  $parsed_code .= str_repeat('</span>', $close);
4097                  $close = 0;
4098                }
4099                elseif ($i + 1 < $n) {
4100                    $parsed_code .= "\n";
4101                }
4102                unset($code[$i]);
4103            }
4104
4105            if ($this->header_type == GESHI_HEADER_PRE_VALID || $this->header_type == GESHI_HEADER_PRE_TABLE) {
4106                $parsed_code .= '</pre>';
4107            }
4108            if ($this->header_type == GESHI_HEADER_PRE_TABLE && $this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4109                $parsed_code .= '</td>';
4110            }
4111        }
4112
4113        $parsed_code .= $this->footer();
4114    }
4115
4116    /**
4117     * Creates the header for the code block (with correct attributes)
4118     *
4119     * @return string The header for the code block
4120     * @since  1.0.0
4121     * @access private
4122     */
4123    function header() {
4124        // Get attributes needed
4125        /**
4126         * @todo   Document behaviour change - class is outputted regardless of whether
4127         *         we're using classes or not. Same with style
4128         */
4129        $attributes = ' class="' . $this->_genCSSName($this->language);
4130        if ($this->overall_class != '') {
4131            $attributes .= " ".$this->_genCSSName($this->overall_class);
4132        }
4133        $attributes .= '"';
4134
4135        if ($this->overall_id != '') {
4136            $attributes .= " id=\"{$this->overall_id}\"";
4137        }
4138        if ($this->overall_style != '' && !$this->use_classes) {
4139            $attributes .= ' style="' . $this->overall_style . '"';
4140        }
4141
4142        $ol_attributes = '';
4143
4144        if ($this->line_numbers_start != 1) {
4145            $ol_attributes .= ' start="' . $this->line_numbers_start . '"';
4146        }
4147
4148        // Get the header HTML
4149        $header = $this->header_content;
4150        if ($header) {
4151            if ($this->header_type == GESHI_HEADER_PRE || $this->header_type == GESHI_HEADER_PRE_VALID) {
4152                $header = str_replace("\n", '', $header);
4153            }
4154            $header = $this->replace_keywords($header);
4155
4156            if ($this->use_classes) {
4157                $attr = ' class="head"';
4158            } else {
4159                $attr = " style=\"{$this->header_content_style}\"";
4160            }
4161            if ($this->header_type == GESHI_HEADER_PRE_TABLE && $this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4162                $header = "<thead><tr><td colspan=\"2\" $attr>$header</td></tr></thead>";
4163            } else {
4164                $header = "<div$attr>$header</div>";
4165            }
4166        }
4167
4168        if (GESHI_HEADER_NONE == $this->header_type) {
4169            if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4170                return "$header<ol$attributes$ol_attributes>";
4171            }
4172            return $header . ($this->force_code_block ? '<div>' : '');
4173        }
4174
4175        // Work out what to return and do it
4176        if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4177            if ($this->header_type == GESHI_HEADER_PRE) {
4178                return "<pre$attributes>$header<ol$ol_attributes>";
4179            } elseif ($this->header_type == GESHI_HEADER_DIV ||
4180                $this->header_type == GESHI_HEADER_PRE_VALID) {
4181                return "<div$attributes>$header<ol$ol_attributes>";
4182            } elseif ($this->header_type == GESHI_HEADER_PRE_TABLE) {
4183                return "<table$attributes>$header<tbody><tr class=\"li1\">";
4184            }
4185        } else {
4186            if ($this->header_type == GESHI_HEADER_PRE) {
4187                return "<pre$attributes>$header"  .
4188                    ($this->force_code_block ? '<div>' : '');
4189            } else {
4190                return "<div$attributes>$header" .
4191                    ($this->force_code_block ? '<div>' : '');
4192            }
4193        }
4194    }
4195
4196    /**
4197     * Returns the footer for the code block.
4198     *
4199     * @return string The footer for the code block
4200     * @since  1.0.0
4201     * @access private
4202     */
4203    function footer() {
4204        $footer = $this->footer_content;
4205        if ($footer) {
4206            if ($this->header_type == GESHI_HEADER_PRE) {
4207                $footer = str_replace("\n", '', $footer);;
4208            }
4209            $footer = $this->replace_keywords($footer);
4210
4211            if ($this->use_classes) {
4212                $attr = ' class="foot"';
4213            } else {
4214                $attr = " style=\"{$this->footer_content_style}\"";
4215            }
4216            if ($this->header_type == GESHI_HEADER_PRE_TABLE && $this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4217                $footer = "<tfoot><tr><td colspan=\"2\">$footer</td></tr></tfoot>";
4218            } else {
4219                $footer = "<div$attr>$footer</div>";
4220            }
4221        }
4222
4223        if (GESHI_HEADER_NONE == $this->header_type) {
4224            return ($this->line_numbers != GESHI_NO_LINE_NUMBERS) ? '</ol>' . $footer : $footer;
4225        }
4226
4227        if ($this->header_type == GESHI_HEADER_DIV || $this->header_type == GESHI_HEADER_PRE_VALID) {
4228            if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4229                return "</ol>$footer</div>";
4230            }
4231            return ($this->force_code_block ? '</div>' : '') .
4232                "$footer</div>";
4233        }
4234        elseif ($this->header_type == GESHI_HEADER_PRE_TABLE) {
4235            if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4236                return "</tr></tbody>$footer</table>";
4237            }
4238            return ($this->force_code_block ? '</div>' : '') .
4239                "$footer</div>";
4240        }
4241        else {
4242            if ($this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4243                return "</ol>$footer</pre>";
4244            }
4245            return ($this->force_code_block ? '</div>' : '') .
4246                "$footer</pre>";
4247        }
4248    }
4249
4250    /**
4251     * Replaces certain keywords in the header and footer with
4252     * certain configuration values
4253     *
4254     * @param  string The header or footer content to do replacement on
4255     * @return string The header or footer with replaced keywords
4256     * @since  1.0.2
4257     * @access private
4258     */
4259    function replace_keywords($instr) {
4260        $keywords = $replacements = array();
4261
4262        $keywords[] = '<TIME>';
4263        $keywords[] = '{TIME}';
4264        $replacements[] = $replacements[] = number_format($time = $this->get_time(), 3);
4265
4266        $keywords[] = '<LANGUAGE>';
4267        $keywords[] = '{LANGUAGE}';
4268        $replacements[] = $replacements[] = $this->language_data['LANG_NAME'];
4269
4270        $keywords[] = '<VERSION>';
4271        $keywords[] = '{VERSION}';
4272        $replacements[] = $replacements[] = GESHI_VERSION;
4273
4274        $keywords[] = '<SPEED>';
4275        $keywords[] = '{SPEED}';
4276        if ($time <= 0) {
4277            $speed = 'N/A';
4278        } else {
4279            $speed = strlen($this->source) / $time;
4280            if ($speed >= 1024) {
4281                $speed = sprintf("%.2f KB/s", $speed / 1024.0);
4282            } else {
4283                $speed = sprintf("%.0f B/s", $speed);
4284            }
4285        }
4286        $replacements[] = $replacements[] = $speed;
4287
4288        return str_replace($keywords, $replacements, $instr);
4289    }
4290
4291    /**
4292     * Secure replacement for PHP built-in function htmlspecialchars().
4293     *
4294     * See ticket #427 (http://wush.net/trac/wikka/ticket/427) for the rationale
4295     * for this replacement function.
4296     *
4297     * The INTERFACE for this function is almost the same as that for
4298     * htmlspecialchars(), with the same default for quote style; however, there
4299     * is no 'charset' parameter. The reason for this is as follows:
4300     *
4301     * The PHP docs say:
4302     *      "The third argument charset defines character set used in conversion."
4303     *
4304     * I suspect PHP's htmlspecialchars() is working at the byte-value level and
4305     * thus _needs_ to know (or asssume) a character set because the special
4306     * characters to be replaced could exist at different code points in
4307     * different character sets. (If indeed htmlspecialchars() works at
4308     * byte-value level that goes some  way towards explaining why the
4309     * vulnerability would exist in this function, too, and not only in
4310     * htmlentities() which certainly is working at byte-value level.)
4311     *
4312     * This replacement function however works at character level and should
4313     * therefore be "immune" to character set differences - so no charset
4314     * parameter is needed or provided. If a third parameter is passed, it will
4315     * be silently ignored.
4316     *
4317     * In the OUTPUT there is a minor difference in that we use '&#39;' instead
4318     * of PHP's '&#039;' for a single quote: this provides compatibility with
4319     *      get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES)
4320     * (see comment by mikiwoz at yahoo dot co dot uk on
4321     * http://php.net/htmlspecialchars); it also matches the entity definition
4322     * for XML 1.0
4323     * (http://www.w3.org/TR/xhtml1/dtds.html#a_dtd_Special_characters).
4324     * Like PHP we use a numeric character reference instead of '&apos;' for the
4325     * single quote. For the other special characters we use the named entity
4326     * references, as PHP is doing.
4327     *
4328     * @author      {@link http://wikkawiki.org/JavaWoman Marjolein Katsma}
4329     *
4330     * @license     http://www.gnu.org/copyleft/lgpl.html
4331     *              GNU Lesser General Public License
4332     * @copyright   Copyright 2007, {@link http://wikkawiki.org/CreditsPage
4333     *              Wikka Development Team}
4334     *
4335     * @access      private
4336     * @param       string  $string string to be converted
4337     * @param       integer $quote_style
4338     *                      - ENT_COMPAT:   escapes &, <, > and double quote (default)
4339     *                      - ENT_NOQUOTES: escapes only &, < and >
4340     *                      - ENT_QUOTES:   escapes &, <, >, double and single quotes
4341     * @return      string  converted string
4342     * @since       1.0.7.18
4343     */
4344    function hsc($string, $quote_style = ENT_COMPAT) {
4345        // init
4346        static $aTransSpecchar = array(
4347            '&' => '&amp;',
4348            '"' => '&quot;',
4349            '<' => '&lt;',
4350            '>' => '&gt;',
4351
4352            //This fix is related to SF#1923020, but has to be applied
4353            //regardless of actually highlighting symbols.
4354
4355            //Circumvent a bug with symbol highlighting
4356            //This is required as ; would produce undesirable side-effects if it
4357            //was not to be processed as an entity.
4358            ';' => '<SEMI>', // Force ; to be processed as entity
4359            '|' => '<PIPE>' // Force | to be processed as entity
4360            );                      // ENT_COMPAT set
4361
4362        switch ($quote_style) {
4363            case ENT_NOQUOTES: // don't convert double quotes
4364                unset($aTransSpecchar['"']);
4365                break;
4366            case ENT_QUOTES: // convert single quotes as well
4367                $aTransSpecchar["'"] = '&#39;'; // (apos) htmlspecialchars() uses '&#039;'
4368                break;
4369        }
4370
4371        // return translated string
4372        return strtr($string, $aTransSpecchar);
4373    }
4374
4375    function _genCSSName($name){
4376        return (is_numeric($name[0]) ? '_' : '') . $name;
4377    }
4378
4379    /**
4380     * Returns a stylesheet for the highlighted code. If $economy mode
4381     * is true, we only return the stylesheet declarations that matter for
4382     * this code block instead of the whole thing
4383     *
4384     * @param  boolean Whether to use economy mode or not
4385     * @return string A stylesheet built on the data for the current language
4386     * @since  1.0.0
4387     */
4388    function get_stylesheet($economy_mode = true) {
4389        // If there's an error, chances are that the language file
4390        // won't have populated the language data file, so we can't
4391        // risk getting a stylesheet...
4392        if ($this->error) {
4393            return '';
4394        }
4395
4396        //Check if the style rearrangements have been processed ...
4397        //This also does some preprocessing to check which style groups are useable ...
4398        if(!isset($this->language_data['NUMBERS_CACHE'])) {
4399            $this->build_style_cache();
4400        }
4401
4402        // First, work out what the selector should be. If there's an ID,
4403        // that should be used, the same for a class. Otherwise, a selector
4404        // of '' means that these styles will be applied anywhere
4405        if ($this->overall_id) {
4406            $selector = '#' . $this->_genCSSName($this->overall_id);
4407        } else {
4408            $selector = '.' . $this->_genCSSName($this->language);
4409            if ($this->overall_class) {
4410                $selector .= '.' . $this->_genCSSName($this->overall_class);
4411            }
4412        }
4413        $selector .= ' ';
4414
4415        // Header of the stylesheet
4416        if (!$economy_mode) {
4417            $stylesheet = "/**\n".
4418                " * GeSHi Dynamically Generated Stylesheet\n".
4419                " * --------------------------------------\n".
4420                " * Dynamically generated stylesheet for {$this->language}\n".
4421                " * CSS class: {$this->overall_class}, CSS id: {$this->overall_id}\n".
4422                " * GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2014 Benny Baumann\n" .
4423                " * (http://qbnz.com/highlighter/ and http://geshi.org/)\n".
4424                " * --------------------------------------\n".
4425                " */\n";
4426        } else {
4427            $stylesheet = "/**\n".
4428                " * GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2014 Benny Baumann\n" .
4429                " * (http://qbnz.com/highlighter/ and http://geshi.org/)\n".
4430                " */\n";
4431        }
4432
4433        // Set the <ol> to have no effect at all if there are line numbers
4434        // (<ol>s have margins that should be destroyed so all layout is
4435        // controlled by the set_overall_style method, which works on the
4436        // <pre> or <div> container). Additionally, set default styles for lines
4437        if (!$economy_mode || $this->line_numbers != GESHI_NO_LINE_NUMBERS) {
4438            //$stylesheet .= "$selector, {$selector}ol, {$selector}ol li {margin: 0;}\n";
4439            $stylesheet .= "$selector.de1, $selector.de2 {{$this->code_style}}\n";
4440        }
4441
4442        // Add overall styles
4443        // note: neglect economy_mode, empty styles are meaningless
4444        if ($this->overall_style != '') {
4445            $stylesheet .= "$selector {{$this->overall_style}}\n";
4446        }
4447
4448        // Add styles for links
4449        // note: economy mode does not make _any_ sense here
4450        //       either the style is empty and thus no selector is needed
4451        //       or the appropriate key is given.
4452        foreach ($this->link_styles as $key => $style) {
4453            if ($style != '') {
4454                switch ($key) {
4455                    case GESHI_LINK:
4456                        $stylesheet .= "{$selector}a:link {{$style}}\n";
4457                        break;
4458                    case GESHI_HOVER:
4459                        $stylesheet .= "{$selector}a:hover {{$style}}\n";
4460                        break;
4461                    case GESHI_ACTIVE:
4462                        $stylesheet .= "{$selector}a:active {{$style}}\n";
4463                        break;
4464                    case GESHI_VISITED:
4465                        $stylesheet .= "{$selector}a:visited {{$style}}\n";
4466                        break;
4467                }
4468            }
4469        }
4470
4471        // Header and footer
4472        // note: neglect economy_mode, empty styles are meaningless
4473        if ($this->header_content_style != '') {
4474            $stylesheet .= "$selector.head {{$this->header_content_style}}\n";
4475        }
4476        if ($this->footer_content_style != '') {
4477            $stylesheet .= "$selector.foot {{$this->footer_content_style}}\n";
4478        }
4479
4480        // Styles for important stuff
4481        // note: neglect economy_mode, empty styles are meaningless
4482        if ($this->important_styles != '') {
4483            $stylesheet .= "$selector.imp {{$this->important_styles}}\n";
4484        }
4485
4486        // Simple line number styles
4487        if ((!$economy_mode || $this->line_numbers != GESHI_NO_LINE_NUMBERS) && $this->line_style1 != '') {
4488            $stylesheet .= "{$selector}li, {$selector}.li1 {{$this->line_style1}}\n";
4489        }
4490        if ((!$economy_mode || $this->line_numbers != GESHI_NO_LINE_NUMBERS) && $this->table_linenumber_style != '') {
4491            $stylesheet .= "{$selector}.ln {{$this->table_linenumber_style}}\n";
4492        }
4493        // If there is a style set for fancy line numbers, echo it out
4494        if ((!$economy_mode || $this->line_numbers == GESHI_FANCY_LINE_NUMBERS) && $this->line_style2 != '') {
4495            $stylesheet .= "{$selector}.li2 {{$this->line_style2}}\n";
4496        }
4497
4498        // note: empty styles are meaningless
4499        foreach ($this->language_data['STYLES']['KEYWORDS'] as $group => $styles) {
4500            if ($styles != '' && (!$economy_mode ||
4501                (isset($this->lexic_permissions['KEYWORDS'][$group]) &&
4502                $this->lexic_permissions['KEYWORDS'][$group]))) {
4503                $stylesheet .= "$selector.kw$group {{$styles}}\n";
4504            }
4505        }
4506        foreach ($this->language_data['STYLES']['COMMENTS'] as $group => $styles) {
4507            if ($styles != '' && (!$economy_mode ||
4508                (isset($this->lexic_permissions['COMMENTS'][$group]) &&
4509                $this->lexic_permissions['COMMENTS'][$group]) ||
4510                (!empty($this->language_data['COMMENT_REGEXP']) &&
4511                !empty($this->language_data['COMMENT_REGEXP'][$group])))) {
4512                $stylesheet .= "$selector.co$group {{$styles}}\n";
4513            }
4514        }
4515        foreach ($this->language_data['STYLES']['ESCAPE_CHAR'] as $group => $styles) {
4516            if ($styles != '' && (!$economy_mode || $this->lexic_permissions['ESCAPE_CHAR'])) {
4517                // NEW: since 1.0.8 we have to handle hardescapes
4518                if ($group === 'HARD') {
4519                    $group = '_h';
4520                }
4521                $stylesheet .= "$selector.es$group {{$styles}}\n";
4522            }
4523        }
4524        foreach ($this->language_data['STYLES']['BRACKETS'] as $group => $styles) {
4525            if ($styles != '' && (!$economy_mode || $this->lexic_permissions['BRACKETS'])) {
4526                $stylesheet .= "$selector.br$group {{$styles}}\n";
4527            }
4528        }
4529        foreach ($this->language_data['STYLES']['SYMBOLS'] as $group => $styles) {
4530            if ($styles != '' && (!$economy_mode || $this->lexic_permissions['SYMBOLS'])) {
4531                $stylesheet .= "$selector.sy$group {{$styles}}\n";
4532            }
4533        }
4534        foreach ($this->language_data['STYLES']['STRINGS'] as $group => $styles) {
4535            if ($styles != '' && (!$economy_mode || $this->lexic_permissions['STRINGS'])) {
4536                // NEW: since 1.0.8 we have to handle hardquotes
4537                if ($group === 'HARD') {
4538                    $group = '_h';
4539                }
4540                $stylesheet .= "$selector.st$group {{$styles}}\n";
4541            }
4542        }
4543        foreach ($this->language_data['STYLES']['NUMBERS'] as $group => $styles) {
4544            if ($styles != '' && (!$economy_mode || $this->lexic_permissions['NUMBERS'])) {
4545                $stylesheet .= "$selector.nu$group {{$styles}}\n";
4546            }
4547        }
4548        foreach ($this->language_data['STYLES']['METHODS'] as $group => $styles) {
4549            if ($styles != '' && (!$economy_mode || $this->lexic_permissions['METHODS'])) {
4550                $stylesheet .= "$selector.me$group {{$styles}}\n";
4551            }
4552        }
4553        // note: neglect economy_mode, empty styles are meaningless
4554        foreach ($this->language_data['STYLES']['SCRIPT'] as $group => $styles) {
4555            if ($styles != '') {
4556                $stylesheet .= "$selector.sc$group {{$styles}}\n";
4557            }
4558        }
4559        foreach ($this->language_data['STYLES']['REGEXPS'] as $group => $styles) {
4560            if ($styles != '' && (!$economy_mode ||
4561                (isset($this->lexic_permissions['REGEXPS'][$group]) &&
4562                $this->lexic_permissions['REGEXPS'][$group]))) {
4563                if (is_array($this->language_data['REGEXPS'][$group]) &&
4564                    array_key_exists(GESHI_CLASS, $this->language_data['REGEXPS'][$group])) {
4565                    $stylesheet .= "$selector.";
4566                    $stylesheet .= $this->language_data['REGEXPS'][$group][GESHI_CLASS];
4567                    $stylesheet .= " {{$styles}}\n";
4568                } else {
4569                    $stylesheet .= "$selector.re$group {{$styles}}\n";
4570                }
4571            }
4572        }
4573        // Styles for lines being highlighted extra
4574        if (!$economy_mode || (count($this->highlight_extra_lines)!=count($this->highlight_extra_lines_styles))) {
4575            $stylesheet .= "{$selector}.ln-xtra, {$selector}li.ln-xtra, {$selector}div.ln-xtra {{$this->highlight_extra_lines_style}}\n";
4576        }
4577        $stylesheet .= "{$selector}span.xtra { display:block; }\n";
4578        foreach ($this->highlight_extra_lines_styles as $lineid => $linestyle) {
4579            $stylesheet .= "{$selector}.lx$lineid, {$selector}li.lx$lineid, {$selector}div.lx$lineid {{$linestyle}}\n";
4580        }
4581
4582        return $stylesheet;
4583    }
4584
4585    /**
4586     * Get's the style that is used for the specified line
4587     *
4588     * @param int The line number information is requested for
4589     * @access private
4590     * @since 1.0.7.21
4591     */
4592    function get_line_style($line) {
4593        //$style = null;
4594        $style = null;
4595        if (isset($this->highlight_extra_lines_styles[$line])) {
4596            $style = $this->highlight_extra_lines_styles[$line];
4597        } else { // if no "extra" style assigned
4598            $style = $this->highlight_extra_lines_style;
4599        }
4600
4601        return $style;
4602    }
4603
4604    /**
4605    * this functions creates an optimized regular expression list
4606    * of an array of strings.
4607    *
4608    * Example:
4609    * <code>$list = array('faa', 'foo', 'foobar');
4610    *          => string 'f(aa|oo(bar)?)'</code>
4611    *
4612    * @param $list array of (unquoted) strings
4613    * @param $regexp_delimiter your regular expression delimiter, @see preg_quote()
4614    * @return string for regular expression
4615    * @author Milian Wolff <mail@milianw.de>
4616    * @since 1.0.8
4617    * @access private
4618    */
4619    function optimize_regexp_list($list, $regexp_delimiter = '/') {
4620        $regex_chars = array('.', '\\', '+', '-', '*', '?', '[', '^', ']', '$',
4621            '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', $regexp_delimiter);
4622        sort($list);
4623        $regexp_list = array('');
4624        $num_subpatterns = 0;
4625        $list_key = 0;
4626
4627        // the tokens which we will use to generate the regexp list
4628        $tokens = array();
4629        $prev_keys = array();
4630        // go through all entries of the list and generate the token list
4631        $cur_len = 0;
4632        for ($i = 0, $i_max = count($list); $i < $i_max; ++$i) {
4633            if ($cur_len > GESHI_MAX_PCRE_LENGTH) {
4634                // seems like the length of this pcre is growing exorbitantly
4635                $regexp_list[++$list_key] = $this->_optimize_regexp_list_tokens_to_string($tokens);
4636                $num_subpatterns = substr_count($regexp_list[$list_key], '(?:');
4637                $tokens = array();
4638                $cur_len = 0;
4639            }
4640            $level = 0;
4641            $entry = preg_quote((string) $list[$i], $regexp_delimiter);
4642            $pointer = &$tokens;
4643            // properly assign the new entry to the correct position in the token array
4644            // possibly generate smaller common denominator keys
4645            while (true) {
4646                // get the common denominator
4647                if (isset($prev_keys[$level])) {
4648                    if ($prev_keys[$level] == $entry) {
4649                        // this is a duplicate entry, skip it
4650                        continue 2;
4651                    }
4652                    $char = 0;
4653                    while (isset($entry[$char]) && isset($prev_keys[$level][$char])
4654                            && $entry[$char] == $prev_keys[$level][$char]) {
4655                        ++$char;
4656                    }
4657                    if ($char > 0) {
4658                        // this entry has at least some chars in common with the current key
4659                        if ($char == strlen($prev_keys[$level])) {
4660                            // current key is totally matched, i.e. this entry has just some bits appended
4661                            $pointer = &$pointer[$prev_keys[$level]];
4662                        } else {
4663                            // only part of the keys match
4664                            $new_key_part1 = substr($prev_keys[$level], 0, $char);
4665                            $new_key_part2 = substr($prev_keys[$level], $char);
4666
4667                            if (in_array($new_key_part1[0], $regex_chars)
4668                                || in_array($new_key_part2[0], $regex_chars)) {
4669                                // this is bad, a regex char as first character
4670                                $pointer[$entry] = array('' => true);
4671                                array_splice($prev_keys, $level, count($prev_keys), $entry);
4672                                $cur_len += strlen($entry);
4673                                continue;
4674                            } else {
4675                                // relocate previous tokens
4676                                $pointer[$new_key_part1] = array($new_key_part2 => $pointer[$prev_keys[$level]]);
4677                                unset($pointer[$prev_keys[$level]]);
4678                                $pointer = &$pointer[$new_key_part1];
4679                                // recreate key index
4680                                array_splice($prev_keys, $level, count($prev_keys), array($new_key_part1, $new_key_part2));
4681                                $cur_len += strlen($new_key_part2);
4682                            }
4683                        }
4684                        ++$level;
4685                        $entry = substr($entry, $char);
4686                        continue;
4687                    }
4688                    // else: fall trough, i.e. no common denominator was found
4689                }
4690                if ($level == 0 && !empty($tokens)) {
4691                    // we can dump current tokens into the string and throw them away afterwards
4692                    $new_entry = $this->_optimize_regexp_list_tokens_to_string($tokens);
4693                    $new_subpatterns = substr_count($new_entry, '(?:');
4694                    if (GESHI_MAX_PCRE_SUBPATTERNS && $num_subpatterns + $new_subpatterns > GESHI_MAX_PCRE_SUBPATTERNS) {
4695                        $regexp_list[++$list_key] = $new_entry;
4696                        $num_subpatterns = $new_subpatterns;
4697                    } else {
4698                        if (!empty($regexp_list[$list_key])) {
4699                            $new_entry = '|' . $new_entry;
4700                        }
4701                        $regexp_list[$list_key] .= $new_entry;
4702                        $num_subpatterns += $new_subpatterns;
4703                    }
4704                    $tokens = array();
4705                    $cur_len = 0;
4706                }
4707                // no further common denominator found
4708                $pointer[$entry] = array('' => true);
4709                array_splice($prev_keys, $level, count($prev_keys), $entry);
4710
4711                $cur_len += strlen($entry);
4712                break;
4713            }
4714            unset($list[$i]);
4715        }
4716        // make sure the last tokens get converted as well
4717        $new_entry = $this->_optimize_regexp_list_tokens_to_string($tokens);
4718        if (GESHI_MAX_PCRE_SUBPATTERNS && $num_subpatterns + substr_count($new_entry, '(?:') > GESHI_MAX_PCRE_SUBPATTERNS) {
4719            if ( !empty($regexp_list[$list_key]) ) {
4720              ++$list_key;
4721            }
4722            $regexp_list[$list_key] = $new_entry;
4723        } else {
4724            if (!empty($regexp_list[$list_key])) {
4725                $new_entry = '|' . $new_entry;
4726            }
4727            $regexp_list[$list_key] .= $new_entry;
4728        }
4729        return $regexp_list;
4730    }
4731    /**
4732    * this function creates the appropriate regexp string of an token array
4733    * you should not call this function directly, @see $this->optimize_regexp_list().
4734    *
4735    * @param &$tokens array of tokens
4736    * @param $recursed bool to know wether we recursed or not
4737    * @return string
4738    * @author Milian Wolff <mail@milianw.de>
4739    * @since 1.0.8
4740    * @access private
4741    */
4742    function _optimize_regexp_list_tokens_to_string(&$tokens, $recursed = false) {
4743        $list = '';
4744        foreach ($tokens as $token => $sub_tokens) {
4745            $list .= $token;
4746            $close_entry = isset($sub_tokens['']);
4747            unset($sub_tokens['']);
4748            if (!empty($sub_tokens)) {
4749                $list .= '(?:' . $this->_optimize_regexp_list_tokens_to_string($sub_tokens, true) . ')';
4750                if ($close_entry) {
4751                    // make sub_tokens optional
4752                    $list .= '?';
4753                }
4754            }
4755            $list .= '|';
4756        }
4757        if (!$recursed) {
4758            // do some optimizations
4759            // common trailing strings
4760            // BUGGY!
4761            //$list = preg_replace_callback('#(?<=^|\:|\|)\w+?(\w+)(?:\|.+\1)+(?=\|)#', create_function(
4762            //    '$matches', 'return "(?:" . preg_replace("#" . preg_quote($matches[1], "#") . "(?=\||$)#", "", $matches[0]) . ")" . $matches[1];'), $list);
4763            // (?:p)? => p?
4764            $list = preg_replace('#\(\?\:(.)\)\?#', '\1?', $list);
4765            // (?:a|b|c|d|...)? => [abcd...]?
4766            // TODO: a|bb|c => [ac]|bb
4767            static $callback_2;
4768            if (!isset($callback_2)) {
4769                $callback_2 = create_function('$matches', 'return "[" . str_replace("|", "", $matches[1]) . "]";');
4770            }
4771            $list = preg_replace_callback('#\(\?\:((?:.\|)+.)\)#', $callback_2, $list);
4772        }
4773        // return $list without trailing pipe
4774        return substr($list, 0, -1);
4775    }
4776} // End Class GeSHi
4777
4778
4779if (!function_exists('geshi_highlight')) {
4780    /**
4781     * Easy way to highlight stuff. Behaves just like highlight_string
4782     *
4783     * @param string The code to highlight
4784     * @param string The language to highlight the code in
4785     * @param string The path to the language files. You can leave this blank if you need
4786     *               as from version 1.0.7 the path should be automatically detected
4787     * @param boolean Whether to return the result or to echo
4788     * @return string The code highlighted (if $return is true)
4789     * @since 1.0.2
4790     */
4791    function geshi_highlight($string, $language, $path = null, $return = false) {
4792        $geshi = new GeSHi($string, $language, $path);
4793        $geshi->set_header_type(GESHI_HEADER_NONE);
4794
4795        if ($return) {
4796            return '<code>' . $geshi->parse_code() . '</code>';
4797        }
4798
4799        echo '<code>' . $geshi->parse_code() . '</code>';
4800
4801        if ($geshi->error()) {
4802            return false;
4803        }
4804        return true;
4805    }
4806}
Note: See TracBrowser for help on using the repository browser.