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