/src/helpers.coffee

http://github.com/jashkenas/coffee-script · CoffeeScript · 320 lines · 212 code · 43 blank · 65 comment · 44 complexity · 4ba84d2876b50969527cf95f9cadfc2a MD5 · raw file

  1. # This file contains the common helper functions that we'd like to share among
  2. # the **Lexer**, **Rewriter**, and the **Nodes**. Merge objects, flatten
  3. # arrays, count characters, that sort of thing.
  4. # Peek at the beginning of a given string to see if it matches a sequence.
  5. exports.starts = (string, literal, start) ->
  6. literal is string.substr start, literal.length
  7. # Peek at the end of a given string to see if it matches a sequence.
  8. exports.ends = (string, literal, back) ->
  9. len = literal.length
  10. literal is string.substr string.length - len - (back or 0), len
  11. # Repeat a string `n` times.
  12. exports.repeat = repeat = (str, n) ->
  13. # Use clever algorithm to have O(log(n)) string concatenation operations.
  14. res = ''
  15. while n > 0
  16. res += str if n & 1
  17. n >>>= 1
  18. str += str
  19. res
  20. # Trim out all falsy values from an array.
  21. exports.compact = (array) ->
  22. item for item in array when item
  23. # Count the number of occurrences of a string in a string.
  24. exports.count = (string, substr) ->
  25. num = pos = 0
  26. return 1/0 unless substr.length
  27. num++ while pos = 1 + string.indexOf substr, pos
  28. num
  29. # Merge objects, returning a fresh copy with attributes from both sides.
  30. # Used every time `Base#compile` is called, to allow properties in the
  31. # options hash to propagate down the tree without polluting other branches.
  32. exports.merge = (options, overrides) ->
  33. extend (extend {}, options), overrides
  34. # Extend a source object with the properties of another object (shallow copy).
  35. extend = exports.extend = (object, properties) ->
  36. for key, val of properties
  37. object[key] = val
  38. object
  39. # Return a flattened version of an array.
  40. # Handy for getting a list of `children` from the nodes.
  41. exports.flatten = flatten = (array) ->
  42. flattened = []
  43. for element in array
  44. if '[object Array]' is Object::toString.call element
  45. flattened = flattened.concat flatten element
  46. else
  47. flattened.push element
  48. flattened
  49. # Delete a key from an object, returning the value. Useful when a node is
  50. # looking for a particular method in an options hash.
  51. exports.del = (obj, key) ->
  52. val = obj[key]
  53. delete obj[key]
  54. val
  55. # Typical Array::some
  56. exports.some = Array::some ? (fn) ->
  57. return true for e in this when fn e
  58. false
  59. # Helper function for extracting code from Literate CoffeeScript by stripping
  60. # out all non-code blocks, producing a string of CoffeeScript code that can
  61. # be compiled normally.
  62. exports.invertLiterate = (code) ->
  63. out = []
  64. blankLine = /^\s*$/
  65. indented = /^[\t ]/
  66. listItemStart = /// ^
  67. (?:\t?|\ {0,3}) # Up to one tab, or up to three spaces, or neither;
  68. (?:
  69. [\*\-\+] | # followed by `*`, `-` or `+`;
  70. [0-9]{1,9}\. # or by an integer up to 9 digits long, followed by a period;
  71. )
  72. [\ \t] # followed by a space or a tab.
  73. ///
  74. insideComment = no
  75. for line in code.split('\n')
  76. if blankLine.test(line)
  77. insideComment = no
  78. out.push line
  79. else if insideComment or listItemStart.test(line)
  80. insideComment = yes
  81. out.push "# #{line}"
  82. else if not insideComment and indented.test(line)
  83. out.push line
  84. else
  85. insideComment = yes
  86. out.push "# #{line}"
  87. out.join '\n'
  88. # Merge two jison-style location data objects together.
  89. # If `last` is not provided, this will simply return `first`.
  90. buildLocationData = (first, last) ->
  91. if not last
  92. first
  93. else
  94. first_line: first.first_line
  95. first_column: first.first_column
  96. last_line: last.last_line
  97. last_column: last.last_column
  98. last_line_exclusive: last.last_line_exclusive
  99. last_column_exclusive: last.last_column_exclusive
  100. range: [
  101. first.range[0]
  102. last.range[1]
  103. ]
  104. # Build a list of all comments attached to tokens.
  105. exports.extractAllCommentTokens = (tokens) ->
  106. allCommentsObj = {}
  107. for token in tokens when token.comments
  108. for comment in token.comments
  109. commentKey = comment.locationData.range[0]
  110. allCommentsObj[commentKey] = comment
  111. sortedKeys = Object.keys(allCommentsObj).sort (a, b) -> a - b
  112. for key in sortedKeys
  113. allCommentsObj[key]
  114. # Get a lookup hash for a token based on its location data.
  115. # Multiple tokens might have the same location hash, but using exclusive
  116. # location data distinguishes e.g. zero-length generated tokens from
  117. # actual source tokens.
  118. buildLocationHash = (loc) ->
  119. "#{loc.range[0]}-#{loc.range[1]}"
  120. # Build a dictionary of extra token properties organized by tokens locations
  121. # used as lookup hashes.
  122. exports.buildTokenDataDictionary = buildTokenDataDictionary = (tokens) ->
  123. tokenData = {}
  124. for token in tokens when token.comments
  125. tokenHash = buildLocationHash token[2]
  126. # Multiple tokens might have the same location hash, such as the generated
  127. # `JS` tokens added at the start or end of the token stream to hold
  128. # comments that start or end a file.
  129. tokenData[tokenHash] ?= {}
  130. if token.comments # `comments` is always an array.
  131. # For overlapping tokens, that is tokens with the same location data
  132. # and therefore matching `tokenHash`es, merge the comments from both/all
  133. # tokens together into one array, even if there are duplicate comments;
  134. # they will get sorted out later.
  135. (tokenData[tokenHash].comments ?= []).push token.comments...
  136. tokenData
  137. # This returns a function which takes an object as a parameter, and if that
  138. # object is an AST node, updates that object's locationData.
  139. # The object is returned either way.
  140. exports.addDataToNode = (parserState, firstLocationData, firstValue, lastLocationData, lastValue, forceUpdateLocation = yes) ->
  141. (obj) ->
  142. # Add location data.
  143. locationData = buildLocationData(firstValue?.locationData ? firstLocationData, lastValue?.locationData ? lastLocationData)
  144. if obj?.updateLocationDataIfMissing? and firstLocationData?
  145. obj.updateLocationDataIfMissing locationData, forceUpdateLocation
  146. else
  147. obj.locationData = locationData
  148. # Add comments, building the dictionary of token data if it hasnt been
  149. # built yet.
  150. parserState.tokenData ?= buildTokenDataDictionary parserState.parser.tokens
  151. if obj.locationData?
  152. objHash = buildLocationHash obj.locationData
  153. if parserState.tokenData[objHash]?.comments?
  154. attachCommentsToNode parserState.tokenData[objHash].comments, obj
  155. obj
  156. exports.attachCommentsToNode = attachCommentsToNode = (comments, node) ->
  157. return if not comments? or comments.length is 0
  158. node.comments ?= []
  159. node.comments.push comments...
  160. # Convert jison location data to a string.
  161. # `obj` can be a token, or a locationData.
  162. exports.locationDataToString = (obj) ->
  163. if ("2" of obj) and ("first_line" of obj[2]) then locationData = obj[2]
  164. else if "first_line" of obj then locationData = obj
  165. if locationData
  166. "#{locationData.first_line + 1}:#{locationData.first_column + 1}-" +
  167. "#{locationData.last_line + 1}:#{locationData.last_column + 1}"
  168. else
  169. "No location data"
  170. # A `.coffee.md` compatible version of `basename`, that returns the file sans-extension.
  171. exports.baseFileName = (file, stripExt = no, useWinPathSep = no) ->
  172. pathSep = if useWinPathSep then /\\|\// else /\//
  173. parts = file.split(pathSep)
  174. file = parts[parts.length - 1]
  175. return file unless stripExt and file.indexOf('.') >= 0
  176. parts = file.split('.')
  177. parts.pop()
  178. parts.pop() if parts[parts.length - 1] is 'coffee' and parts.length > 1
  179. parts.join('.')
  180. # Determine if a filename represents a CoffeeScript file.
  181. exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file
  182. # Determine if a filename represents a Literate CoffeeScript file.
  183. exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file
  184. # Throws a SyntaxError from a given location.
  185. # The error's `toString` will return an error message following the "standard"
  186. # format `<filename>:<line>:<col>: <message>` plus the line with the error and a
  187. # marker showing where the error is.
  188. exports.throwSyntaxError = (message, location) ->
  189. error = new SyntaxError message
  190. error.location = location
  191. error.toString = syntaxErrorToString
  192. # Instead of showing the compiler's stacktrace, show our custom error message
  193. # (this is useful when the error bubbles up in Node.js applications that
  194. # compile CoffeeScript for example).
  195. error.stack = error.toString()
  196. throw error
  197. # Update a compiler SyntaxError with source code information if it didn't have
  198. # it already.
  199. exports.updateSyntaxError = (error, code, filename) ->
  200. # Avoid screwing up the `stack` property of other errors (i.e. possible bugs).
  201. if error.toString is syntaxErrorToString
  202. error.code or= code
  203. error.filename or= filename
  204. error.stack = error.toString()
  205. error
  206. syntaxErrorToString = ->
  207. return Error::toString.call @ unless @code and @location
  208. {first_line, first_column, last_line, last_column} = @location
  209. last_line ?= first_line
  210. last_column ?= first_column
  211. filename = @filename or '[stdin]'
  212. codeLine = @code.split('\n')[first_line]
  213. start = first_column
  214. # Show only the first line on multi-line errors.
  215. end = if first_line is last_line then last_column + 1 else codeLine.length
  216. marker = codeLine[...start].replace(/[^\s]/g, ' ') + repeat('^', end - start)
  217. # Check to see if we're running on a color-enabled TTY.
  218. if process?
  219. colorsEnabled = process.stdout?.isTTY and not process.env?.NODE_DISABLE_COLORS
  220. if @colorful ? colorsEnabled
  221. colorize = (str) -> "\x1B[1;31m#{str}\x1B[0m"
  222. codeLine = codeLine[...start] + colorize(codeLine[start...end]) + codeLine[end..]
  223. marker = colorize marker
  224. """
  225. #{filename}:#{first_line + 1}:#{first_column + 1}: error: #{@message}
  226. #{codeLine}
  227. #{marker}
  228. """
  229. exports.nameWhitespaceCharacter = (string) ->
  230. switch string
  231. when ' ' then 'space'
  232. when '\n' then 'newline'
  233. when '\r' then 'carriage return'
  234. when '\t' then 'tab'
  235. else string
  236. exports.parseNumber = (string) ->
  237. return NaN unless string?
  238. base = switch string.charAt 1
  239. when 'b' then 2
  240. when 'o' then 8
  241. when 'x' then 16
  242. else null
  243. if base?
  244. parseInt string[2..].replace(/_/g, ''), base
  245. else
  246. parseFloat string.replace(/_/g, '')
  247. exports.isFunction = (obj) -> Object::toString.call(obj) is '[object Function]'
  248. exports.isNumber = isNumber = (obj) -> Object::toString.call(obj) is '[object Number]'
  249. exports.isString = isString = (obj) -> Object::toString.call(obj) is '[object String]'
  250. exports.isBoolean = isBoolean = (obj) -> obj is yes or obj is no or Object::toString.call(obj) is '[object Boolean]'
  251. exports.isPlainObject = (obj) -> typeof obj is 'object' and !!obj and not Array.isArray(obj) and not isNumber(obj) and not isString(obj) and not isBoolean(obj)
  252. unicodeCodePointToUnicodeEscapes = (codePoint) ->
  253. toUnicodeEscape = (val) ->
  254. str = val.toString 16
  255. "\\u#{repeat '0', 4 - str.length}#{str}"
  256. return toUnicodeEscape(codePoint) if codePoint < 0x10000
  257. # surrogate pair
  258. high = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800
  259. low = (codePoint - 0x10000) % 0x400 + 0xDC00
  260. "#{toUnicodeEscape(high)}#{toUnicodeEscape(low)}"
  261. # Replace `\u{...}` with `\uxxxx[\uxxxx]` in regexes without `u` flag
  262. exports.replaceUnicodeCodePointEscapes = (str, {flags, error, delimiter = ''} = {}) ->
  263. shouldReplace = flags? and 'u' not in flags
  264. str.replace UNICODE_CODE_POINT_ESCAPE, (match, escapedBackslash, codePointHex, offset) ->
  265. return escapedBackslash if escapedBackslash
  266. codePointDecimal = parseInt codePointHex, 16
  267. if codePointDecimal > 0x10ffff
  268. error "unicode code point escapes greater than \\u{10ffff} are not allowed",
  269. offset: offset + delimiter.length
  270. length: codePointHex.length + 4
  271. return match unless shouldReplace
  272. unicodeCodePointToUnicodeEscapes codePointDecimal
  273. UNICODE_CODE_POINT_ESCAPE = ///
  274. ( \\\\ ) # Make sure the escape isnt escaped.
  275. |
  276. \\u\{ ( [\da-fA-F]+ ) \}
  277. ///g