• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * Link to the project's GitHub page:
3 * https://github.com/pickhardt/coffeescript-codemirror-mode
4 */
5CodeMirror.defineMode('coffeescript', function(conf) {
6    var ERRORCLASS = 'error';
7
8    function wordRegexp(words) {
9        return new RegExp("^((" + words.join(")|(") + "))\\b");
10    }
11
12    var singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\?]");
13    var singleDelimiters = new RegExp('^[\\(\\)\\[\\]\\{\\},:`=;\\.]');
14    var doubleOperators = new RegExp("^((\->)|(\=>)|(\\+\\+)|(\\+\\=)|(\\-\\-)|(\\-\\=)|(\\*\\*)|(\\*\\=)|(\\/\\/)|(\\/\\=)|(==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//))");
15    var doubleDelimiters = new RegExp("^((\\.\\.)|(\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))");
16    var tripleDelimiters = new RegExp("^((\\.\\.\\.)|(//=)|(>>=)|(<<=)|(\\*\\*=))");
17    var identifiers = new RegExp("^[_A-Za-z$][_A-Za-z$0-9]*");
18    var properties = new RegExp("^(@|this\.)[_A-Za-z$][_A-Za-z$0-9]*");
19
20    var wordOperators = wordRegexp(['and', 'or', 'not',
21                                    'is', 'isnt', 'in',
22                                    'instanceof', 'typeof']);
23    var indentKeywords = ['for', 'while', 'loop', 'if', 'unless', 'else',
24                          'switch', 'try', 'catch', 'finally', 'class'];
25    var commonKeywords = ['break', 'by', 'continue', 'debugger', 'delete',
26                          'do', 'in', 'of', 'new', 'return', 'then',
27                          'this', 'throw', 'when', 'until'];
28
29    var keywords = wordRegexp(indentKeywords.concat(commonKeywords));
30
31    indentKeywords = wordRegexp(indentKeywords);
32
33
34    var stringPrefixes = new RegExp("^('{3}|\"{3}|['\"])");
35    var regexPrefixes = new RegExp("^(/{3}|/)");
36    var commonConstants = ['Infinity', 'NaN', 'undefined', 'null', 'true', 'false', 'on', 'off', 'yes', 'no'];
37    var constants = wordRegexp(commonConstants);
38
39    // Tokenizers
40    function tokenBase(stream, state) {
41        // Handle scope changes
42        if (stream.sol()) {
43            var scopeOffset = state.scopes[0].offset;
44            if (stream.eatSpace()) {
45                var lineOffset = stream.indentation();
46                if (lineOffset > scopeOffset) {
47                    return 'indent';
48                } else if (lineOffset < scopeOffset) {
49                    return 'dedent';
50                }
51                return null;
52            } else {
53                if (scopeOffset > 0) {
54                    dedent(stream, state);
55                }
56            }
57        }
58        if (stream.eatSpace()) {
59            return null;
60        }
61
62        var ch = stream.peek();
63
64        // Handle docco title comment (single line)
65        if (stream.match("####")) {
66            stream.skipToEnd();
67            return 'comment';
68        }
69
70        // Handle multi line comments
71        if (stream.match("###")) {
72            state.tokenize = longComment;
73            return state.tokenize(stream, state);
74        }
75
76        // Single line comment
77        if (ch === '#') {
78            stream.skipToEnd();
79            return 'comment';
80        }
81
82        // Handle number literals
83        if (stream.match(/^-?[0-9\.]/, false)) {
84            var floatLiteral = false;
85            // Floats
86            if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)) {
87              floatLiteral = true;
88            }
89            if (stream.match(/^-?\d+\.\d*/)) {
90              floatLiteral = true;
91            }
92            if (stream.match(/^-?\.\d+/)) {
93              floatLiteral = true;
94            }
95
96            if (floatLiteral) {
97                // prevent from getting extra . on 1..
98                if (stream.peek() == "."){
99                    stream.backUp(1);
100                }
101                return 'number';
102            }
103            // Integers
104            var intLiteral = false;
105            // Hex
106            if (stream.match(/^-?0x[0-9a-f]+/i)) {
107              intLiteral = true;
108            }
109            // Decimal
110            if (stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)) {
111                intLiteral = true;
112            }
113            // Zero by itself with no other piece of number.
114            if (stream.match(/^-?0(?![\dx])/i)) {
115              intLiteral = true;
116            }
117            if (intLiteral) {
118                return 'number';
119            }
120        }
121
122        // Handle strings
123        if (stream.match(stringPrefixes)) {
124            state.tokenize = tokenFactory(stream.current(), 'string');
125            return state.tokenize(stream, state);
126        }
127        // Handle regex literals
128        if (stream.match(regexPrefixes)) {
129            if (stream.current() != '/' || stream.match(/^.*\//, false)) { // prevent highlight of division
130                state.tokenize = tokenFactory(stream.current(), 'string-2');
131                return state.tokenize(stream, state);
132            } else {
133                stream.backUp(1);
134            }
135        }
136
137        // Handle operators and delimiters
138        if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) {
139            return 'punctuation';
140        }
141        if (stream.match(doubleOperators)
142            || stream.match(singleOperators)
143            || stream.match(wordOperators)) {
144            return 'operator';
145        }
146        if (stream.match(singleDelimiters)) {
147            return 'punctuation';
148        }
149
150        if (stream.match(constants)) {
151            return 'atom';
152        }
153
154        if (stream.match(keywords)) {
155            return 'keyword';
156        }
157
158        if (stream.match(identifiers)) {
159            return 'variable';
160        }
161
162        if (stream.match(properties)) {
163            return 'property';
164        }
165
166        // Handle non-detected items
167        stream.next();
168        return ERRORCLASS;
169    }
170
171    function tokenFactory(delimiter, outclass) {
172        var singleline = delimiter.length == 1;
173        return function(stream, state) {
174            while (!stream.eol()) {
175                stream.eatWhile(/[^'"\/\\]/);
176                if (stream.eat('\\')) {
177                    stream.next();
178                    if (singleline && stream.eol()) {
179                        return outclass;
180                    }
181                } else if (stream.match(delimiter)) {
182                    state.tokenize = tokenBase;
183                    return outclass;
184                } else {
185                    stream.eat(/['"\/]/);
186                }
187            }
188            if (singleline) {
189                if (conf.mode.singleLineStringErrors) {
190                    outclass = ERRORCLASS;
191                } else {
192                    state.tokenize = tokenBase;
193                }
194            }
195            return outclass;
196        };
197    }
198
199    function longComment(stream, state) {
200        while (!stream.eol()) {
201            stream.eatWhile(/[^#]/);
202            if (stream.match("###")) {
203                state.tokenize = tokenBase;
204                break;
205            }
206            stream.eatWhile("#");
207        }
208        return "comment";
209    }
210
211    function indent(stream, state, type) {
212        type = type || 'coffee';
213        var indentUnit = 0;
214        if (type === 'coffee') {
215            for (var i = 0; i < state.scopes.length; i++) {
216                if (state.scopes[i].type === 'coffee') {
217                    indentUnit = state.scopes[i].offset + conf.indentUnit;
218                    break;
219                }
220            }
221        } else {
222            indentUnit = stream.column() + stream.current().length;
223        }
224        state.scopes.unshift({
225            offset: indentUnit,
226            type: type
227        });
228    }
229
230    function dedent(stream, state) {
231        if (state.scopes.length == 1) return;
232        if (state.scopes[0].type === 'coffee') {
233            var _indent = stream.indentation();
234            var _indent_index = -1;
235            for (var i = 0; i < state.scopes.length; ++i) {
236                if (_indent === state.scopes[i].offset) {
237                    _indent_index = i;
238                    break;
239                }
240            }
241            if (_indent_index === -1) {
242                return true;
243            }
244            while (state.scopes[0].offset !== _indent) {
245                state.scopes.shift();
246            }
247            return false;
248        } else {
249            state.scopes.shift();
250            return false;
251        }
252    }
253
254    function tokenLexer(stream, state) {
255        var style = state.tokenize(stream, state);
256        var current = stream.current();
257
258        // Handle '.' connected identifiers
259        if (current === '.') {
260            style = state.tokenize(stream, state);
261            current = stream.current();
262            if (style === 'variable') {
263                return 'variable';
264            } else {
265                return ERRORCLASS;
266            }
267        }
268
269        // Handle scope changes.
270        if (current === 'return') {
271            state.dedent += 1;
272        }
273        if (((current === '->' || current === '=>') &&
274                  !state.lambda &&
275                  state.scopes[0].type == 'coffee' &&
276                  stream.peek() === '')
277               || style === 'indent') {
278            indent(stream, state);
279        }
280        var delimiter_index = '[({'.indexOf(current);
281        if (delimiter_index !== -1) {
282            indent(stream, state, '])}'.slice(delimiter_index, delimiter_index+1));
283        }
284        if (indentKeywords.exec(current)){
285            indent(stream, state);
286        }
287        if (current == 'then'){
288            dedent(stream, state);
289        }
290
291
292        if (style === 'dedent') {
293            if (dedent(stream, state)) {
294                return ERRORCLASS;
295            }
296        }
297        delimiter_index = '])}'.indexOf(current);
298        if (delimiter_index !== -1) {
299            if (dedent(stream, state)) {
300                return ERRORCLASS;
301            }
302        }
303        if (state.dedent > 0 && stream.eol() && state.scopes[0].type == 'coffee') {
304            if (state.scopes.length > 1) state.scopes.shift();
305            state.dedent -= 1;
306        }
307
308        return style;
309    }
310
311    var external = {
312        startState: function(basecolumn) {
313            return {
314              tokenize: tokenBase,
315              scopes: [{offset:basecolumn || 0, type:'coffee'}],
316              lastToken: null,
317              lambda: false,
318              dedent: 0
319          };
320        },
321
322        token: function(stream, state) {
323            var style = tokenLexer(stream, state);
324
325            state.lastToken = {style:style, content: stream.current()};
326
327            if (stream.eol() && stream.lambda) {
328                state.lambda = false;
329            }
330
331            return style;
332        },
333
334        indent: function(state) {
335            if (state.tokenize != tokenBase) {
336                return 0;
337            }
338
339            return state.scopes[0].offset;
340        },
341
342        lineComment: "#"
343    };
344    return external;
345});
346
347CodeMirror.defineMIME('text/x-coffeescript', 'coffeescript');
348