diff --git a/TestHScript.hx b/TestHScript.hx index 844eda17..db5efe65 100644 --- a/TestHScript.hx +++ b/TestHScript.hx @@ -252,6 +252,14 @@ class TestHScript extends TestCase { assertScript('var newMap = [{a:"a"}=>"foo", objKey=>"bar"]; newMap[objKey];', 'bar', vars); } + function testStringInterpolation():Void { + assertScript("var a = 5; 'a is ${a}'", 'a is 5'); + assertScript("var a = 5; 'a is ${a + 1}'", 'a is 6'); + assertScript("var a = 5; 'a is ${if (a > 3) \"big\" else \"small\"}'", 'a is big'); + assertScript("var a = 5; 'a is ${switch (a) { case 0: \"zero\"; case 5: \"five\"; default: \"other\"; }}'", 'a is five'); + assertScript("'Hello, ${{var val = false; if (val) \"world\" else {var num = 5 + 3; 'userid_${num}';}}}!'", 'Hello, userid_8!'); + } + static function main() { #if ((haxe_ver < 4) && php) // uncaught exception: The each() function is deprecated. This message will be suppressed on further calls (errno: 8192) diff --git a/hscript/Expr.hx b/hscript/Expr.hx index 7d5803c4..4e879e03 100644 --- a/hscript/Expr.hx +++ b/hscript/Expr.hx @@ -21,10 +21,15 @@ */ package hscript; +enum StringKind { + DoubleQuotes; + SingleQuotes; +} + enum Const { CInt( v : Int ); CFloat( f : Float ); - CString( s : String ); + CString( s : String , ?kind : StringKind); } #if hscriptPos diff --git a/hscript/Parser.hx b/hscript/Parser.hx index 9f917309..9abe445c 100644 --- a/hscript/Parser.hx +++ b/hscript/Parser.hx @@ -354,6 +354,19 @@ class Parser { e = mk(EIdent(id)); return isBlock(e) ? e : parseExprNext(e); case TConst(c): + if (c.match(CString(_, SingleQuotes))) + { + var ex = makeInterpolatedStr(tk); + if (ex.length == 1) + return parseExprNext(ex[0]); + else + { + var result = ex[0]; + for (i in 1...ex.length) + result = makeBinop("+", result, ex[i]); + return parseExprNext(mk(EParent(result), p1)); + } + } return parseExprNext(mk(EConst(c))); case TPOpen: tk = token(); @@ -644,6 +657,143 @@ class Parser { } } + function makeInterpolatedStr(tk:Token):Array { + var ex = new Array(); + var s = switch(tk) + { + case TConst(c): + switch(c) + { + case CString(s): + s; + case _: + null; + } + case _: + null; + } + + if (s == null) + error(ECustom("Invalid string literal"), tokenMin, tokenMax); + + var nextStr = ""; + function pushNextStr() { + if (nextStr != "") { + ex.push(mk(EConst(CString(nextStr)), tokenMin, tokenMax)); + nextStr = ""; + } + } + + var i = 0; + var c = s.charCodeAt(i); + while (true) + { + if (StringTools.isEof(c)) + { + pushNextStr(); + break; + } + + if (c != "$".code) + { + nextStr += String.fromCharCode(c); + c = s.charCodeAt(++i); + continue; + } + + c = s.charCodeAt(++i); + if (c >= 48 && c <= 57 || c == "$".code) + { + nextStr += "$" + String.fromCharCode(c); + c = s.charCodeAt(++i); + continue; + } + + if( idents[c] ) { + var id = String.fromCharCode(c); + while( true ) { + c = s.charCodeAt(++i); + if( StringTools.isEof(c) ) c = 0; + if( !idents[c] ) { + break; + } + id += String.fromCharCode(c); + } + pushNextStr(); + ex.push(mk(EIdent(id), tokenMin, tokenMax)); + nextStr = ""; + } + else if (c == "{".code) + { + pushNextStr(); + nextStr = ""; + var lastInput = input; + var lastReadPos = readPos; + var lastChar = char; + var lastTokens = tokens; + + function reset() + { + input = s; + readPos = i; + char = -1; + #if hscriptPos + tokens = new List(); + #else + tokens = new haxe.ds.GenericStack(); + #end + } + reset(); + var brOpens = -1; + while( true ) { + var tk = token(); + if ( tk == TBrOpen ) + { + if (brOpens == -1) + brOpens = 0; + brOpens++; + } + else if ( tk == TBrClose ) + { + if (brOpens == -1) + unexpected(tk); + brOpens--; + if (brOpens <= 0) + break; + } + if( tk == TEof ) + error(EUnterminatedString, currentPos, currentPos); + } + var endPos = readPos - 1; + reset(); + // cool hack to quickly close expressions + input = s.substring(i, endPos) + ";}"; + readPos = 0; + var a = new Array(); + while( true ) { + var tk = token(); + if( tk == TEof ) break; + push(tk); + parseFullExpr(a); + } + pushNextStr(); + ex.push(mk(EBlock(a),0)); + nextStr = ""; + input = lastInput; + i = endPos + 1; + c = s.charCodeAt(i); + readPos = lastReadPos; + char = lastChar; + tokens = lastTokens; + } + else + { + nextStr += "$"; + } + } + return ex; + } + function parseStructure(id) { #if hscriptPos var p1 = tokenMin; @@ -1385,6 +1535,10 @@ class Parser { inline function readChar() { return StringTools.fastCodeAt(input, readPos++); } + + inline function peekChar() { + return StringTools.fastCodeAt(input, readPos); + } function readString( until ) { var c = 0; @@ -1438,7 +1592,36 @@ class Parser { esc = true; else if( c == until ) break; - else { + else if (c == '$'.code && peekChar() == '{'.code) + { + b.addChar('$'.code); + var brOpens = -1; + while (true) + { + c = readChar(); + if (StringTools.isEof(c)) + { + line = old; + error(EUnterminatedString, p1, p1); + break; + } + b.addChar(c); + if ( c == '{'.code ) + { + if (brOpens == -1) + brOpens = 0; + brOpens++; + } + else if ( c == '}'.code ) + { + if (brOpens == -1) + invalidChar(c); + brOpens--; + if (brOpens <= 0) + break; + } + } + } else { if( c == 10 ) line++; b.addChar(c); } @@ -1589,7 +1772,8 @@ class Parser { case "}".code: return TBrClose; case "[".code: return TBkOpen; case "]".code: return TBkClose; - case "'".code, '"'.code: return TConst( CString(readString(char)) ); + case "'".code: return TConst( CString(readString(char), SingleQuotes) ); + case '"'.code: return TConst( CString(readString(char), DoubleQuotes) ); case "?".code: char = readChar(); if( char == ".".code )