source: branches/rel/html/test/kakinaka/js/dpSyntaxHighlighter.js @ 12157

Revision 12157, 19.9 KB checked in by uehara, 17 years ago (diff)
Line 
1/**
2 * Code Syntax Highlighter.
3 * Version 1.2.0
4 * Copyright (C) 2004 Alex Gorbatchev.
5 * http://www.dreamprojections.com/syntaxhighlighter/
6 *
7 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General
8 * Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option)
9 * any later version.
10 *
11 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
12 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
13 * details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to
16 * the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 */
18
19//
20// create namespaces
21//
22var dp = {
23    sh :                        // dp.sh
24    {
25            Utils   : {},       // dp.sh.Utils
26            Brushes : {},       // dp.sh.Brushes
27            Strings : {}
28    },
29    Version : '1.2.0'
30};
31
32dp.sh.Strings = {
33    AboutDialog : '<html><head><title>About...</title></head><body class="dp-about"><table cellspacing="0"><tr><td class="copy"><div class="para title">dp.SyntaxHighlighter</div><div class="para">Version: {V}</div><div class="para"><a href="http://www.dreamprojections.com/sh/?ref=about" target="_blank">http://www.dreamprojections.com/SyntaxHighlighter</a></div>&copy;2004-2005 Alex Gorbatchev. All right reserved.</td></tr><tr><td class="footer"><input type="button" class="close" value="OK" onClick="window.close()"/></td></tr></table></body></html>',
34   
35    // tools
36    ExpandCode : '+ expand code',
37    ViewPlain : 'view plain',
38    Print : '',
39    CopyToClipboard : '',
40    About : '',
41   
42    CopiedToClipboard : 'The code is in your clipboard now.'
43};
44
45dp.SyntaxHighlighter = dp.sh;
46
47//
48// Dialog and toolbar functions
49//
50
51dp.sh.Utils.Expand = function(sender)
52{
53    var table = sender;
54    var span = sender;
55
56    // find the span in which the text label and pipe contained so we can hide it
57    while(span != null && span.tagName != 'SPAN')
58        span = span.parentNode;
59
60    // find the table
61    while(table != null && table.tagName != 'TABLE')
62        table = table.parentNode;
63   
64    // remove the 'expand code' button
65    span.parentNode.removeChild(span);
66   
67    table.tBodies[0].className = 'show';
68    table.parentNode.style.height = '100%'; // containing div isn't getting updated properly when the TBODY is shown
69}
70
71// opens a new windows and puts the original unformatted source code inside.
72dp.sh.Utils.ViewSource = function(sender)
73{
74    var code = sender.parentNode.originalCode;
75    var wnd = window.open('', '_blank', 'width=750, height=400, location=0, resizable=1, menubar=0, scrollbars=1');
76   
77    code = code.replace(/</g, '&lt;');
78   
79    wnd.document.write('<pre>' + code + '</pre>');
80    wnd.document.close();
81}
82
83// copies the original source code in to the clipboard (IE only)
84dp.sh.Utils.ToClipboard = function(sender)
85{
86    var code = sender.parentNode.originalCode;
87   
88    // This works only for IE. There's a way to make it work with Mozilla as well,
89    // but it requires security settings changed on the client, which isn't by
90    // default, so 99% of users won't have it working anyways.
91    if(window.clipboardData)
92    {
93        window.clipboardData.setData('text', code);
94       
95        alert(dp.sh.Strings.CopiedToClipboard);
96    }
97}
98
99// creates an invisible iframe, puts the original source code inside and prints it
100dp.sh.Utils.PrintSource = function(sender)
101{
102    var td      = sender.parentNode;
103    var code    = td.processedCode;
104    var iframe  = document.createElement('IFRAME');
105    var doc     = null;
106    var wnd     =
107
108    // this hides the iframe
109    iframe.style.cssText = 'position:absolute; width:0px; height:0px; left:-5px; top:-5px;';
110   
111    td.appendChild(iframe);
112   
113    doc = iframe.contentWindow.document;
114    code = code.replace(/</g, '&lt;');
115   
116    doc.open();
117    doc.write('<pre>' + code + '</pre>');
118    doc.close();
119   
120    iframe.contentWindow.focus();
121    iframe.contentWindow.print();
122   
123    td.removeChild(iframe);
124}
125
126dp.sh.Utils.About = function()
127{
128    var wnd = window.open('', '_blank', 'dialog,width=320,height=150,scrollbars=0');
129    var doc = wnd.document;
130   
131    var styles = document.getElementsByTagName('style');
132    var links = document.getElementsByTagName('link');
133   
134    doc.write(dp.sh.Strings.AboutDialog.replace('{V}', dp.sh.Version));
135   
136    // copy over ALL the styles from the parent page
137    for(var i = 0; i < styles.length; i++)
138        doc.write('<style>' + styles[i].innerHTML + '</style>');
139
140    for(var i = 0; i < links.length; i++)
141        if(links[i].rel.toLowerCase() == 'stylesheet')
142            doc.write('<link type="text/css" rel="stylesheet" href="' + links[i].href + '"></link>');
143   
144    doc.close();
145    wnd.focus();
146}
147
148//
149// Match object
150//
151dp.sh.Match = function(value, index, css)
152{
153    this.value      = value;
154    this.index      = index;
155    this.length     = value.length;
156    this.css        = css;
157}
158
159//
160// Highlighter object
161//
162dp.sh.Highlighter = function()
163{
164    this.addGutter = true;
165    this.addControls = true;
166    this.collapse = false;
167    this.tabsToSpaces = true;
168}
169
170// static callback for the match sorting
171dp.sh.Highlighter.SortCallback = function(m1, m2)
172{
173    // sort matches by index first
174    if(m1.index < m2.index)
175        return -1;
176    else if(m1.index > m2.index)
177        return 1;
178    else
179    {
180        // if index is the same, sort by length
181        if(m1.length < m2.length)
182            return -1;
183        else if(m1.length > m2.length)
184            return 1;
185    }
186    return 0;
187}
188
189// gets a list of all matches for a given regular expression
190dp.sh.Highlighter.prototype.GetMatches = function(regex, css)
191{
192    var index = 0;
193    var match = null;
194
195    while((match = regex.exec(this.code)) != null)
196    {
197        this.matches[this.matches.length] = new dp.sh.Match(match[0], match.index, css);
198    }
199}
200
201dp.sh.Highlighter.prototype.AddBit = function(str, css)
202{
203    var span = document.createElement('span');
204   
205    str = str.replace(/&/g, '&amp;');
206    str = str.replace(/ /g, '&nbsp;');
207    str = str.replace(/</g, '&lt;');
208    str = str.replace(/\n/gm, '&nbsp;<br>');
209
210    // when adding a piece of code, check to see if it has line breaks in it
211    // and if it does, wrap individual line breaks with span tags
212    if(css != null)
213    {
214        var regex = new RegExp('<br>', 'gi');
215       
216        if(regex.test(str))
217        {
218            var lines = str.split('&nbsp;<br>');
219           
220            str = '';
221           
222            for(var i = 0; i < lines.length; i++)
223            {
224                span            = document.createElement('SPAN');
225                span.className  = css;
226                span.innerHTML  = lines[i];
227               
228                this.div.appendChild(span);
229               
230                // don't add a <BR> for the last line
231                if(i + 1 < lines.length)
232                    this.div.appendChild(document.createElement('BR'));
233            }
234        }
235        else
236        {
237            span.className = css;
238            span.innerHTML = str;
239            this.div.appendChild(span);
240        }
241    }
242    else
243    {
244        span.innerHTML = str;
245        this.div.appendChild(span);
246    }
247}
248
249// checks if one match is inside any other match
250dp.sh.Highlighter.prototype.IsInside = function(match)
251{
252    if(match == null || match.length == 0)
253        return;
254   
255    for(var i = 0; i < this.matches.length; i++)
256    {
257        var c = this.matches[i];
258       
259        if(c == null)
260            continue;
261       
262        if((match.index > c.index) && (match.index <= c.index + c.length))
263            return true;
264    }
265   
266    return false;
267}
268
269dp.sh.Highlighter.prototype.ProcessRegexList = function()
270{
271    for(var i = 0; i < this.regexList.length; i++)
272        this.GetMatches(this.regexList[i].regex, this.regexList[i].css);
273}
274
275dp.sh.Highlighter.prototype.ProcessSmartTabs = function(code)
276{
277    var lines   = code.split('\n');
278    var result  = '';
279    var tabSize = 4;
280    var tab     = '\t';
281
282    // This function inserts specified amount of spaces in the string
283    // where a tab is while removing that given tab.
284    function InsertSpaces(line, pos, count)
285    {
286        var left    = line.substr(0, pos);
287        var right   = line.substr(pos + 1, line.length);    // pos + 1 will get rid of the tab
288        var spaces  = '';
289       
290        for(var i = 0; i < count; i++)
291            spaces += ' ';
292       
293        return left + spaces + right;
294    }
295
296    // This function process one line for 'smart tabs'
297    function ProcessLine(line, tabSize)
298    {
299        if(line.indexOf(tab) == -1)
300            return line;
301
302        var pos = 0;
303
304        while((pos = line.indexOf(tab)) != -1)
305        {
306            // This is pretty much all there is to the 'smart tabs' logic.
307            // Based on the position within the line and size of a tab,
308            // calculate the amount of spaces we need to insert.
309            var spaces = tabSize - pos % tabSize;
310           
311            line = InsertSpaces(line, pos, spaces);
312        }
313       
314        return line;
315    }
316
317    // Go through all the lines and do the 'smart tabs' magic.
318    for(var i = 0; i < lines.length; i++)
319        result += ProcessLine(lines[i], tabSize) + '\n';
320   
321    return result;
322}
323
324dp.sh.Highlighter.prototype.SwitchToTable = function()
325{
326    // Safari fix: for some reason lowercase <br> isn't getting picked up, even though 'i' is set
327    var lines   = this.div.innerHTML.split(/<BR>/gi);
328    var row     = null;
329    var cell    = null;
330    var tBody   = null;
331    var html    = '';
332    var pipe    = ' | ';
333
334    // creates an anchor to a utility
335    function UtilHref(util, text)
336    {
337        return '<a href="#" onclick="dp.sh.Utils.' + util + '(this); return false;">' + text + '</a>';
338    }
339   
340    tBody = document.createElement('TBODY');    // can be created and all others go to tBodies collection.
341
342    this.table.appendChild(tBody);
343       
344    if(this.addGutter == true)
345    {
346        row = tBody.insertRow(-1);
347        cell = row.insertCell(-1);
348        cell.className = 'tools-corner';
349    }
350
351    if(this.addControls == true)
352    {
353        var tHead = document.createElement('THEAD');    // controls will be placed in here
354        this.table.appendChild(tHead);
355
356        row = tHead.insertRow(-1);
357
358        // add corner if there's a gutter
359        if(this.addGutter == true)
360        {
361            cell = row.insertCell(-1);
362            cell.className = 'tools-corner';
363        }
364       
365        cell = row.insertCell(-1);
366       
367        // preserve some variables for the controls
368        cell.originalCode = this.originalCode;
369        cell.processedCode = this.code;
370        cell.className = 'tools';
371       
372        if(this.collapse == true)
373        {
374            tBody.className = 'hide';
375            cell.innerHTML += '<span><b>' + UtilHref('Expand', dp.sh.Strings.ExpandCode) + '</b>' + pipe + '</span>';
376        }
377
378        cell.innerHTML += UtilHref('ViewSource', dp.sh.Strings.ViewPlain) ;
379       
380        // IE has this clipboard object which is easy enough to use
381        if(window.clipboardData)
382            cell.innerHTML += pipe + UtilHref('ToClipboard', dp.sh.Strings.CopyToClipboard);
383    }
384
385    for(var i = 0; i < lines.length - 1; i++)
386    {
387        row = tBody.insertRow(-1);
388       
389        if(this.addGutter == true)
390        {
391            cell = row.insertCell(-1);
392            cell.className = 'gutter';
393            cell.innerHTML = i + 1;
394        }
395
396        cell = row.insertCell(-1);
397        cell.className = 'line' + (i % 2 + 1);      // uses .line1 and .line2 css styles for alternating lines
398        cell.innerHTML = lines[i];
399    }
400   
401    this.div.innerHTML  = '';
402}
403
404dp.sh.Highlighter.prototype.Highlight = function(code)
405{
406    function Trim(str)
407    {
408        return str.replace(/^\s*(.*?)[\s\n]*$/g, '$1');
409    }
410   
411    function Chop(str)
412    {
413        return str.replace(/\n*$/, '').replace(/^\n*/, '');
414    }
415
416    function Unindent(str)
417    {
418        var lines = str.split('\n');
419        var indents = new Array();
420        var regex = new RegExp('^\\s*', 'g');
421        var min = 1000;
422
423        // go through every line and check for common number of indents
424        for(var i = 0; i < lines.length && min > 0; i++)
425        {
426            if(Trim(lines[i]).length == 0)
427                continue;
428               
429            var matches = regex.exec(lines[i]);
430
431            if(matches != null && matches.length > 0)
432                min = Math.min(matches[0].length, min);
433        }
434
435        // trim minimum common number of white space from the begining of every line
436        if(min > 0)
437            for(var i = 0; i < lines.length; i++)
438                lines[i] = lines[i].substr(min);
439
440        return lines.join('\n');
441    }
442   
443    // This function returns a portions of the string from pos1 to pos2 inclusive
444    function Copy(string, pos1, pos2)
445    {
446        return string.substr(pos1, pos2 - pos1);
447    }
448
449    var pos = 0;
450   
451    this.originalCode = code;
452    this.code = Chop(Unindent(code));
453    this.div = document.createElement('DIV');
454    this.table = document.createElement('TABLE');
455    this.matches = new Array();
456
457    if(this.CssClass != null)
458        this.table.className = this.CssClass;
459
460    // replace tabs with spaces
461    if(this.tabsToSpaces == true)
462        this.code = this.ProcessSmartTabs(this.code);
463
464    this.table.border = 0;
465    this.table.cellSpacing = 0;
466    this.table.cellPadding = 0;
467
468    this.ProcessRegexList();   
469
470    // if no matches found, add entire code as plain text
471    if(this.matches.length == 0)
472    {
473        this.AddBit(this.code, null);
474        this.SwitchToTable();
475        return;
476    }
477
478    // sort the matches
479    this.matches = this.matches.sort(dp.sh.Highlighter.SortCallback);
480
481    // The following loop checks to see if any of the matches are inside
482    // of other matches. This process would get rid of highligting strings
483    // inside comments, keywords inside strings and so on.
484    for(var i = 0; i < this.matches.length; i++)
485        if(this.IsInside(this.matches[i]))
486            this.matches[i] = null;
487
488    // Finally, go through the final list of matches and pull the all
489    // together adding everything in between that isn't a match.
490    for(var i = 0; i < this.matches.length; i++)
491    {
492        var match = this.matches[i];
493
494        if(match == null || match.length == 0)
495            continue;
496       
497        this.AddBit(Copy(this.code, pos, match.index), null);
498        this.AddBit(match.value, match.css);
499       
500        pos = match.index + match.length;
501    }
502   
503    this.AddBit(this.code.substr(pos), null);
504
505    this.SwitchToTable();
506}
507
508dp.sh.Highlighter.prototype.GetKeywords = function(str)
509{
510    return '\\b' + str.replace(/ /g, '\\b|\\b') + '\\b';
511}
512
513// highlightes all elements identified by name and gets source code from specified property
514dp.sh.HighlightAll = function(name, showGutter /* optional */, showControls /* optional */, collapseAll /* optional */)
515{
516    function FindValue()
517    {
518        var a = arguments;
519       
520        for(var i = 0; i < a.length; i++)
521        {
522            if(a[i] == null)
523                continue;
524               
525            if(typeof(a[i]) == 'string' && a[i] != '')
526                return a[i] + '';
527       
528            if(typeof(a[i]) == 'object' && a[i].value != '')
529                return a[i].value + '';
530        }
531       
532        return null;
533    }
534   
535    function IsOptionSet(value, list)
536    {
537        for(var i = 0; i < list.length; i++)
538            if(list[i] == value)
539                return true;
540       
541        return false;
542    }
543
544    var elements = document.getElementsByName(name);
545    var highlighter = null;
546    var registered = new Object();
547    var propertyName = 'value';
548   
549    // if no code blocks found, leave
550    if(elements == null)
551        return;
552
553    // register all brushes
554    for(var brush in dp.sh.Brushes)
555    {
556        var aliases = dp.sh.Brushes[brush].Aliases;
557       
558        if(aliases == null)
559            continue;
560       
561        for(var i = 0; i < aliases.length; i++)
562            registered[aliases[i]] = brush;
563    }
564
565    for(var i = 0; i < elements.length; i++)
566    {
567        var element = elements[i];
568        var options = FindValue(
569                element.attributes['class'], element.className,
570                element.attributes['language'], element.language
571                );
572        var language = '';
573       
574        if(options == null)
575            continue;
576       
577        options = options.split(':');
578       
579        language = options[0].toLowerCase();
580       
581        if(registered[language] == null)
582            continue;
583       
584        // instantiate a brush
585        highlighter = new dp.sh.Brushes[registered[language]]();
586       
587        // hide the original element
588        element.style.display = 'none';
589
590        highlighter.addGutter = (showGutter == null) ? !IsOptionSet('nogutter', options) : showGutter;
591        highlighter.addControls = (showControls == null) ? !IsOptionSet('nocontrols', options) : showControls;
592        highlighter.collapse = (collapseAll == null) ? IsOptionSet('collapse', options) : collapseAll;
593       
594        highlighter.Highlight(element[propertyName]);
595
596        // place the result table inside a div
597        var div = document.createElement('DIV');
598       
599        div.className = 'dp-highlighter';
600        div.appendChild(highlighter.table);
601
602        element.parentNode.insertBefore(div, element);     
603    }   
604}
605
606
607dp.sh.Brushes.JScript = function()
608{
609    var keywords =  'abstract boolean break byte case catch char class const continue debugger ' +
610                    'default delete do double else enum export extends false final finally float ' +
611                    'for function goto if implements import in instanceof int interface long native ' +
612                    'new null package private protected public return short static super switch ' +
613                    'synchronized this throw throws transient true try typeof var void volatile while with';
614
615    this.regexList = [
616        { regex: new RegExp('//.*$', 'gm'),                         css: 'comment' },           // one line comments
617        { regex: new RegExp('/\\*[\\s\\S]*?\\*/', 'g'),             css: 'comment' },           // multiline comments
618        { regex: new RegExp('"(?:[^"\n]|[\"])*"', 'g'),             css: 'string' },            // double quoted strings
619        { regex: new RegExp("'(?:[^'\n]|[\'])*'", 'g'),             css: 'string' },            // single quoted strings
620        { regex: new RegExp('^\\s*#.*', 'gm'),                      css: 'preprocessor' },      // preprocessor tags like #region and #endregion
621        { regex: new RegExp(this.GetKeywords(keywords), 'gm'),      css: 'keyword' }            // keywords
622        ];
623
624    this.CssClass = 'dp-c';
625}
626
627dp.sh.Brushes.JScript.prototype = new dp.sh.Highlighter();
628dp.sh.Brushes.JScript.Aliases   = ['js', 'jscript', 'javascript'];
629
630
631dp.sh.Brushes.Php = function()
632{
633    var keywords =  'and or xor __FILE__ __LINE__ array as break case ' +
634                    'cfunction class const continue declare default die do echo else ' +
635                    'elseif empty enddeclare endfor endforeach endif endswitch endwhile eval exit ' +
636                    'extends for foreach function global if include include_once isset list ' +
637                    'new old_function print require require_once return static switch unset use ' +
638                    'var while __FUNCTION__ __CLASS__';
639
640    this.regexList = [
641        { regex: new RegExp('//.*$', 'gm'),                         css: 'comment' },           // one line comments
642        { regex: new RegExp('/\\*[\\s\\S]*?\\*/', 'g'),             css: 'comment' },           // multiline comments
643        { regex: new RegExp('"(?:[^"\n]|[\"])*"', 'g'),             css: 'string' },            // double quoted strings
644        { regex: new RegExp("'(?:[^'\n]|[\'])*'", 'g'),             css: 'string' },            // single quoted strings
645        { regex: new RegExp('\\$\\w+', 'g'),                        css: 'vars' },              // variables
646        { regex: new RegExp(this.GetKeywords(keywords), 'gm'),      css: 'keyword' }            // keyword
647        ];
648
649    this.CssClass = 'dp-c';
650}
651
652dp.sh.Brushes.Php.prototype = new dp.sh.Highlighter();
653dp.sh.Brushes.Php.Aliases   = ['php'];
654
655
656
657dp.sh.Brushes.Xml = function()
658{
659    this.CssClass = 'dp-xml';
660}
661
662dp.sh.Brushes.Xml.prototype = new dp.sh.Highlighter();
663dp.sh.Brushes.Xml.Aliases   = ['xml', 'xhtml', 'xslt', 'html', 'xhtml'];
664
665dp.sh.Brushes.Xml.prototype.ProcessRegexList = function()
666{
667    function push(array, value)
668    {
669        array[array.length] = value;
670    }
671   
672    /* If only there was a way to get index of a group within a match, the whole XML
673       could be matched with the expression looking something like that:
674   
675       (<!\[CDATA\[\s*.*\s*\]\]>)
676       | (<!--\s*.*\s*?-->)
677       | (<)*(\w+)*\s*(\w+)\s*=\s*(".*?"|'.*?'|\w+)(/*>)*
678       | (</?)(.*?)(/?>)
679    */
680    var index   = 0;
681    var match   = null;
682    var regex   = null;
683
684    // Match CDATA in the following format <![ ... [ ... ]]>
685    // <\!\[[\w\s]*?\[(.|\s)*?\]\]>
686    this.GetMatches(new RegExp('<\\!\\[[\\w\\s]*?\\[(.|\\s)*?\\]\\]>', 'gm'), 'cdata');
687   
688    // Match comments
689    // <!--\s*.*\s*?-->
690    this.GetMatches(new RegExp('<!--\\s*.*\\s*?-->', 'gm'), 'comments');
691
692    // Match attributes and their values
693    // (\w+)\s*=\s*(".*?"|\'.*?\'|\w+)*
694    regex = new RegExp('([\\w-\.]+)\\s*=\\s*(".*?"|\'.*?\'|\\w+)*', 'gm');
695    while((match = regex.exec(this.code)) != null)
696    {
697        push(this.matches, new dp.sh.Match(match[1], match.index, 'attribute'));
698   
699        // if xml is invalid and attribute has no property value, ignore it
700        if(match[2] != undefined)
701        {
702            push(this.matches, new dp.sh.Match(match[2], match.index + match[0].indexOf(match[2]), 'attribute-value'));
703        }
704    }
705
706    // Match opening and closing tag brackets
707    // </*\?*(?!\!)|/*\?*>
708    this.GetMatches(new RegExp('</*\\?*(?!\\!)|/*\\?*>', 'gm'), 'tag');
709
710    // Match tag names
711    // </*\?*\s*(\w+)
712    regex = new RegExp('</*\\?*\\s*([\\w-\.]+)', 'gm');
713    while((match = regex.exec(this.code)) != null)
714    {
715        push(this.matches, new dp.sh.Match(match[1], match.index + match[0].indexOf(match[1]), 'tag-name'));
716    }
717}
718
719
720dp.sh.Brushes.CSS = function()
721{
722    var keywords =  'link over active visited';
723
724    this.regexList = [
725        { regex: new RegExp('/\\*[\\s\\S]*?\\*/', 'g'),             css: 'comment' },           // multiline comments
726        { regex: new RegExp('"(?:[^"\n]|[\"])*"', 'g'),             css: 'string' },            // double quoted strings
727        { regex: new RegExp("'(?:[^'\n]|[\'])*'", 'g'),             css: 'string' },            // single quoted strings
728        { regex: new RegExp('^\\s*#.*', 'gm'),                      css: 'preprocessor' },      // preprocessor tags like #region and #endregion
729        { regex: new RegExp(this.GetKeywords(keywords), 'gm'),      css: 'keyword' }            // keywords
730        ];
731
732    this.CssClass = 'dp-c';
733}
734
735dp.sh.Brushes.CSS.prototype = new dp.sh.Highlighter();
736dp.sh.Brushes.CSS.Aliases   = ['css'];
Note: See TracBrowser for help on using the repository browser.