coffeescript.js 9.7 KB

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