PageRenderTime 64ms CodeModel.GetById 17ms app.highlight 40ms RepoModel.GetById 1ms app.codeStats 1ms

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