|
1 | 1 | (function(){ |
2 | | - var ACCESSORS, ASSIGNMENT, BEFORE_WHEN, CALLABLE, CODE, COFFEE_KEYWORDS, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, JS, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, KEYWORDS, LAST_DENT, LAST_DENTS, Lexer, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX, RESERVED, Rewriter, STRING, STRING_NEWLINES, WHITESPACE; |
| 2 | + var ACCESSORS, ASSIGNMENT, BEFORE_WHEN, CALLABLE, CODE, COFFEE_KEYWORDS, COMMENT, COMMENT_CLEANER, HEREDOC, HEREDOC_INDENT, IDENTIFIER, JS, JS_CLEANER, JS_FORBIDDEN, JS_KEYWORDS, KEYWORDS, LAST_DENT, LAST_DENTS, Lexer, MULTILINER, MULTI_DENT, NOT_REGEX, NO_NEWLINE, NUMBER, OPERATOR, REGEX, RESERVED, Rewriter, STRING, STRING_NEWLINES, WHITESPACE, include; |
3 | 3 | if ((typeof process !== "undefined" && process !== null)) { |
4 | 4 | Rewriter = require('./rewriter').Rewriter; |
5 | 5 | } else { |
|
60 | 60 | // Scan by attempting to match tokens one character at a time. Slow and steady. |
61 | 61 | Lexer.prototype.tokenize = function tokenize(code) { |
62 | 62 | this.code = code; |
63 | | - // Cleanup code by remove extra line breaks, TODO: chomp |
| 63 | + // The remainder of the source code. |
64 | 64 | this.i = 0; |
65 | | - // Current character position we're parsing |
| 65 | + // Current character position we're parsing. |
66 | 66 | this.line = 1; |
67 | 67 | // The current line. |
68 | 68 | this.indent = 0; |
69 | 69 | // The current indent level. |
70 | 70 | this.indents = []; |
71 | 71 | // The stack of all indent levels we are currently within. |
72 | 72 | this.tokens = []; |
73 | | - // Collection of all parsed tokens in the form [:TOKEN_TYPE, value] |
| 73 | + // Collection of all parsed tokens in the form ['TOKEN_TYPE', value] |
74 | 74 | while (this.i < this.code.length) { |
75 | 75 | this.chunk = this.code.slice(this.i); |
76 | 76 | this.extract_next_token(); |
|
117 | 117 | if (!((id = this.match(IDENTIFIER, 1)))) { |
118 | 118 | return false; |
119 | 119 | } |
120 | | - if (this.value() === '::') { |
121 | | - this.tag(1, 'PROTOTYPE_ACCESS'); |
122 | | - } |
123 | | - if (this.value() === '.' && !(this.value(2) === '.')) { |
124 | | - if (this.tag(2) === '?') { |
125 | | - this.tag(1, 'SOAK_ACCESS'); |
126 | | - this.tokens.splice(-2, 1); |
127 | | - } else { |
128 | | - this.tag(1, 'PROPERTY_ACCESS'); |
129 | | - } |
130 | | - } |
| 120 | + this.name_access_type(); |
131 | 121 | tag = 'IDENTIFIER'; |
132 | | - if (KEYWORDS.indexOf(id) >= 0 && !((ACCESSORS.indexOf(this.tag()) >= 0) && !this.prev().spaced)) { |
| 122 | + if (include(KEYWORDS, id) && !(include(ACCESSORS, this.tag(0)) && !this.prev().spaced)) { |
133 | 123 | tag = id.toUpperCase(); |
134 | 124 | } |
135 | | - if (RESERVED.indexOf(id) >= 0) { |
136 | | - throw new Error('SyntaxError: Reserved word "' + id + '" on line ' + this.line); |
| 125 | + if (include(RESERVED, id)) { |
| 126 | + this.identifier_error(id); |
137 | 127 | } |
138 | | - if (tag === 'WHEN' && BEFORE_WHEN.indexOf(this.tag()) >= 0) { |
| 128 | + if (tag === 'WHEN' && include(BEFORE_WHEN, this.tag())) { |
139 | 129 | tag = 'LEADING_WHEN'; |
140 | 130 | } |
141 | 131 | this.token(tag, id); |
|
166 | 156 | }; |
167 | 157 | // Matches heredocs, adjusting indentation to the correct level. |
168 | 158 | Lexer.prototype.heredoc_token = function heredoc_token() { |
169 | | - var doc, indent, match; |
| 159 | + var doc, match; |
170 | 160 | if (!((match = this.chunk.match(HEREDOC)))) { |
171 | 161 | return false; |
172 | 162 | } |
173 | | - doc = match[2] || match[4]; |
174 | | - indent = (doc.match(HEREDOC_INDENT) || ['']).sort()[0]; |
175 | | - doc = doc.replace(new RegExp("^" + indent, 'gm'), '').replace(MULTILINER, "\\n").replace(/"/g, '\\"'); |
| 163 | + doc = this.sanitize_heredoc(match[2] || match[4]); |
176 | 164 | this.token('STRING', '"' + doc + '"'); |
177 | 165 | this.line += this.count(match[1], "\n"); |
178 | 166 | this.i += match[1].length; |
|
194 | 182 | if (!((regex = this.match(REGEX, 1)))) { |
195 | 183 | return false; |
196 | 184 | } |
197 | | - if (NOT_REGEX.indexOf(this.tag()) >= 0) { |
| 185 | + if (include(NOT_REGEX, this.tag())) { |
198 | 186 | return false; |
199 | 187 | } |
200 | 188 | this.token('REGEX', regex); |
|
221 | 209 | } |
222 | 210 | this.line += indent.match(MULTILINER).length; |
223 | 211 | this.i += indent.length; |
224 | | - next_character = this.chunk.match(MULTI_DENT)[4]; |
225 | 212 | prev = this.prev(2); |
226 | 213 | size = indent.match(LAST_DENTS).reverse()[0].match(LAST_DENT)[1].length; |
| 214 | + next_character = this.chunk.match(MULTI_DENT)[4]; |
227 | 215 | no_newlines = next_character === '.' || (this.value() && this.value().match(NO_NEWLINE) && prev && (prev[0] !== '.') && !this.value().match(CODE)); |
228 | 216 | if (size === this.indent) { |
229 | 217 | if (no_newlines) { |
|
243 | 231 | this.indent = size; |
244 | 232 | return true; |
245 | 233 | }; |
246 | | - // Record an oudent token or tokens, if we're moving back inwards past |
| 234 | + // Record an outdent token or tokens, if we're moving back inwards past |
247 | 235 | // multiple recorded indents. |
248 | 236 | Lexer.prototype.outdent_token = function outdent_token(move_out, no_newlines) { |
249 | 237 | var last_indent; |
|
257 | 245 | } |
258 | 246 | return true; |
259 | 247 | }; |
260 | | - // Matches and consumes non-meaningful whitespace. |
| 248 | + // Matches and consumes non-meaningful whitespace. Tag the previous token |
| 249 | + // as being "spaced", because there are some cases where it matters. |
261 | 250 | Lexer.prototype.whitespace_token = function whitespace_token() { |
262 | 251 | var prev, space; |
263 | 252 | if (!((space = this.match(WHITESPACE, 1)))) { |
|
300 | 289 | tag = value; |
301 | 290 | if (value.match(ASSIGNMENT)) { |
302 | 291 | tag = 'ASSIGN'; |
303 | | - if (JS_FORBIDDEN.indexOf(this.value()) >= 0) { |
304 | | - throw new Error('SyntaxError: Reserved word "' + this.value() + '" on line ' + this.line + ' can\'t be assigned'); |
| 292 | + if (include(JS_FORBIDDEN, this.value)) { |
| 293 | + this.assignment_error(); |
305 | 294 | } |
306 | 295 | } else if (value === ';') { |
307 | 296 | tag = 'TERMINATOR'; |
|
312 | 301 | } else if (value === ']' && this.soaked_index) { |
313 | 302 | tag = 'SOAKED_INDEX_END'; |
314 | 303 | this.soaked_index = false; |
315 | | - } else if (CALLABLE.indexOf(this.tag()) >= 0 && not_spaced) { |
| 304 | + } else if (include(CALLABLE, this.tag()) && not_spaced) { |
316 | 305 | if (value === '(') { |
317 | 306 | tag = 'CALL_START'; |
318 | 307 | } |
|
324 | 313 | this.i += value.length; |
325 | 314 | return true; |
326 | 315 | }; |
| 316 | + // Token Manipulators ================================================== |
| 317 | + // As we consume a new IDENTIFIER, look at the previous token to determine |
| 318 | + // if it's a special kind of access. |
| 319 | + Lexer.prototype.name_access_type = function name_access_type() { |
| 320 | + if (this.value() === '::') { |
| 321 | + this.tag(1, 'PROTOTYPE_ACCESS'); |
| 322 | + } |
| 323 | + if (this.value() === '.' && !(this.value(2) === '.')) { |
| 324 | + if (this.tag(2) === '?') { |
| 325 | + this.tag(1, 'SOAK_ACCESS'); |
| 326 | + return this.tokens.splice(-2, 1); |
| 327 | + } else { |
| 328 | + return this.tag(1, 'PROPERTY_ACCESS'); |
| 329 | + } |
| 330 | + } |
| 331 | + }; |
| 332 | + // Sanitize a heredoc by escaping double quotes and erasing all external |
| 333 | + // indentation on the left-hand side. |
| 334 | + Lexer.prototype.sanitize_heredoc = function sanitize_heredoc(doc) { |
| 335 | + var indent; |
| 336 | + indent = (doc.match(HEREDOC_INDENT) || ['']).sort()[0]; |
| 337 | + return doc.replace(new RegExp("^" + indent, 'gm'), '').replace(MULTILINER, "\\n").replace(/"/g, '\\"'); |
| 338 | + }; |
| 339 | + // When you try to use a forbidden word in JavaScript as an identifier. |
| 340 | + Lexer.prototype.identifier_error = function identifier_error(word) { |
| 341 | + throw new Error('SyntaxError: Reserved word "' + word + '" on line ' + this.line); |
| 342 | + }; |
| 343 | + // When you try to assign to a reserved word in JavaScript, like "function". |
| 344 | + Lexer.prototype.assignment_error = function assignment_error() { |
| 345 | + throw new Error('SyntaxError: Reserved word "' + this.value() + '" on line ' + this.line + ' can\'t be assigned'); |
| 346 | + }; |
327 | 347 | // Helpers ============================================================= |
328 | 348 | // Add a token to the results, taking note of the line number. |
329 | 349 | Lexer.prototype.token = function token(tag, value) { |
|
408 | 428 | }; |
409 | 429 | return Lexer; |
410 | 430 | }).call(this); |
| 431 | + // Helper functions: |
| 432 | + // Does a list include a value? |
| 433 | + include = function include(list, value) { |
| 434 | + return list.indexOf(value) >= 0; |
| 435 | + }; |
411 | 436 | })(); |
0 commit comments