/src/helpers.coffee
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