ruby.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/LICENSE
  3. ;(function (mod) {
  4. if (typeof exports == 'object' && typeof module == 'object')
  5. // CommonJS
  6. mod(require('../../lib/codemirror'))
  7. else if (typeof define == 'function' && define.amd)
  8. // AMD
  9. define(['../../lib/codemirror'], mod)
  10. // Plain browser env
  11. else mod(CodeMirror)
  12. })(function (CodeMirror) {
  13. 'use strict'
  14. function wordObj(words) {
  15. var o = {}
  16. for (var i = 0, e = words.length; i < e; ++i) o[words[i]] = true
  17. return o
  18. }
  19. var keywordList = [
  20. 'alias',
  21. 'and',
  22. 'BEGIN',
  23. 'begin',
  24. 'break',
  25. 'case',
  26. 'class',
  27. 'def',
  28. 'defined?',
  29. 'do',
  30. 'else',
  31. 'elsif',
  32. 'END',
  33. 'end',
  34. 'ensure',
  35. 'false',
  36. 'for',
  37. 'if',
  38. 'in',
  39. 'module',
  40. 'next',
  41. 'not',
  42. 'or',
  43. 'redo',
  44. 'rescue',
  45. 'retry',
  46. 'return',
  47. 'self',
  48. 'super',
  49. 'then',
  50. 'true',
  51. 'undef',
  52. 'unless',
  53. 'until',
  54. 'when',
  55. 'while',
  56. 'yield',
  57. 'nil',
  58. 'raise',
  59. 'throw',
  60. 'catch',
  61. 'fail',
  62. 'loop',
  63. 'callcc',
  64. 'caller',
  65. 'lambda',
  66. 'proc',
  67. 'public',
  68. 'protected',
  69. 'private',
  70. 'require',
  71. 'load',
  72. 'require_relative',
  73. 'extend',
  74. 'autoload',
  75. '__END__',
  76. '__FILE__',
  77. '__LINE__',
  78. '__dir__',
  79. ],
  80. keywords = wordObj(keywordList)
  81. var indentWords = wordObj(['def', 'class', 'case', 'for', 'while', 'until', 'module', 'then', 'catch', 'loop', 'proc', 'begin'])
  82. var dedentWords = wordObj(['end', 'until'])
  83. var opening = { '[': ']', '{': '}', '(': ')' }
  84. var closing = { ']': '[', '}': '{', ')': '(' }
  85. CodeMirror.defineMode('ruby', function (config) {
  86. var curPunc
  87. function chain(newtok, stream, state) {
  88. state.tokenize.push(newtok)
  89. return newtok(stream, state)
  90. }
  91. function tokenBase(stream, state) {
  92. if (stream.sol() && stream.match('=begin') && stream.eol()) {
  93. state.tokenize.push(readBlockComment)
  94. return 'comment'
  95. }
  96. if (stream.eatSpace()) return null
  97. var ch = stream.next(),
  98. m
  99. if (ch == '`' || ch == "'" || ch == '"') {
  100. return chain(readQuoted(ch, 'string', ch == '"' || ch == '`'), stream, state)
  101. } else if (ch == '/') {
  102. if (regexpAhead(stream)) return chain(readQuoted(ch, 'string-2', true), stream, state)
  103. else return 'operator'
  104. } else if (ch == '%') {
  105. var style = 'string',
  106. embed = true
  107. if (stream.eat('s')) style = 'atom'
  108. else if (stream.eat(/[WQ]/)) style = 'string'
  109. else if (stream.eat(/[r]/)) style = 'string-2'
  110. else if (stream.eat(/[wxq]/)) {
  111. style = 'string'
  112. embed = false
  113. }
  114. var delim = stream.eat(/[^\w\s=]/)
  115. if (!delim) return 'operator'
  116. if (opening.propertyIsEnumerable(delim)) delim = opening[delim]
  117. return chain(readQuoted(delim, style, embed, true), stream, state)
  118. } else if (ch == '#') {
  119. stream.skipToEnd()
  120. return 'comment'
  121. } else if (ch == '<' && (m = stream.match(/^<([-~])[\`\"\']?([a-zA-Z_?]\w*)[\`\"\']?(?:;|$)/))) {
  122. return chain(readHereDoc(m[2], m[1]), stream, state)
  123. } else if (ch == '0') {
  124. if (stream.eat('x')) stream.eatWhile(/[\da-fA-F]/)
  125. else if (stream.eat('b')) stream.eatWhile(/[01]/)
  126. else stream.eatWhile(/[0-7]/)
  127. return 'number'
  128. } else if (/\d/.test(ch)) {
  129. stream.match(/^[\d_]*(?:\.[\d_]+)?(?:[eE][+\-]?[\d_]+)?/)
  130. return 'number'
  131. } else if (ch == '?') {
  132. while (stream.match(/^\\[CM]-/)) {}
  133. if (stream.eat('\\')) stream.eatWhile(/\w/)
  134. else stream.next()
  135. return 'string'
  136. } else if (ch == ':') {
  137. if (stream.eat("'")) return chain(readQuoted("'", 'atom', false), stream, state)
  138. if (stream.eat('"')) return chain(readQuoted('"', 'atom', true), stream, state)
  139. // :> :>> :< :<< are valid symbols
  140. if (stream.eat(/[\<\>]/)) {
  141. stream.eat(/[\<\>]/)
  142. return 'atom'
  143. }
  144. // :+ :- :/ :* :| :& :! are valid symbols
  145. if (stream.eat(/[\+\-\*\/\&\|\:\!]/)) {
  146. return 'atom'
  147. }
  148. // Symbols can't start by a digit
  149. if (stream.eat(/[a-zA-Z$@_\xa1-\uffff]/)) {
  150. stream.eatWhile(/[\w$\xa1-\uffff]/)
  151. // Only one ? ! = is allowed and only as the last character
  152. stream.eat(/[\?\!\=]/)
  153. return 'atom'
  154. }
  155. return 'operator'
  156. } else if (ch == '@' && stream.match(/^@?[a-zA-Z_\xa1-\uffff]/)) {
  157. stream.eat('@')
  158. stream.eatWhile(/[\w\xa1-\uffff]/)
  159. return 'variable-2'
  160. } else if (ch == '$') {
  161. if (stream.eat(/[a-zA-Z_]/)) {
  162. stream.eatWhile(/[\w]/)
  163. } else if (stream.eat(/\d/)) {
  164. stream.eat(/\d/)
  165. } else {
  166. stream.next() // Must be a special global like $: or $!
  167. }
  168. return 'variable-3'
  169. } else if (/[a-zA-Z_\xa1-\uffff]/.test(ch)) {
  170. stream.eatWhile(/[\w\xa1-\uffff]/)
  171. stream.eat(/[\?\!]/)
  172. if (stream.eat(':')) return 'atom'
  173. return 'ident'
  174. } else if (ch == '|' && (state.varList || state.lastTok == '{' || state.lastTok == 'do')) {
  175. curPunc = '|'
  176. return null
  177. } else if (/[\(\)\[\]{}\\;]/.test(ch)) {
  178. curPunc = ch
  179. return null
  180. } else if (ch == '-' && stream.eat('>')) {
  181. return 'arrow'
  182. } else if (/[=+\-\/*:\.^%<>~|]/.test(ch)) {
  183. var more = stream.eatWhile(/[=+\-\/*:\.^%<>~|]/)
  184. if (ch == '.' && !more) curPunc = '.'
  185. return 'operator'
  186. } else {
  187. return null
  188. }
  189. }
  190. function regexpAhead(stream) {
  191. var start = stream.pos,
  192. depth = 0,
  193. next,
  194. found = false,
  195. escaped = false
  196. while ((next = stream.next()) != null) {
  197. if (!escaped) {
  198. if ('[{('.indexOf(next) > -1) {
  199. depth++
  200. } else if (']})'.indexOf(next) > -1) {
  201. depth--
  202. if (depth < 0) break
  203. } else if (next == '/' && depth == 0) {
  204. found = true
  205. break
  206. }
  207. escaped = next == '\\'
  208. } else {
  209. escaped = false
  210. }
  211. }
  212. stream.backUp(stream.pos - start)
  213. return found
  214. }
  215. function tokenBaseUntilBrace(depth) {
  216. if (!depth) depth = 1
  217. return function (stream, state) {
  218. if (stream.peek() == '}') {
  219. if (depth == 1) {
  220. state.tokenize.pop()
  221. return state.tokenize[state.tokenize.length - 1](stream, state)
  222. } else {
  223. state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace(depth - 1)
  224. }
  225. } else if (stream.peek() == '{') {
  226. state.tokenize[state.tokenize.length - 1] = tokenBaseUntilBrace(depth + 1)
  227. }
  228. return tokenBase(stream, state)
  229. }
  230. }
  231. function tokenBaseOnce() {
  232. var alreadyCalled = false
  233. return function (stream, state) {
  234. if (alreadyCalled) {
  235. state.tokenize.pop()
  236. return state.tokenize[state.tokenize.length - 1](stream, state)
  237. }
  238. alreadyCalled = true
  239. return tokenBase(stream, state)
  240. }
  241. }
  242. function readQuoted(quote, style, embed, unescaped) {
  243. return function (stream, state) {
  244. var escaped = false,
  245. ch
  246. if (state.context.type === 'read-quoted-paused') {
  247. state.context = state.context.prev
  248. stream.eat('}')
  249. }
  250. while ((ch = stream.next()) != null) {
  251. if (ch == quote && (unescaped || !escaped)) {
  252. state.tokenize.pop()
  253. break
  254. }
  255. if (embed && ch == '#' && !escaped) {
  256. if (stream.eat('{')) {
  257. if (quote == '}') {
  258. state.context = { prev: state.context, type: 'read-quoted-paused' }
  259. }
  260. state.tokenize.push(tokenBaseUntilBrace())
  261. break
  262. } else if (/[@\$]/.test(stream.peek())) {
  263. state.tokenize.push(tokenBaseOnce())
  264. break
  265. }
  266. }
  267. escaped = !escaped && ch == '\\'
  268. }
  269. return style
  270. }
  271. }
  272. function readHereDoc(phrase, mayIndent) {
  273. return function (stream, state) {
  274. if (mayIndent) stream.eatSpace()
  275. if (stream.match(phrase)) state.tokenize.pop()
  276. else stream.skipToEnd()
  277. return 'string'
  278. }
  279. }
  280. function readBlockComment(stream, state) {
  281. if (stream.sol() && stream.match('=end') && stream.eol()) state.tokenize.pop()
  282. stream.skipToEnd()
  283. return 'comment'
  284. }
  285. return {
  286. startState: function () {
  287. return { tokenize: [tokenBase], indented: 0, context: { type: 'top', indented: -config.indentUnit }, continuedLine: false, lastTok: null, varList: false }
  288. },
  289. token: function (stream, state) {
  290. curPunc = null
  291. if (stream.sol()) state.indented = stream.indentation()
  292. var style = state.tokenize[state.tokenize.length - 1](stream, state),
  293. kwtype
  294. var thisTok = curPunc
  295. if (style == 'ident') {
  296. var word = stream.current()
  297. style =
  298. state.lastTok == '.'
  299. ? 'property'
  300. : keywords.propertyIsEnumerable(stream.current())
  301. ? 'keyword'
  302. : /^[A-Z]/.test(word)
  303. ? 'tag'
  304. : state.lastTok == 'def' || state.lastTok == 'class' || state.varList
  305. ? 'def'
  306. : 'variable'
  307. if (style == 'keyword') {
  308. thisTok = word
  309. if (indentWords.propertyIsEnumerable(word)) kwtype = 'indent'
  310. else if (dedentWords.propertyIsEnumerable(word)) kwtype = 'dedent'
  311. else if ((word == 'if' || word == 'unless') && stream.column() == stream.indentation()) kwtype = 'indent'
  312. else if (word == 'do' && state.context.indented < state.indented) kwtype = 'indent'
  313. }
  314. }
  315. if (curPunc || (style && style != 'comment')) state.lastTok = thisTok
  316. if (curPunc == '|') state.varList = !state.varList
  317. if (kwtype == 'indent' || /[\(\[\{]/.test(curPunc)) state.context = { prev: state.context, type: curPunc || style, indented: state.indented }
  318. else if ((kwtype == 'dedent' || /[\)\]\}]/.test(curPunc)) && state.context.prev) state.context = state.context.prev
  319. if (stream.eol()) state.continuedLine = curPunc == '\\' || style == 'operator'
  320. return style
  321. },
  322. indent: function (state, textAfter) {
  323. if (state.tokenize[state.tokenize.length - 1] != tokenBase) return CodeMirror.Pass
  324. var firstChar = textAfter && textAfter.charAt(0)
  325. var ct = state.context
  326. var closed = ct.type == closing[firstChar] || (ct.type == 'keyword' && /^(?:end|until|else|elsif|when|rescue)\b/.test(textAfter))
  327. return ct.indented + (closed ? 0 : config.indentUnit) + (state.continuedLine ? config.indentUnit : 0)
  328. },
  329. electricInput: /^\s*(?:end|rescue|elsif|else|\})$/,
  330. lineComment: '#',
  331. fold: 'indent',
  332. }
  333. })
  334. CodeMirror.defineMIME('text/x-ruby', 'ruby')
  335. CodeMirror.registerHelper('hintWords', 'ruby', keywordList)
  336. })