coffeescript.js 11 KB

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