You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

359 lines
9.8 KiB

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