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.

433 lines
13 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. (function(mod) {
  4. if (typeof exports == "object" && typeof module == "object") // CommonJS
  5. mod(require("../../lib/codemirror"));
  6. else if (typeof define == "function" && define.amd) // AMD
  7. define(["../../lib/codemirror"], mod);
  8. else // Plain browser env
  9. mod(CodeMirror);
  10. })(function(CodeMirror) {
  11. "use strict";
  12. CodeMirror.defineMode("crystal", function(config) {
  13. function wordRegExp(words, end) {
  14. return new RegExp((end ? "" : "^") + "(?:" + words.join("|") + ")" + (end ? "$" : "\\b"));
  15. }
  16. function chain(tokenize, stream, state) {
  17. state.tokenize.push(tokenize);
  18. return tokenize(stream, state);
  19. }
  20. var operators = /^(?:[-+/%|&^]|\*\*?|[<>]{2})/;
  21. var conditionalOperators = /^(?:[=!]~|===|<=>|[<>=!]=?|[|&]{2}|~)/;
  22. var indexingOperators = /^(?:\[\][?=]?)/;
  23. var anotherOperators = /^(?:\.(?:\.{2})?|->|[?:])/;
  24. var idents = /^[a-z_\u009F-\uFFFF][a-zA-Z0-9_\u009F-\uFFFF]*/;
  25. var types = /^[A-Z_\u009F-\uFFFF][a-zA-Z0-9_\u009F-\uFFFF]*/;
  26. var keywords = wordRegExp([
  27. "abstract", "alias", "as", "asm", "begin", "break", "case", "class", "def", "do",
  28. "else", "elsif", "end", "ensure", "enum", "extend", "for", "fun", "if",
  29. "include", "instance_sizeof", "lib", "macro", "module", "next", "of", "out", "pointerof",
  30. "private", "protected", "rescue", "return", "require", "select", "sizeof", "struct",
  31. "super", "then", "type", "typeof", "uninitialized", "union", "unless", "until", "when", "while", "with",
  32. "yield", "__DIR__", "__END_LINE__", "__FILE__", "__LINE__"
  33. ]);
  34. var atomWords = wordRegExp(["true", "false", "nil", "self"]);
  35. var indentKeywordsArray = [
  36. "def", "fun", "macro",
  37. "class", "module", "struct", "lib", "enum", "union",
  38. "do", "for"
  39. ];
  40. var indentKeywords = wordRegExp(indentKeywordsArray);
  41. var indentExpressionKeywordsArray = ["if", "unless", "case", "while", "until", "begin", "then"];
  42. var indentExpressionKeywords = wordRegExp(indentExpressionKeywordsArray);
  43. var dedentKeywordsArray = ["end", "else", "elsif", "rescue", "ensure"];
  44. var dedentKeywords = wordRegExp(dedentKeywordsArray);
  45. var dedentPunctualsArray = ["\\)", "\\}", "\\]"];
  46. var dedentPunctuals = new RegExp("^(?:" + dedentPunctualsArray.join("|") + ")$");
  47. var nextTokenizer = {
  48. "def": tokenFollowIdent, "fun": tokenFollowIdent, "macro": tokenMacroDef,
  49. "class": tokenFollowType, "module": tokenFollowType, "struct": tokenFollowType,
  50. "lib": tokenFollowType, "enum": tokenFollowType, "union": tokenFollowType
  51. };
  52. var matching = {"[": "]", "{": "}", "(": ")", "<": ">"};
  53. function tokenBase(stream, state) {
  54. if (stream.eatSpace()) {
  55. return null;
  56. }
  57. // Macros
  58. if (state.lastToken != "\\" && stream.match("{%", false)) {
  59. return chain(tokenMacro("%", "%"), stream, state);
  60. }
  61. if (state.lastToken != "\\" && stream.match("{{", false)) {
  62. return chain(tokenMacro("{", "}"), stream, state);
  63. }
  64. // Comments
  65. if (stream.peek() == "#") {
  66. stream.skipToEnd();
  67. return "comment";
  68. }
  69. // Variables and keywords
  70. var matched;
  71. if (stream.match(idents)) {
  72. stream.eat(/[?!]/);
  73. matched = stream.current();
  74. if (stream.eat(":")) {
  75. return "atom";
  76. } else if (state.lastToken == ".") {
  77. return "property";
  78. } else if (keywords.test(matched)) {
  79. if (indentKeywords.test(matched)) {
  80. if (!(matched == "fun" && state.blocks.indexOf("lib") >= 0) && !(matched == "def" && state.lastToken == "abstract")) {
  81. state.blocks.push(matched);
  82. state.currentIndent += 1;
  83. }
  84. } else if ((state.lastStyle == "operator" || !state.lastStyle) && indentExpressionKeywords.test(matched)) {
  85. state.blocks.push(matched);
  86. state.currentIndent += 1;
  87. } else if (matched == "end") {
  88. state.blocks.pop();
  89. state.currentIndent -= 1;
  90. }
  91. if (nextTokenizer.hasOwnProperty(matched)) {
  92. state.tokenize.push(nextTokenizer[matched]);
  93. }
  94. return "keyword";
  95. } else if (atomWords.test(matched)) {
  96. return "atom";
  97. }
  98. return "variable";
  99. }
  100. // Class variables and instance variables
  101. // or attributes
  102. if (stream.eat("@")) {
  103. if (stream.peek() == "[") {
  104. return chain(tokenNest("[", "]", "meta"), stream, state);
  105. }
  106. stream.eat("@");
  107. stream.match(idents) || stream.match(types);
  108. return "variable-2";
  109. }
  110. // Constants and types
  111. if (stream.match(types)) {
  112. return "tag";
  113. }
  114. // Symbols or ':' operator
  115. if (stream.eat(":")) {
  116. if (stream.eat("\"")) {
  117. return chain(tokenQuote("\"", "atom", false), stream, state);
  118. } else if (stream.match(idents) || stream.match(types) ||
  119. stream.match(operators) || stream.match(conditionalOperators) || stream.match(indexingOperators)) {
  120. return "atom";
  121. }
  122. stream.eat(":");
  123. return "operator";
  124. }
  125. // Strings
  126. if (stream.eat("\"")) {
  127. return chain(tokenQuote("\"", "string", true), stream, state);
  128. }
  129. // Strings or regexps or macro variables or '%' operator
  130. if (stream.peek() == "%") {
  131. var style = "string";
  132. var embed = true;
  133. var delim;
  134. if (stream.match("%r")) {
  135. // Regexps
  136. style = "string-2";
  137. delim = stream.next();
  138. } else if (stream.match("%w")) {
  139. embed = false;
  140. delim = stream.next();
  141. } else if (stream.match("%q")) {
  142. embed = false;
  143. delim = stream.next();
  144. } else {
  145. if(delim = stream.match(/^%([^\w\s=])/)) {
  146. delim = delim[1];
  147. } else if (stream.match(/^%[a-zA-Z_\u009F-\uFFFF][\w\u009F-\uFFFF]*/)) {
  148. // Macro variables
  149. return "meta";
  150. } else if (stream.eat('%')) {
  151. // '%' operator
  152. return "operator";
  153. }
  154. }
  155. if (matching.hasOwnProperty(delim)) {
  156. delim = matching[delim];
  157. }
  158. return chain(tokenQuote(delim, style, embed), stream, state);
  159. }
  160. // Here Docs
  161. if (matched = stream.match(/^<<-('?)([A-Z]\w*)\1/)) {
  162. return chain(tokenHereDoc(matched[2], !matched[1]), stream, state)
  163. }
  164. // Characters
  165. if (stream.eat("'")) {
  166. stream.match(/^(?:[^']|\\(?:[befnrtv0'"]|[0-7]{3}|u(?:[0-9a-fA-F]{4}|\{[0-9a-fA-F]{1,6}\})))/);
  167. stream.eat("'");
  168. return "atom";
  169. }
  170. // Numbers
  171. if (stream.eat("0")) {
  172. if (stream.eat("x")) {
  173. stream.match(/^[0-9a-fA-F_]+/);
  174. } else if (stream.eat("o")) {
  175. stream.match(/^[0-7_]+/);
  176. } else if (stream.eat("b")) {
  177. stream.match(/^[01_]+/);
  178. }
  179. return "number";
  180. }
  181. if (stream.eat(/^\d/)) {
  182. stream.match(/^[\d_]*(?:\.[\d_]+)?(?:[eE][+-]?\d+)?/);
  183. return "number";
  184. }
  185. // Operators
  186. if (stream.match(operators)) {
  187. stream.eat("="); // Operators can follow assign symbol.
  188. return "operator";
  189. }
  190. if (stream.match(conditionalOperators) || stream.match(anotherOperators)) {
  191. return "operator";
  192. }
  193. // Parens and braces
  194. if (matched = stream.match(/[({[]/, false)) {
  195. matched = matched[0];
  196. return chain(tokenNest(matched, matching[matched], null), stream, state);
  197. }
  198. // Escapes
  199. if (stream.eat("\\")) {
  200. stream.next();
  201. return "meta";
  202. }
  203. stream.next();
  204. return null;
  205. }
  206. function tokenNest(begin, end, style, started) {
  207. return function (stream, state) {
  208. if (!started && stream.match(begin)) {
  209. state.tokenize[state.tokenize.length - 1] = tokenNest(begin, end, style, true);
  210. state.currentIndent += 1;
  211. return style;
  212. }
  213. var nextStyle = tokenBase(stream, state);
  214. if (stream.current() === end) {
  215. state.tokenize.pop();
  216. state.currentIndent -= 1;
  217. nextStyle = style;
  218. }
  219. return nextStyle;
  220. };
  221. }
  222. function tokenMacro(begin, end, started) {
  223. return function (stream, state) {
  224. if (!started && stream.match("{" + begin)) {
  225. state.currentIndent += 1;
  226. state.tokenize[state.tokenize.length - 1] = tokenMacro(begin, end, true);
  227. return "meta";
  228. }
  229. if (stream.match(end + "}")) {
  230. state.currentIndent -= 1;
  231. state.tokenize.pop();
  232. return "meta";
  233. }
  234. return tokenBase(stream, state);
  235. };
  236. }
  237. function tokenMacroDef(stream, state) {
  238. if (stream.eatSpace()) {
  239. return null;
  240. }
  241. var matched;
  242. if (matched = stream.match(idents)) {
  243. if (matched == "def") {
  244. return "keyword";
  245. }
  246. stream.eat(/[?!]/);
  247. }
  248. state.tokenize.pop();
  249. return "def";
  250. }
  251. function tokenFollowIdent(stream, state) {
  252. if (stream.eatSpace()) {
  253. return null;
  254. }
  255. if (stream.match(idents)) {
  256. stream.eat(/[!?]/);
  257. } else {
  258. stream.match(operators) || stream.match(conditionalOperators) || stream.match(indexingOperators);
  259. }
  260. state.tokenize.pop();
  261. return "def";
  262. }
  263. function tokenFollowType(stream, state) {
  264. if (stream.eatSpace()) {
  265. return null;
  266. }
  267. stream.match(types);
  268. state.tokenize.pop();
  269. return "def";
  270. }
  271. function tokenQuote(end, style, embed) {
  272. return function (stream, state) {
  273. var escaped = false;
  274. while (stream.peek()) {
  275. if (!escaped) {
  276. if (stream.match("{%", false)) {
  277. state.tokenize.push(tokenMacro("%", "%"));
  278. return style;
  279. }
  280. if (stream.match("{{", false)) {
  281. state.tokenize.push(tokenMacro("{", "}"));
  282. return style;
  283. }
  284. if (embed && stream.match("#{", false)) {
  285. state.tokenize.push(tokenNest("#{", "}", "meta"));
  286. return style;
  287. }
  288. var ch = stream.next();
  289. if (ch == end) {
  290. state.tokenize.pop();
  291. return style;
  292. }
  293. escaped = embed && ch == "\\";
  294. } else {
  295. stream.next();
  296. escaped = false;
  297. }
  298. }
  299. return style;
  300. };
  301. }
  302. function tokenHereDoc(phrase, embed) {
  303. return function (stream, state) {
  304. if (stream.sol()) {
  305. stream.eatSpace()
  306. if (stream.match(phrase)) {
  307. state.tokenize.pop();
  308. return "string";
  309. }
  310. }
  311. var escaped = false;
  312. while (stream.peek()) {
  313. if (!escaped) {
  314. if (stream.match("{%", false)) {
  315. state.tokenize.push(tokenMacro("%", "%"));
  316. return "string";
  317. }
  318. if (stream.match("{{", false)) {
  319. state.tokenize.push(tokenMacro("{", "}"));
  320. return "string";
  321. }
  322. if (embed && stream.match("#{", false)) {
  323. state.tokenize.push(tokenNest("#{", "}", "meta"));
  324. return "string";
  325. }
  326. escaped = embed && stream.next() == "\\";
  327. } else {
  328. stream.next();
  329. escaped = false;
  330. }
  331. }
  332. return "string";
  333. }
  334. }
  335. return {
  336. startState: function () {
  337. return {
  338. tokenize: [tokenBase],
  339. currentIndent: 0,
  340. lastToken: null,
  341. lastStyle: null,
  342. blocks: []
  343. };
  344. },
  345. token: function (stream, state) {
  346. var style = state.tokenize[state.tokenize.length - 1](stream, state);
  347. var token = stream.current();
  348. if (style && style != "comment") {
  349. state.lastToken = token;
  350. state.lastStyle = style;
  351. }
  352. return style;
  353. },
  354. indent: function (state, textAfter) {
  355. textAfter = textAfter.replace(/^\s*(?:\{%)?\s*|\s*(?:%\})?\s*$/g, "");
  356. if (dedentKeywords.test(textAfter) || dedentPunctuals.test(textAfter)) {
  357. return config.indentUnit * (state.currentIndent - 1);
  358. }
  359. return config.indentUnit * state.currentIndent;
  360. },
  361. fold: "indent",
  362. electricInput: wordRegExp(dedentPunctualsArray.concat(dedentKeywordsArray), true),
  363. lineComment: '#'
  364. };
  365. });
  366. CodeMirror.defineMIME("text/x-crystal", "crystal");
  367. });