source: projects/documentation/geshi.php @ 37

Last change on this file since 37 was 30, checked in by sven, 15 years ago

Added new documentation as well as a great new layout and some new
features for the documentation system like syntax higlighting (using
geshi).
Furthermore much directory clean up and movements accross directories.

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