source: Dev/trunk/src/client/dojox/highlight/_base.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 12.4 KB
Line 
1define([
2        "dojo/_base/lang",
3        "dojo/_base/array",
4        "dojo/dom",
5        "dojo/dom-class"
6], function(lang, array, dom, domClass){
7
8
9        var dh = lang.getObject("dojox.highlight", true),
10                C_NUMBER_RE = '\\b(0x[A-Za-z0-9]+|\\d+(\\.\\d+)?)'
11        ;
12        /*=====
13         dh = {
14                 // summary:
15                 //             Syntax highlighting with language auto-detection package
16                 // description:
17                 //             Syntax highlighting with language auto-detection package.
18                 //             Released under CLA by the Dojo Toolkit, original BSD release
19                 //             available from: http://softwaremaniacs.org/soft/highlight/
20         };
21         =====*/
22
23        dh.languages = dh.languages || {};
24        // constants
25
26        dh.constants = {
27                IDENT_RE: '[a-zA-Z][a-zA-Z0-9_]*',
28                UNDERSCORE_IDENT_RE: '[a-zA-Z_][a-zA-Z0-9_]*',
29                NUMBER_RE: '\\b\\d+(\\.\\d+)?',
30                C_NUMBER_RE: C_NUMBER_RE,
31                // Common modes
32                APOS_STRING_MODE: {
33                        className: 'string',
34                        begin: '\'', end: '\'',
35                        illegal: '\\n',
36                        contains: ['escape'],
37                        relevance: 0
38                },
39                QUOTE_STRING_MODE: {
40                        className: 'string',
41                        begin: '"',
42                        end: '"',
43                        illegal: '\\n',
44                        contains: ['escape'],
45                        relevance: 0
46                },
47                BACKSLASH_ESCAPE: {
48                        className: 'escape',
49                        begin: '\\\\.', end: '^',
50                        relevance: 0
51                },
52                C_LINE_COMMENT_MODE: {
53                        className: 'comment',
54                        begin: '//', end: '$',
55                        relevance: 0
56                },
57                C_BLOCK_COMMENT_MODE: {
58                        className: 'comment',
59                        begin: '/\\*', end: '\\*/'
60                },
61                HASH_COMMENT_MODE: {
62                        className: 'comment',
63                        begin: '#', end: '$'
64                },
65                C_NUMBER_MODE: {
66                        className: 'number',
67                        begin: C_NUMBER_RE, end: '^',
68                        relevance: 0
69                }
70        };
71
72        // utilities
73       
74        function esc(value){
75                return value.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
76        }
77       
78        function verifyText(block){
79                return array.every(block.childNodes, function(node){
80                        return node.nodeType == 3 || String(node.nodeName).toLowerCase() == 'br';
81                });
82        }
83
84        function blockText(block){
85                var result = [];
86                array.forEach(block.childNodes, function(node){
87                        if(node.nodeType == 3){
88                                result.push(node.nodeValue);
89                        }else if(String(node.nodeName).toLowerCase() == 'br'){
90                                result.push("\n");
91                        }else{
92                                throw 'Complex markup';
93                        }
94                });
95                return result.join("");
96        }
97
98        function buildKeywordGroups(mode){
99                if(!mode.keywordGroups){
100                        for(var key in mode.keywords){
101                                var kw = mode.keywords[key];
102                        if(kw instanceof Object){  // dojo.isObject?
103                                        mode.keywordGroups = mode.keywords;
104                                }else{
105                                        mode.keywordGroups = {keyword: mode.keywords};
106                                }
107                                break;
108                        }
109                }
110        }
111       
112        function buildKeywords(language){
113                if(language.defaultMode && language.modes){
114                        buildKeywordGroups(language.defaultMode);
115                        array.forEach(language.modes, buildKeywordGroups);
116                }
117        }
118       
119        // main object
120
121        var Highlighter = function(langName, textBlock){
122                // initialize the state
123                this.langName = langName;
124                this.lang = dh.languages[langName];
125                this.modes = [this.lang.defaultMode];
126                this.relevance = 0;
127                this.keywordCount = 0;
128                this.result = [];
129               
130                // build resources lazily
131                if(!this.lang.defaultMode.illegalRe){
132                        this.buildRes();
133                        buildKeywords(this.lang);
134                }
135               
136                // run the algorithm
137                try{
138                        this.highlight(textBlock);
139                        this.result = this.result.join("");
140                }catch(e){
141                        if(e == 'Illegal'){
142                                this.relevance = 0;
143                                this.keywordCount = 0;
144                                this.partialResult = this.result.join("");
145                                this.result = esc(textBlock);
146                        }else{
147                                throw e;
148                        }
149                }
150        };
151
152        lang.extend(Highlighter, {
153                buildRes: function(){
154                        array.forEach(this.lang.modes, function(mode){
155                                if(mode.begin){
156                                        mode.beginRe = this.langRe('^' + mode.begin);
157                                }
158                                if(mode.end){
159                                        mode.endRe = this.langRe('^' + mode.end);
160                                }
161                                if(mode.illegal){
162                                        mode.illegalRe = this.langRe('^(?:' + mode.illegal + ')');
163                                }
164                        }, this);
165                        this.lang.defaultMode.illegalRe = this.langRe('^(?:' + this.lang.defaultMode.illegal + ')');
166                },
167               
168                subMode: function(lexeme){
169                        var classes = this.modes[this.modes.length - 1].contains;
170                        if(classes){
171                                var modes = this.lang.modes;
172                                for(var i = 0; i < classes.length; ++i){
173                                        var className = classes[i];
174                                        for(var j = 0; j < modes.length; ++j){
175                                                var mode = modes[j];
176                                                if(mode.className == className && mode.beginRe.test(lexeme)){ return mode; }
177                                        }
178                                }
179                        }
180                        return null;
181                },
182
183                endOfMode: function(lexeme){
184                        for(var i = this.modes.length - 1; i >= 0; --i){
185                                var mode = this.modes[i];
186                                if(mode.end && mode.endRe.test(lexeme)){ return this.modes.length - i; }
187                                if(!mode.endsWithParent){ break; }
188                        }
189                        return 0;
190                },
191
192                isIllegal: function(lexeme){
193                        var illegalRe = this.modes[this.modes.length - 1].illegalRe;
194                        return illegalRe && illegalRe.test(lexeme);
195                },
196
197
198                langRe: function(value, global){
199                        var mode =  'm' + (this.lang.case_insensitive ? 'i' : '') + (global ? 'g' : '');
200                        return new RegExp(value, mode);
201                },
202       
203                buildTerminators: function(){
204                        var mode = this.modes[this.modes.length - 1],
205                                terminators = {};
206                        if(mode.contains){
207                                array.forEach(this.lang.modes, function(lmode){
208                                        if(array.indexOf(mode.contains, lmode.className) >= 0){
209                                                terminators[lmode.begin] = 1;
210                                        }
211                                });
212                        }
213                        for(var i = this.modes.length - 1; i >= 0; --i){
214                                var m = this.modes[i];
215                                if(m.end){ terminators[m.end] = 1; }
216                                if(!m.endsWithParent){ break; }
217                        }
218                        if(mode.illegal){ terminators[mode.illegal] = 1; }
219                        var t = [];
220                        for(i in terminators){ t.push(i); }
221                        mode.terminatorsRe = this.langRe("(" + t.join("|") + ")");
222                },
223
224                eatModeChunk: function(value, index){
225                        var mode = this.modes[this.modes.length - 1];
226                       
227                        // create terminators lazily
228                        if(!mode.terminatorsRe){
229                                this.buildTerminators();
230                        }
231       
232                        value = value.substr(index);
233                        var match = mode.terminatorsRe.exec(value);
234                        if(!match){
235                                return {
236                                        buffer: value,
237                                        lexeme: "",
238                                        end:    true
239                                };
240                        }
241                        return {
242                                buffer: match.index ? value.substr(0, match.index) : "",
243                                lexeme: match[0],
244                                end:    false
245                        };
246                },
247       
248                keywordMatch: function(mode, match){
249                        var matchStr = match[0];
250                        if(this.lang.case_insensitive){ matchStr = matchStr.toLowerCase(); }
251                        for(var className in mode.keywordGroups){
252                                if(matchStr in mode.keywordGroups[className]){ return className; }
253                        }
254                        return "";
255                },
256               
257                buildLexemes: function(mode){
258                        var lexemes = {};
259                        array.forEach(mode.lexems, function(lexeme){
260                                lexemes[lexeme] = 1;
261                        });
262                        var t = [];
263                        for(var i in lexemes){ t.push(i); }
264                        mode.lexemsRe = this.langRe("(" + t.join("|") + ")", true);
265                },
266       
267                processKeywords: function(buffer){
268                        var mode = this.modes[this.modes.length - 1];
269                        if(!mode.keywords || !mode.lexems){
270                                return esc(buffer);
271                        }
272                       
273                        // create lexemes lazily
274                        if(!mode.lexemsRe){
275                                this.buildLexemes(mode);
276                        }
277                       
278                        mode.lexemsRe.lastIndex = 0;
279                        var result = [], lastIndex = 0,
280                                match = mode.lexemsRe.exec(buffer);
281                        while(match){
282                                result.push(esc(buffer.substr(lastIndex, match.index - lastIndex)));
283                                var keywordM = this.keywordMatch(mode, match);
284                                if(keywordM){
285                                        ++this.keywordCount;
286                                        result.push('<span class="'+ keywordM +'">' + esc(match[0]) + '</span>');
287                                }else{
288                                        result.push(esc(match[0]));
289                                }
290                                lastIndex = mode.lexemsRe.lastIndex;
291                                match = mode.lexemsRe.exec(buffer);
292                        }
293                        result.push(esc(buffer.substr(lastIndex, buffer.length - lastIndex)));
294                        return result.join("");
295                },
296       
297                processModeInfo: function(buffer, lexeme, end) {
298                        var mode = this.modes[this.modes.length - 1];
299                        if(end){
300                                this.result.push(this.processKeywords(mode.buffer + buffer));
301                                return;
302                        }
303                        if(this.isIllegal(lexeme)){ throw 'Illegal'; }
304                        var newMode = this.subMode(lexeme);
305                        if(newMode){
306                                mode.buffer += buffer;
307                                this.result.push(this.processKeywords(mode.buffer));
308                                if(newMode.excludeBegin){
309                                        this.result.push(lexeme + '<span class="' + newMode.className + '">');
310                                        newMode.buffer = '';
311                                }else{
312                                        this.result.push('<span class="' + newMode.className + '">');
313                                        newMode.buffer = lexeme;
314                                }
315                                this.modes.push(newMode);
316                                this.relevance += typeof newMode.relevance == "number" ? newMode.relevance : 1;
317                                return;
318                        }
319                        var endLevel = this.endOfMode(lexeme);
320                        if(endLevel){
321                                mode.buffer += buffer;
322                                if(mode.excludeEnd){
323                                        this.result.push(this.processKeywords(mode.buffer) + '</span>' + lexeme);
324                                }else{
325                                        this.result.push(this.processKeywords(mode.buffer + lexeme) + '</span>');
326                                }
327                                while(endLevel > 1){
328                                        this.result.push('</span>');
329                                        --endLevel;
330                                        this.modes.pop();
331                                }
332                                this.modes.pop();
333                                this.modes[this.modes.length - 1].buffer = '';
334                                return;
335                        }
336                },
337       
338                highlight: function(value){
339                        var index = 0;
340                        this.lang.defaultMode.buffer = '';
341                        do{
342                                var modeInfo = this.eatModeChunk(value, index);
343                                this.processModeInfo(modeInfo.buffer, modeInfo.lexeme, modeInfo.end);
344                                index += modeInfo.buffer.length + modeInfo.lexeme.length;
345                        }while(!modeInfo.end);
346                        if(this.modes.length > 1){
347                                throw 'Illegal';
348                        }
349                }
350        });
351       
352        // more utilities
353       
354        function replaceText(node, className, text){
355                if(String(node.tagName).toLowerCase() == "code" && String(node.parentNode.tagName).toLowerCase() == "pre"){
356                        // See these 4 lines? This is IE's notion of "node.innerHTML = text". Love this browser :-/
357                        var container = document.createElement('div'),
358                                environment = node.parentNode.parentNode;
359                        container.innerHTML = '<pre><code class="' + className + '">' + text + '</code></pre>';
360                        environment.replaceChild(container.firstChild, node.parentNode);
361                }else{
362                        node.className = className;
363                        node.innerHTML = text;
364                }
365        }
366        function highlightStringLanguage(language, str){
367                var highlight = new Highlighter(language, str);
368                return {result:highlight.result, langName:language, partialResult:highlight.partialResult};
369        }
370
371        function highlightLanguage(block, language){
372                var result = highlightStringLanguage(language, blockText(block));
373                replaceText(block, block.className, result.result);
374        }
375
376        function highlightStringAuto(str){
377                var result = "", langName = "", bestRelevance = 2,
378                        textBlock = str;
379                for(var key in dh.languages){
380                        if(!dh.languages[key].defaultMode){ continue; } // skip internal members
381                        var highlight = new Highlighter(key, textBlock),
382                                relevance = highlight.keywordCount + highlight.relevance, relevanceMax = 0;
383                        if(!result || relevance > relevanceMax){
384                                relevanceMax = relevance;
385                                result = highlight.result;
386                                langName = highlight.langName;
387                        }
388                }
389                return {result:result, langName:langName};
390        }
391       
392        function highlightAuto(block){
393                var result = highlightStringAuto(blockText(block));
394                if(result.result){
395                        replaceText(block, result.langName, result.result);
396                }
397        }
398       
399        // the public API
400
401        dojox.highlight.processString = function(/* String */ str, /* String? */lang){
402                // summary:
403                //              highlight a string of text
404                // returns: Object
405                //              Object containing:
406                //
407                //              - result - string of html with spans to apply formatting
408                //              - partialResult - if the formatting failed: string of html
409                //                up to the point of the failure, otherwise: undefined
410                //              - langName - the language used to do the formatting
411                return lang ? highlightStringLanguage(lang, str) : highlightStringAuto(str);
412        };
413
414        dojox.highlight.init = function(/* String|DomNode */ node){
415                // summary:
416                //              Highlight a passed node
417                // description:
418                //              Syntax highlight a passed DomNode or String ID of a DomNode
419                // example:
420                //      |       dojox.highlight.init("someId");
421                //
422                node = dom.byId(node);
423                if(domClass.contains(node, "no-highlight")){ return; }
424                if(!verifyText(node)){ return; }
425       
426                var classes = node.className.split(/\s+/),
427                        flag = array.some(classes, function(className){
428                                if(className.charAt(0) != "_" && dh.languages[className]){
429                                        highlightLanguage(node, className);
430                                        return true;    // stop iterations
431                                }
432                                return false;   // continue iterations
433                        });
434                if(!flag){
435                        highlightAuto(node);
436                }
437        };
438
439        dh.Code = function(props, node){
440                // summary:
441                //              A class object to allow for dojoType usage with the highlight engine. This is
442                //              NOT a Widget in the conventional sense, and does not have any member functions for
443                //              the instance. This is provided as a convenience. You likely should be calling
444                //              `dojox.highlight.init` directly.
445                // props: Object?
446                //              Unused. Pass 'null' or {}. Positional usage to allow `dojo.parser` to instantiate
447                //              this class as other Widgets would be.
448                // node: String|DomNode
449                //              A String ID or DomNode reference to use as the root node of this instance.
450                // example:
451                //      |       <pre><code dojoType="dojox.highlight.Code">for(var i in obj){ ... }</code></pre>
452                //
453                // example:
454                //      |       var inst = new dojox.highlight.Code({}, "someId");
455                //
456                dh.init(node);
457        };
458
459        return dh;
460
461});
Note: See TracBrowser for help on using the repository browser.