coffeescript.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: https://codemirror.net/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')
  9. // CommonJS
  10. mod(require('../../lib/codemirror'))
  11. else if (typeof define == 'function' && define.amd)
  12. // AMD
  13. define(['../../lib/codemirror'], mod)
  14. // Plain browser env
  15. else mod(CodeMirror)
  16. })(function (CodeMirror) {
  17. 'use strict'
  18. CodeMirror.defineMode('coffeescript', function (conf, parserConf) {
  19. var ERRORCLASS = 'error'
  20. function wordRegexp(words) {
  21. return new RegExp('^((' + words.join(')|(') + '))\\b')
  22. }
  23. var operators = /^(?:->|=>|\+[+=]?|-[\-=]?|\*[\*=]?|\/[\/=]?|[=!]=|<[><]?=?|>>?=?|%=?|&=?|\|=?|\^=?|\~|!|\?|(or|and|\|\||&&|\?)=)/
  24. var delimiters = /^(?:[()\[\]{},:`=;]|\.\.?\.?)/
  25. var identifiers = /^[_A-Za-z$][_A-Za-z$0-9]*/
  26. var atProp = /^@[_A-Za-z$][_A-Za-z$0-9]*/
  27. var wordOperators = wordRegexp(['and', 'or', 'not', 'is', 'isnt', 'in', 'instanceof', 'typeof'])
  28. var indentKeywords = ['for', 'while', 'loop', 'if', 'unless', 'else', 'switch', 'try', 'catch', 'finally', 'class']
  29. var commonKeywords = ['break', 'by', 'continue', 'debugger', 'delete', 'do', 'in', 'of', 'new', 'return', 'then', 'this', '@', 'throw', 'when', 'until', 'extends']
  30. var keywords = wordRegexp(indentKeywords.concat(commonKeywords))
  31. indentKeywords = wordRegexp(indentKeywords)
  32. var stringPrefixes = /^('{3}|\"{3}|['\"])/
  33. var regexPrefixes = /^(\/{3}|\/)/
  34. var commonConstants = ['Infinity', 'NaN', 'undefined', 'null', 'true', 'false', 'on', 'off', 'yes', 'no']
  35. var constants = wordRegexp(commonConstants)
  36. // Tokenizers
  37. function tokenBase(stream, state) {
  38. // Handle scope changes
  39. if (stream.sol()) {
  40. if (state.scope.align === null) state.scope.align = false
  41. var scopeOffset = state.scope.offset
  42. if (stream.eatSpace()) {
  43. var lineOffset = stream.indentation()
  44. if (lineOffset > scopeOffset && state.scope.type == 'coffee') {
  45. return 'indent'
  46. } else if (lineOffset < scopeOffset) {
  47. return 'dedent'
  48. }
  49. return null
  50. } else {
  51. if (scopeOffset > 0) {
  52. dedent(stream, state)
  53. }
  54. }
  55. }
  56. if (stream.eatSpace()) {
  57. return null
  58. }
  59. var ch = stream.peek()
  60. // Handle docco title comment (single line)
  61. if (stream.match('####')) {
  62. stream.skipToEnd()
  63. return 'comment'
  64. }
  65. // Handle multi line comments
  66. if (stream.match('###')) {
  67. state.tokenize = longComment
  68. return state.tokenize(stream, state)
  69. }
  70. // Single line comment
  71. if (ch === '#') {
  72. stream.skipToEnd()
  73. return 'comment'
  74. }
  75. // Handle number literals
  76. if (stream.match(/^-?[0-9\.]/, false)) {
  77. var floatLiteral = false
  78. // Floats
  79. if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)) {
  80. floatLiteral = true
  81. }
  82. if (stream.match(/^-?\d+\.\d*/)) {
  83. floatLiteral = true
  84. }
  85. if (stream.match(/^-?\.\d+/)) {
  86. floatLiteral = true
  87. }
  88. if (floatLiteral) {
  89. // prevent from getting extra . on 1..
  90. if (stream.peek() == '.') {
  91. stream.backUp(1)
  92. }
  93. return 'number'
  94. }
  95. // Integers
  96. var intLiteral = false
  97. // Hex
  98. if (stream.match(/^-?0x[0-9a-f]+/i)) {
  99. intLiteral = true
  100. }
  101. // Decimal
  102. if (stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)) {
  103. intLiteral = true
  104. }
  105. // Zero by itself with no other piece of number.
  106. if (stream.match(/^-?0(?![\dx])/i)) {
  107. intLiteral = true
  108. }
  109. if (intLiteral) {
  110. return 'number'
  111. }
  112. }
  113. // Handle strings
  114. if (stream.match(stringPrefixes)) {
  115. state.tokenize = tokenFactory(stream.current(), false, 'string')
  116. return state.tokenize(stream, state)
  117. }
  118. // Handle regex literals
  119. if (stream.match(regexPrefixes)) {
  120. if (stream.current() != '/' || stream.match(/^.*\//, false)) {
  121. // 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(atProp) || (state.prop && stream.match(identifiers))) {
  139. return 'property'
  140. }
  141. if (stream.match(keywords)) {
  142. return 'keyword'
  143. }
  144. if (stream.match(identifiers)) {
  145. return 'variable'
  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 (parserConf.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,
  191. align = false,
  192. 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()) || style === 'indent') {
  244. indent(stream, state)
  245. }
  246. var delimiter_index = '[({'.indexOf(current)
  247. if (delimiter_index !== -1) {
  248. indent(stream, state, '])}'.slice(delimiter_index, delimiter_index + 1))
  249. }
  250. if (indentKeywords.exec(current)) {
  251. indent(stream, state)
  252. }
  253. if (current == 'then') {
  254. dedent(stream, state)
  255. }
  256. if (style === 'dedent') {
  257. if (dedent(stream, state)) {
  258. return ERRORCLASS
  259. }
  260. }
  261. delimiter_index = '])}'.indexOf(current)
  262. if (delimiter_index !== -1) {
  263. while (state.scope.type == 'coffee' && state.scope.prev) state.scope = state.scope.prev
  264. if (state.scope.type == current) state.scope = state.scope.prev
  265. }
  266. if (state.dedent && stream.eol()) {
  267. if (state.scope.type == 'coffee' && state.scope.prev) state.scope = state.scope.prev
  268. state.dedent = false
  269. }
  270. return style
  271. }
  272. var external = {
  273. startState: function (basecolumn) {
  274. return {
  275. tokenize: tokenBase,
  276. scope: { offset: basecolumn || 0, type: 'coffee', prev: null, align: false },
  277. prop: false,
  278. dedent: 0,
  279. }
  280. },
  281. token: function (stream, state) {
  282. var fillAlign = state.scope.align === null && state.scope
  283. if (fillAlign && stream.sol()) fillAlign.align = false
  284. var style = tokenLexer(stream, state)
  285. if (style && style != 'comment') {
  286. if (fillAlign) fillAlign.align = true
  287. state.prop = style == 'punctuation' && stream.current() == '.'
  288. }
  289. return style
  290. },
  291. indent: function (state, text) {
  292. if (state.tokenize != tokenBase) return 0
  293. var scope = state.scope
  294. var closer = text && '])}'.indexOf(text.charAt(0)) > -1
  295. if (closer) while (scope.type == 'coffee' && scope.prev) scope = scope.prev
  296. var closes = closer && scope.type === text.charAt(0)
  297. if (scope.align) return scope.alignOffset - (closes ? 1 : 0)
  298. else return (closes ? scope.prev : scope).offset
  299. },
  300. lineComment: '#',
  301. fold: 'indent',
  302. }
  303. return external
  304. })
  305. // IANA registered media type
  306. // https://www.iana.org/assignments/media-types/
  307. CodeMirror.defineMIME('application/vnd.coffeescript', 'coffeescript')
  308. CodeMirror.defineMIME('text/x-coffeescript', 'coffeescript')
  309. CodeMirror.defineMIME('text/coffeescript', 'coffeescript')
  310. })