From fe7d5dfd1951bceaa440ab1ba27704759c078124 Mon Sep 17 00:00:00 2001 From: Stan Angeloff Date: Sat, 6 Mar 2010 00:30:49 +0200 Subject: [PATCH] Added string interpolation for identifiers --- lib/lexer.js | 53 ++++++++++++++++++++++++++- src/lexer.coffee | 32 +++++++++++++++- test/test_string_interpolation.coffee | 18 +++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 test/test_string_interpolation.coffee diff --git a/lib/lexer.js b/lib/lexer.js index 8512f883..494d26b5 100644 --- a/lib/lexer.js +++ b/lib/lexer.js @@ -168,7 +168,7 @@ return false; } escaped = string.replace(STRING_NEWLINES, " \\\n"); - this.token('STRING', escaped); + this.interpolate_string(escaped); this.line += count(string, "\n"); this.i += string.length; return true; @@ -397,6 +397,57 @@ Lexer.prototype.assignment_error = function assignment_error() { throw new Error('SyntaxError: Reserved word "' + this.value() + '" on line ' + this.line + ' can\'t be assigned'); }; + // Replace variables and block calls inside double-quoted strings. + Lexer.prototype.interpolate_string = function interpolate_string(escaped) { + var _a, _b, _c, _d, _e, _f, _g, _h, before, each, group, i, identifier, identifier_match, quote, tokens; + if (escaped.length < 3 || escaped.indexOf('"') !== 0) { + return this.token('STRING', escaped); + } else { + tokens = []; + quote = escaped.substring(0, 1); + escaped = escaped.substring(1, escaped.length - 1); + while (escaped.length) { + identifier_match = escaped.match(/(^|[\s\S]*?(?:[\\]|\\\\)?)(\$([a-zA-Z_]\w*))/); + if (identifier_match) { + _a = identifier_match; + group = _a[0]; + before = _a[1]; + identifier = _a[2]; + if (before.substring(before.length - 1) === '\\') { + if (before.length) { + tokens.push(['STRING', quote + before.substring(0, before.length - 1) + identifier + quote]); + } + } else { + if (before.length) { + tokens.push(['STRING', quote + before + quote]); + } + tokens.push(['IDENTIFIER', identifier.substring(1)]); + } + escaped = escaped.substring(group.length); + } else { + tokens.push(['STRING', quote + escaped + quote]); + escaped = ''; + } + } + if (tokens.length > 1) { + _d = tokens.length - 1; _e = 1; + for (_c = 0, i = _d; (_d <= _e ? i <= _e : i >= _e); (_d <= _e ? i += 1 : i -= 1), _c++) { + tokens[i][0] === 'STRING' && tokens[i - 1][0] === 'STRING' ? tokens.splice(i - 1, 2, ['STRING', quote + tokens[i - 1][1].substring(1, tokens[i - 1][1].length - 1) + tokens[i][1].substring(1, tokens[i][1].length - 1) + quote]) : null; + } + } + _f = []; _g = tokens; + for (i = 0, _h = _g.length; i < _h; i++) { + each = _g[i]; + _f.push((function() { + this.token(each[0], each[1]); + if (i < tokens.length - 1) { + return this.token('+', '+'); + } + }).call(this)); + } + return _f; + } + }; // Helpers // ------- // Add a token to the results, taking note of the line number. diff --git a/src/lexer.coffee b/src/lexer.coffee index bfafb2f6..fb0102ef 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -166,7 +166,7 @@ exports.Lexer: class Lexer string_token: -> return false unless string: @match STRING, 1 escaped: string.replace STRING_NEWLINES, " \\\n" - @token 'STRING', escaped + @interpolate_string escaped @line += count string, "\n" @i += string.length true @@ -340,6 +340,36 @@ exports.Lexer: class Lexer assignment_error: -> throw new Error 'SyntaxError: Reserved word "' + @value() + '" on line ' + @line + ' can\'t be assigned' + # Replace variables and block calls inside double-quoted strings. + interpolate_string: (escaped) -> + if escaped.length < 3 or escaped.indexOf('"') isnt 0 + @token 'STRING', escaped + else + tokens: [] + quote: escaped.substring(0, 1) + escaped: escaped.substring(1, escaped.length - 1) + while escaped.length + identifier_match: escaped.match /(^|[\s\S]*?(?:[\\]|\\\\)?)(\$([a-zA-Z_]\w*))/ + if identifier_match + [group, before, identifier]: identifier_match + if before.substring(before.length - 1) is '\\' + tokens.push ['STRING', quote + before.substring(0, before.length - 1) + identifier + quote] if before.length + else + tokens.push ['STRING', quote + before + quote] if before.length + tokens.push ['IDENTIFIER', identifier.substring(1)] + escaped: escaped.substring(group.length) + else + tokens.push ['STRING', quote + escaped + quote] + escaped: '' + if tokens.length > 1 + for i in [tokens.length - 1..1] + if tokens[i][0] is 'STRING' and tokens[i - 1][0] is 'STRING' + tokens.splice i - 1, 2, ['STRING', quote + tokens[i - 1][1].substring(1, tokens[i - 1][1].length - 1) + + tokens[i][1].substring(1, tokens[i][1].length - 1) + quote] + for each, i in tokens + @token each[0], each[1] + @token '+', '+' if i < tokens.length - 1 + # Helpers # ------- diff --git a/test/test_string_interpolation.coffee b/test/test_string_interpolation.coffee new file mode 100644 index 00000000..bccb712a --- /dev/null +++ b/test/test_string_interpolation.coffee @@ -0,0 +1,18 @@ +hello: 'Hello' +world: 'World' +ok '$hello $world!' is '$hello $world!' +ok "$hello $world!" is 'Hello World!' +ok "[$hello$world]" is '[HelloWorld]' +ok "$hello$$world" is 'Hello$World' + +[s, t, r, i, n, g]: ['s', 't', 'r', 'i', 'n', 'g'] +ok "$s$t$r$i$n$g" is 'string' +ok "\\$s\\$t\\$r\\$i\\$n\\$g" is '$s$t$r$i$n$g' +ok "\\$string" is '$string' + +ok "\\$Escaping first" is '$Escaping first' +ok "Escaping \\$in middle" is 'Escaping $in middle' +ok "Escaping \\$last" is 'Escaping $last' + +ok "$$" is '$$' +ok "\\\\$$" is '\\\\$$'