diff --git a/lib/lexer.js b/lib/lexer.js index f1caca4d..f83f64ee 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -103,8 +103,8 @@ if (!(string = this.balancedString(this.chunk, [['"', '"'], ['#{', '}']]))) { return false; } - if (~string.indexOf('#{')) { - this.interpolateString(string); + if (0 < string.indexOf('#{', 1)) { + this.interpolateString(string.slice(1, -1)); } else { this.token('STRING', this.escapeLines(string)); } @@ -127,12 +127,12 @@ quote: quote, indent: null }); - if (quote === '"' && ~doc.indexOf('#{')) { - this.interpolateString(quote + doc + quote, { + if (quote === '"' && (0 <= doc.indexOf('#{'))) { + this.interpolateString(doc, { heredoc: true }); } else { - this.token('STRING', quote + this.escapeLines(doc, true) + quote); + this.token('STRING', this.makeString(doc, quote, true)); } this.line += count(heredoc, '\n'); this.i += heredoc.length; @@ -186,7 +186,7 @@ var _i, _len, _ref2, _ref3, _this, body, flags, heregex, re, tag, tokens, value; _ref2 = match, heregex = _ref2[0], body = _ref2[1], flags = _ref2[2]; this.i += heregex.length; - if (!(~body.indexOf('#{'))) { + if (0 > body.indexOf('#{')) { re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/'); this.token('REGEX', "/" + (re || '(?:)') + "/" + flags); return true; @@ -194,7 +194,7 @@ this.token('IDENTIFIER', 'RegExp'); this.tokens.push(['CALL_START', '(']); tokens = []; - _ref2 = this.interpolateString('"' + body + '"', { + _ref2 = this.interpolateString(body, { regex: true }); for (_i = 0, _len = _ref2.length; _i < _len; _i++) { @@ -202,10 +202,11 @@ if (tag === 'TOKENS') { tokens.push.apply(tokens, value); } else { - if (!(value = value.slice(1, -1).replace(HEREGEX_OMIT, ''))) { + if (!(value = value.replace(HEREGEX_OMIT, ''))) { continue; } - tokens.push(['STRING', '"' + value.replace(/[\\\"]/g, '\\$&') + '"']); + value = value.replace(/\\/g, '\\\\'); + tokens.push(['STRING', this.makeString(value, '"', true)]); } tokens.push(['+', '+']); } @@ -385,13 +386,13 @@ return accessor ? 'accessor' : false; }; Lexer.prototype.sanitizeHeredoc = function(doc, options) { - var _ref2, attempt, herecomment, indent, match, quote; + var _ref2, attempt, herecomment, indent, match; _ref2 = options, indent = _ref2.indent, herecomment = _ref2.herecomment; - if (herecomment && !include(doc, '\n')) { + if (herecomment && 0 > doc.indexOf('\n')) { return doc; } if (!(herecomment)) { - while ((match = HEREDOC_INDENT.exec(doc))) { + while (match = HEREDOC_INDENT.exec(doc)) { attempt = match[1]; if (indent === null || (0 < (_ref2 = attempt.length)) && (_ref2 < indent.length)) { indent = attempt; @@ -401,17 +402,8 @@ if (indent) { doc = doc.replace(RegExp("\\n" + indent, "g"), '\n'); } - if (herecomment) { - return doc; - } - quote = options.quote; - doc = doc.replace(/^\n/, ''); - doc = doc.replace(/\\([\s\S])/g, function(m, c) { - return ('\n' === c || quote === c) ? c : m; - }); - doc = doc.replace(RegExp("" + quote, "g"), '\\$&'); - if (quote === "'") { - doc = this.escapeLines(doc, true); + if (!(herecomment)) { + doc = doc.replace(/^\n/, ''); } return doc; }; @@ -487,15 +479,11 @@ return !i ? false : str.slice(0, i); }; Lexer.prototype.interpolateString = function(str, options) { - var _i, _len, _ref2, _this, char, expr, heredoc, i, inner, interpolated, lexer, nested, pi, regex, tag, tok, tokens, value; - if (str.length < 5) { - return this.token('STRING', this.escapeLines(str, heredoc)); - } + var _len, _ref2, _this, char, expr, heredoc, i, inner, interpolated, nested, pi, regex, tag, tokens, value; _ref2 = options || (options = {}), heredoc = _ref2.heredoc, regex = _ref2.regex; - lexer = new Lexer; tokens = []; - pi = 1; - i = 0; + pi = 0; + i = -1; while (char = str.charAt(i += 1)) { if (char === '\\') { i += 1; @@ -505,45 +493,39 @@ continue; } if (pi < i) { - tokens.push(['STRING', '"' + str.slice(pi, i) + '"']); + tokens.push(['TO_BE_STRING', str.slice(pi, i)]); } inner = expr.slice(1, -1).replace(LEADING_SPACES, '').replace(TRAILING_SPACES, ''); if (inner.length) { - if (heredoc) { - inner = inner.replace(/\\\"/g, '"'); - } - nested = lexer.tokenize("(" + inner + "\n)", { - line: this.line + nested = new Lexer().tokenize(inner, { + line: this.line, + rewrite: false }); - for (_i = 0, _len = nested.length; _i < _len; _i++) { - tok = nested[_i]; - if (tok[0] === 'CALL_END') { - (tok[0] = ')'); - } - } nested.pop(); - if (nested.length < 5) { - nested.pop(); - nested.shift(); + if (nested.length > 1) { + nested.unshift(['(', '(']); + nested.push([')', ')']); } tokens.push(['TOKENS', nested]); } i += expr.length; pi = i + 1; } - if ((i > pi) && (pi < str.length - 1)) { - tokens.push(['STRING', '"' + str.slice(pi)]); + if ((i > pi) && (pi < str.length)) { + tokens.push(['TO_BE_STRING', str.slice(pi)]); } if (regex) { return tokens; } - interpolated = tokens.length > 1; - if ((((_ref2 = tokens[0]) != null) ? _ref2[0] !== 'STRING' : undefined)) { - tokens.unshift(['STRING', '""']); + if (!(tokens.length)) { + return this.token('STRING', '""'); } - if (interpolated) { + if (interpolated = tokens.length > 1) { this.token('(', '('); } + if (tokens[0][0] !== 'TO_BE_STRING') { + this.tokens.push(['STRING', '""'], ['+', '+']); + } for (i = 0, _len = tokens.length; i < _len; i++) { _ref2 = tokens[i], tag = _ref2[0], value = _ref2[1]; if (i) { @@ -552,7 +534,7 @@ if (tag === 'TOKENS') { (_this = this.tokens).push.apply(_this, value); } else { - this.token(tag, this.escapeLines(value, heredoc)); + this.token('STRING', this.makeString(value, '"', heredoc)); } } if (interpolated) { @@ -584,6 +566,13 @@ Lexer.prototype.escapeLines = function(str, heredoc) { return str.replace(MULTILINER, heredoc ? '\\n' : ''); }; + Lexer.prototype.makeString = function(body, quote, heredoc) { + body = body.replace(/\\([\s\S])/g, function($amp, $1) { + return ('\n' === $1 || quote === $1) ? $1 : $amp; + }); + body = body.replace(RegExp("" + quote, "g"), '\\$&'); + return quote + this.escapeLines(body, heredoc) + quote; + }; return Lexer; })(); JS_KEYWORDS = ['if', 'else', 'true', 'false', 'new', 'return', 'try', 'catch', 'finally', 'throw', 'break', 'continue', 'for', 'in', 'while', 'delete', 'instanceof', 'typeof', 'switch', 'super', 'extends', 'class', 'this', 'null', 'debugger']; @@ -593,7 +582,7 @@ JS_FORBIDDEN = JS_KEYWORDS.concat(RESERVED); IDENTIFIER = /^[a-zA-Z_$][\w$]*/; NUMBER = /^0x[\da-f]+|^(?:\d+(\.\d+)?|\.\d+)(?:e[+-]?\d+)?/i; - HEREDOC = /^("""|''')([\s\S]*?)\n?[ \t]*\1/; + HEREDOC = /^("""|''')([\s\S]*?)(?:\n[ \t]*)?\1/; OPERATOR = /^(?:-[-=>]?|\+[+=]?|[*&|\/%=<>^:!?]+)(?=([ \t]*))/; WHITESPACE = /^[ \t]+/; COMMENT = /^###([^#][\s\S]*?)(?:###[ \t]*\n|(?:###)?$)|^(?:\s*#(?!##[^#]).*)+/; diff --git a/lib/nodes.js b/lib/nodes.js index 9edeecd4..bf8c78dd 100644 --- a/lib/nodes.js +++ b/lib/nodes.js @@ -220,10 +220,10 @@ var code; code = this.compileNode(o); if (o.scope.hasAssignments(this)) { - code = ("" + this.tab + "var " + (o.scope.compiledAssignments()) + ";\n" + code); + code = ("" + (this.tab) + "var " + (o.scope.compiledAssignments()) + ";\n" + code); } if (!o.globals && o.scope.hasDeclarations(this)) { - code = ("" + this.tab + "var " + (o.scope.compiledDeclarations()) + ";\n" + code); + code = ("" + (this.tab) + "var " + (o.scope.compiledDeclarations()) + ";\n" + code); } return code; }; @@ -295,7 +295,7 @@ if (this.expression.isStatement(o)) { o.asStatement = true; } - return "" + this.tab + "return " + (this.expression.compile(o)) + ";"; + return "" + (this.tab) + "return " + (this.expression.compile(o)) + ";"; }; return ReturnNode; })(); @@ -588,7 +588,7 @@ a = o.scope.freeVariable('ctor'); b = o.scope.freeVariable('ref'); c = o.scope.freeVariable('result'); - return "(function() {\n" + (idt = this.idt(1)) + "var ctor = function() {};\n" + idt + (utility('extends')) + "(ctor, " + a + " = " + (this.variable.compile(o)) + ");\n" + idt + "return typeof (" + c + " = " + a + ".apply(" + b + " = new ctor, " + splatargs + ")) === \"object\" ? " + c + " : " + b + ";\n" + this.tab + "})." + call; + return "(function() {\n" + (idt = this.idt(1)) + "var ctor = function() {};\n" + idt + (utility('extends')) + "(ctor, " + a + " = " + (this.variable.compile(o)) + ");\n" + idt + "return typeof (" + c + " = " + a + ".apply(" + b + " = new ctor, " + splatargs + ")) === \"object\" ? " + c + " : " + b + ";\n" + (this.tab) + "})." + call; }; return CallNode; })(); @@ -692,9 +692,9 @@ } idx = del(o, 'index'); step = del(o, 'step'); - vars = ("" + idx + " = " + this.fromVar); - intro = ("(" + this.fromVar + " <= " + this.toVar + " ? " + idx); - compare = ("" + intro + " <" + this.equals + " " + this.toVar + " : " + idx + " >" + this.equals + " " + this.toVar + ")"); + vars = ("" + idx + " = " + (this.fromVar)); + intro = ("(" + (this.fromVar) + " <= " + (this.toVar) + " ? " + idx); + compare = ("" + intro + " <" + (this.equals) + " " + (this.toVar) + " : " + idx + " >" + (this.equals) + " " + (this.toVar) + ")"); stepPart = step ? step.compile(o) : '1'; incr = step ? ("" + idx + " += " + stepPart) : ("" + intro + " += " + stepPart + " : " + idx + " -= " + stepPart + ")"); return "" + vars + "; " + compare + "; " + incr; @@ -705,7 +705,7 @@ idx = del(o, 'index'); step = del(o, 'step'); step && (step = ("" + idx + " += " + (step.compile(o)))); - return from <= to ? ("" + idx + " = " + from + "; " + idx + " <" + this.equals + " " + to + "; " + (step || ("" + idx + "++"))) : ("" + idx + " = " + from + "; " + idx + " >" + this.equals + " " + to + "; " + (step || ("" + idx + "--"))); + return from <= to ? ("" + idx + " = " + from + "; " + idx + " <" + (this.equals) + " " + to + "; " + (step || ("" + idx + "++"))) : ("" + idx + " = " + from + "; " + idx + " >" + (this.equals) + " " + to + "; " + (step || ("" + idx + "--"))); }; RangeNode.prototype.compileArray = function(o) { var _i, _ref2, _ref3, _result, body, clause, i, idt, post, pre, range, result, vars; @@ -731,8 +731,8 @@ o.index = i; body = this.compileSimple(o); } else { - clause = ("" + this.fromVar + " <= " + this.toVar + " ?"); - body = ("var " + i + " = " + this.fromVar + "; " + clause + " " + i + " <" + this.equals + " " + this.toVar + " : " + i + " >" + this.equals + " " + this.toVar + "; " + clause + " " + i + " += 1 : " + i + " -= 1"); + clause = ("" + (this.fromVar) + " <= " + (this.toVar) + " ?"); + body = ("var " + i + " = " + (this.fromVar) + "; " + clause + " " + i + " <" + (this.equals) + " " + (this.toVar) + " : " + i + " >" + (this.equals) + " " + (this.toVar) + "; " + clause + " " + i + " += 1 : " + i + " -= 1"); } post = ("{ " + result + ".push(" + i + "); }\n" + idt + "return " + result + ";\n" + (o.indent)); return "(function() {" + pre + "\n" + idt + "for (" + body + ")" + post + "}).call(this)"; @@ -844,7 +844,7 @@ } } objects = objects.join(''); - return indexOf(objects, '\n') >= 0 ? ("[\n" + (this.idt(1)) + objects + "\n" + this.tab + "]") : ("[" + objects + "]"); + return indexOf(objects, '\n') >= 0 ? ("[\n" + (this.idt(1)) + objects + "\n" + (this.tab) + "]") : ("[" + objects + "]"); }; return ArrayNode; })(); @@ -977,7 +977,7 @@ } val = ("" + name + " = " + val); if (stmt) { - return ("" + this.tab + val + ";"); + return ("" + (this.tab) + val + ";"); } return top || this.parenthetical ? val : ("(" + val + ")"); }; @@ -1127,7 +1127,7 @@ code = this.body.expressions.length ? ("\n" + (this.body.compileWithDeclarations(o)) + "\n") : ''; func = ("function(" + (params.join(', ')) + ") {" + code + (code && this.tab) + "}"); if (this.bound) { - return ("(" + (utility('bind')) + "(" + func + ", " + this.context + "))"); + return ("(" + (utility('bind')) + "(" + func + ", " + (this.context) + "))"); } return top ? ("(" + func + ")") : func; }; @@ -1203,7 +1203,7 @@ o.scope.assign(trailing.compile(o), "arguments[" + variadic + " ? " + len + " - " + pos + " : " + (this.index + idx) + "]"); } } - return "" + name + " = " + (utility('slice')) + ".call(arguments, " + this.index + end + ")"; + return "" + name + " = " + (utility('slice')) + ".call(arguments, " + (this.index) + end + ")"; }; SplatNode.prototype.compileValue = function(o, name, index, trailings) { var trail; @@ -1271,12 +1271,12 @@ set = ''; if (!(top)) { rvar = o.scope.freeVariable('result'); - set = ("" + this.tab + rvar + " = [];\n"); + set = ("" + (this.tab) + rvar + " = [];\n"); if (this.body) { this.body = PushNode.wrap(rvar, this.body); } } - pre = ("" + set + this.tab + "while (" + cond + ")"); + pre = ("" + set + (this.tab) + "while (" + cond + ")"); if (this.guard) { this.body = Expressions.wrap([new IfNode(this.guard, this.body)]); } @@ -1287,7 +1287,7 @@ } else { post = ''; } - return "" + pre + " {\n" + (this.body.compile(o)) + "\n" + this.tab + "}" + post; + return "" + pre + " {\n" + (this.body.compile(o)) + "\n" + (this.tab) + "}" + post; }; return WhileNode; })(); @@ -1377,7 +1377,7 @@ shared = this.first.unwrap().second; _ref2 = shared.compileReference(o), this.first.second = _ref2[0], shared = _ref2[1]; _ref2 = [this.first.compile(o), this.second.compile(o), shared.compile(o)], first = _ref2[0], second = _ref2[1], shared = _ref2[2]; - return "(" + first + ") && (" + shared + " " + this.operator + " " + second + ")"; + return "(" + first + ") && (" + shared + " " + (this.operator) + " " + second + ")"; }; OpNode.prototype.compileAssignment = function(o) { var _ref2, left, rite; @@ -1446,7 +1446,7 @@ }), this.arr1 = _ref2[0], this.arr2 = _ref2[1]; _ref2 = [o.scope.freeVariable('i'), o.scope.freeVariable('len')], i = _ref2[0], l = _ref2[1]; prefix = this.obj1 !== this.obj2 ? this.obj1 + '; ' : ''; - return "(function(){ " + prefix + "for (var " + i + "=0, " + l + "=" + this.arr1 + ".length; " + i + "<" + l + "; " + i + "++) { if (" + this.arr2 + "[" + i + "] === " + this.obj2 + ") return true; } return false; }).call(this)"; + return "(function(){ " + prefix + "for (var " + i + "=0, " + l + "=" + (this.arr1) + ".length; " + i + "<" + l + "; " + i + "++) { if (" + (this.arr2) + "[" + i + "] === " + (this.obj2) + ") return true; } return false; }).call(this)"; }; return InNode; })(); @@ -1478,9 +1478,9 @@ o.top = true; attemptPart = this.attempt.compile(o); errorPart = this.error ? (" (" + (this.error.compile(o)) + ") ") : ' '; - catchPart = this.recovery ? (" catch" + errorPart + "{\n" + (this.recovery.compile(o)) + "\n" + this.tab + "}") : ''; - finallyPart = (this.ensure || '') && ' finally {\n' + this.ensure.compile(merge(o)) + ("\n" + this.tab + "}"); - return "" + this.tab + "try {\n" + attemptPart + "\n" + this.tab + "}" + catchPart + finallyPart; + catchPart = this.recovery ? (" catch" + errorPart + "{\n" + (this.recovery.compile(o)) + "\n" + (this.tab) + "}") : ''; + finallyPart = (this.ensure || '') && ' finally {\n' + this.ensure.compile(merge(o)) + ("\n" + (this.tab) + "}"); + return "" + (this.tab) + "try {\n" + attemptPart + "\n" + (this.tab) + "}" + catchPart + finallyPart; }; return TryNode; })(); @@ -1496,7 +1496,7 @@ ThrowNode.prototype.isStatement = YES; ThrowNode.prototype.makeReturn = THIS; ThrowNode.prototype.compileNode = function(o) { - return "" + this.tab + "throw " + (this.expression.compile(o)) + ";"; + return "" + (this.tab) + "throw " + (this.expression.compile(o)) + ";"; }; return ThrowNode; })(); @@ -1651,7 +1651,7 @@ } } sourcePart = (rvar ? ("" + rvar + " = []; ") : '') + sourcePart; - sourcePart = sourcePart ? ("" + this.tab + sourcePart + "\n" + this.tab) : this.tab; + sourcePart = sourcePart ? ("" + (this.tab) + sourcePart + "\n" + (this.tab)) : this.tab; returnResult = this.compileReturnValue(rvar, o); if (!(topLevel)) { body = PushNode.wrap(rvar, body); @@ -1686,7 +1686,7 @@ top: true })); vars = range ? name : ("" + name + ", " + ivar); - return "" + sourcePart + "for (" + forPart + ") {" + guardPart + "\n" + varPart + body + "\n" + this.tab + "}" + returnResult; + return "" + sourcePart + "for (" + forPart + ") {" + guardPart + "\n" + varPart + body + "\n" + (this.tab) + "}" + returnResult; }; return ForNode; })(); @@ -1720,7 +1720,7 @@ var _i, _j, _len, _len2, _ref2, _ref3, block, code, condition, conditions, exprs, idt, pair; idt = (o.indent = this.idt(2)); o.top = true; - code = ("" + this.tab + "switch (" + (this.subject.compile(o)) + ") {"); + code = ("" + (this.tab) + "switch (" + (this.subject.compile(o)) + ") {"); _ref2 = this.cases; for (_i = 0, _len = _ref2.length; _i < _len; _i++) { pair = _ref2[_i]; @@ -1742,7 +1742,7 @@ if (this.otherwise) { code += ("\n" + (this.idt(1)) + "default:\n" + (this.otherwise.compile(o))); } - code += ("\n" + this.tab + "}"); + code += ("\n" + (this.tab) + "}"); return code; }; return SwitchNode; @@ -1828,14 +1828,14 @@ ifDent = child || (top && !this.isStatement(o)) ? '' : this.idt(); comDent = child ? this.idt() : ''; body = this.body.compile(o); - ifPart = ("" + ifDent + "if (" + (this.compileCondition(condO)) + ") {\n" + body + "\n" + this.tab + "}"); + ifPart = ("" + ifDent + "if (" + (this.compileCondition(condO)) + ") {\n" + body + "\n" + (this.tab) + "}"); if (!(this.elseBody)) { return ifPart; } elsePart = this.isChain ? ' else ' + this.elseBodyNode().compile(merge(o, { indent: this.idt(), chainChild: true - })) : (" else {\n" + (this.elseBody.compile(o)) + "\n" + this.tab + "}"); + })) : (" else {\n" + (this.elseBody.compile(o)) + "\n" + (this.tab) + "}"); return "" + ifPart + elsePart; }; IfNode.prototype.compileTernary = function(o) { diff --git a/lib/optparse.js b/lib/optparse.js index 3188b2cc..73f79082 100755 --- a/lib/optparse.js +++ b/lib/optparse.js @@ -40,7 +40,7 @@ var _i, _len, _ref, letPart, lines, rule, spaces; lines = ['Available options:']; if (this.banner) { - lines.unshift("" + this.banner + "\n"); + lines.unshift("" + (this.banner) + "\n"); } _ref = this.rules; for (_i = 0, _len = _ref.length; _i < _len; _i++) { diff --git a/lib/rewriter.js b/lib/rewriter.js index c079c439..30ab6d63 100644 --- a/lib/rewriter.js +++ b/lib/rewriter.js @@ -63,7 +63,7 @@ } else { tokens.splice(i, 0, after); } - } else if (!('TERMINATOR' === (_ref = ((prev != null) ? prev[0] : undefined)) || 'INDENT' === _ref || 'OUTDENT' === _ref)) { + } else if (prev && !('TERMINATOR' === (_ref = prev[0]) || 'INDENT' === _ref || 'OUTDENT' === _ref)) { if (((post != null) ? post[0] === 'TERMINATOR' : undefined) && ((after != null) ? after[0] === 'OUTDENT' : undefined)) { tokens.splice.apply(tokens, [i + 2, 0].concat(tokens.splice(i, 2))); if (tokens[i + 2][0] !== 'TERMINATOR') { diff --git a/src/lexer.coffee b/src/lexer.coffee index cbefc874..399abb09 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -125,8 +125,8 @@ exports.Lexer = class Lexer @token 'STRING', (string = match[0]).replace MULTILINER, '\\\n' when '"' return false unless string = @balancedString @chunk, [['"', '"'], ['#{', '}']] - if ~string.indexOf '#{' - @interpolateString string + if 0 < string.indexOf '#{', 1 + @interpolateString string.slice 1, -1 else @token 'STRING', @escapeLines string else @@ -142,10 +142,10 @@ exports.Lexer = class Lexer heredoc = match[0] quote = heredoc.charAt 0 doc = @sanitizeHeredoc match[2], {quote, indent: null} - if quote is '"' and ~doc.indexOf '#{' - @interpolateString quote + doc + quote, heredoc: yes + if quote is '"' and 0 <= doc.indexOf '#{' + @interpolateString doc, heredoc: yes else - @token 'STRING', quote + @escapeLines(doc, yes) + quote + @token 'STRING', @makeString doc, quote, yes @line += count heredoc, '\n' @i += heredoc.length true @@ -185,19 +185,20 @@ exports.Lexer = class Lexer heregexToken: (match) -> [heregex, body, flags] = match @i += heregex.length - unless ~body.indexOf '#{' + if 0 > body.indexOf '#{' re = body.replace(HEREGEX_OMIT, '').replace(/\//g, '\\/') @token 'REGEX', "/#{ re or '(?:)' }/#{flags}" return true @token 'IDENTIFIER', 'RegExp' @tokens.push ['CALL_START', '('] tokens = [] - for [tag, value] in @interpolateString('"' + body + '"', regex: yes) + for [tag, value] in @interpolateString(body, regex: yes) if tag is 'TOKENS' tokens.push value... else - continue unless value = value.slice(1, -1).replace HEREGEX_OMIT, '' - tokens.push ['STRING', '"' + value.replace(/[\\\"]/g, '\\$&') + '"'] + continue unless value = value.replace HEREGEX_OMIT, '' + value = value.replace /\\/g, '\\\\' + tokens.push ['STRING', @makeString(value, '"', yes)] tokens.push ['+', '+'] tokens.pop() @tokens.push ['STRING', '""'], ['+', '+'] unless tokens[0]?[0] is 'STRING' @@ -344,22 +345,17 @@ exports.Lexer = class Lexer prev[0] is '@' if accessor then 'accessor' else false - # Sanitize a heredoc or herecomment by escaping internal double quotes and + # Sanitize a heredoc or herecomment by # erasing all external indentation on the left-hand side. sanitizeHeredoc: (doc, options) -> {indent, herecomment} = options - return doc if herecomment and not include doc, '\n' + return doc if herecomment and 0 > doc.indexOf '\n' unless herecomment - while (match = HEREDOC_INDENT.exec doc) + while match = HEREDOC_INDENT.exec doc attempt = match[1] indent = attempt if indent is null or 0 < attempt.length < indent.length doc = doc.replace /// \n #{indent} ///g, '\n' if indent - return doc if herecomment - {quote} = options - doc = doc.replace /^\n/, '' - doc = doc.replace /\\([\s\S])/g, (m, c) -> if c in ['\n', quote] then c else m - doc = doc.replace /// #{quote} ///g, '\\$&' - doc = @escapeLines doc, yes if quote is "'" + doc = doc.replace /^\n/, '' unless herecomment doc # A source of ambiguity in our grammar used to be parameter lists in function @@ -429,12 +425,10 @@ exports.Lexer = class Lexer # new Lexer, tokenize the interpolated contents, and merge them into the # token stream. interpolateString: (str, options) -> - return @token 'STRING', @escapeLines(str, heredoc) if str.length < 5 # "#{}" {heredoc, regex} = options or= {} - lexer = new Lexer tokens = [] - pi = 1 - i = 0 + pi = 0 + i = -1 while char = str.charAt i += 1 if char is '\\' i += 1 @@ -442,28 +436,28 @@ exports.Lexer = class Lexer unless char is '#' and str.charAt(i+1) is '{' and (expr = @balancedString str[i+1..], [['{', '}']]) continue - tokens.push ['STRING', '"' + str[pi...i] + '"'] if pi < i + tokens.push ['TO_BE_STRING', str[pi...i]] if pi < i inner = expr.slice(1, -1).replace(LEADING_SPACES, '').replace(TRAILING_SPACES, '') if inner.length - inner = inner.replace /\\\"/g, '"' if heredoc - nested = lexer.tokenize "(#{inner}\n)", line: @line - (tok[0] = ')') for tok in nested when tok[0] is 'CALL_END' + nested = new Lexer().tokenize inner, line: @line, rewrite: off nested.pop() - if nested.length < 5 then nested.pop(); nested.shift() + if nested.length > 1 + nested.unshift ['(', '('] + nested.push [')', ')'] tokens.push ['TOKENS', nested] i += expr.length pi = i + 1 - tokens.push ['STRING', '"' + str.slice pi] if i > pi < str.length - 1 + tokens.push ['TO_BE_STRING', str[pi..]] if i > pi < str.length return tokens if regex - interpolated = tokens.length > 1 - tokens.unshift ['STRING', '""'] unless tokens[0]?[0] is 'STRING' - @token '(', '(' if interpolated + return @token 'STRING', '""' unless tokens.length + @token '(', '(' if interpolated = tokens.length > 1 + @tokens.push ['STRING', '""'], ['+', '+'] unless tokens[0][0] is 'TO_BE_STRING' for [tag, value], i in tokens @token '+', '+' if i if tag is 'TOKENS' @tokens.push value... else - @token tag, @escapeLines value, heredoc + @token 'STRING', @makeString value, '"', heredoc @token ')', ')' if interpolated tokens @@ -494,6 +488,13 @@ exports.Lexer = class Lexer escapeLines: (str, heredoc) -> str.replace MULTILINER, if heredoc then '\\n' else '' + # Constructs a string token by escaping quotes and newlines. + makeString: (body, quote, heredoc) -> + body = body.replace /\\([\s\S])/g, ($amp, $1) -> + if $1 in ['\n', quote] then $1 else $amp + body = body.replace /// #{quote} ///g, '\\$&' + quote + @escapeLines(body, heredoc) + quote + # Constants # --------- @@ -535,7 +536,7 @@ JS_FORBIDDEN = JS_KEYWORDS.concat RESERVED # Token matching regexes. IDENTIFIER = /^[a-zA-Z_$][\w$]*/ NUMBER = /^0x[\da-f]+|^(?:\d+(\.\d+)?|\.\d+)(?:e[+-]?\d+)?/i -HEREDOC = /^("""|''')([\s\S]*?)\n?[ \t]*\1/ +HEREDOC = /^("""|''')([\s\S]*?)(?:\n[ \t]*)?\1/ OPERATOR = /^(?:-[-=>]?|\+[+=]?|[*&|\/%=<>^:!?]+)(?=([ \t]*))/ WHITESPACE = /^[ \t]+/ COMMENT = /^###([^#][\s\S]*?)(?:###[ \t]*\n|(?:###)?$)|^(?:\s*#(?!##[^#]).*)+/ diff --git a/src/rewriter.coffee b/src/rewriter.coffee index 70ea972f..3cb57c6c 100644 --- a/src/rewriter.coffee +++ b/src/rewriter.coffee @@ -73,7 +73,7 @@ class exports.Rewriter tokens.splice i - 2, 1 else tokens.splice i, 0, after - else if prev?[0] not in ['TERMINATOR', 'INDENT', 'OUTDENT'] + else if prev and prev[0] not in ['TERMINATOR', 'INDENT', 'OUTDENT'] if post?[0] is 'TERMINATOR' and after?[0] is 'OUTDENT' tokens.splice i + 2, 0, tokens.splice(i, 2)... if tokens[i + 2][0] isnt 'TERMINATOR' diff --git a/test/test_heredocs.coffee b/test/test_heredocs.coffee index 5c727483..330ecdf7 100644 --- a/test/test_heredocs.coffee +++ b/test/test_heredocs.coffee @@ -89,8 +89,7 @@ a = """ """ ok a is "one\ntwo\n" - -equal ''' line 0 +eq ''' line 0 should not be relevant to the indent level ''', ' @@ -99,10 +98,14 @@ should not be relevant\n to the indent level ' +eq ''' '\\\' ''', " '\\' " +eq """ "\\\" """, ' "\\" ' -equal 'multiline nested interpolations work', """multiline #{ +eq ''' <- keep these spaces -> ''', ' <- keep these spaces -> ' + +eq 'multiline nested "interpolations" work', """multiline #{ "nested #{(-> ok yes - "interpolations" + "\"interpolations\"" )()}" } work""" diff --git a/test/test_regexps.coffee b/test/test_regexps.coffee index 759f75e7..e78e89de 100644 --- a/test/test_regexps.coffee +++ b/test/test_regexps.coffee @@ -25,11 +25,12 @@ ok (obj.width()/id - obj.height()/id) is -5 eq /^I'm\s+Heregex?\/\/\//gim + '', /// ^ I'm \s+ Heregex? / // # or not ///gim + '' -eq '\\\\#{}', /// +eq '\\\\#{}\\\\\\\"', /// #{ "#{ '\\' }" # normal comment } # regex comment \#{} + \\ \" ///.source eq /// /// + '', '/(?:)/'