diff --git a/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj b/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj index 66ac0bc41..a4b527323 100644 --- a/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj +++ b/src/Orchard.Tests.Modules/Orchard.Tests.Modules.csproj @@ -135,10 +135,10 @@ - - - - + + + + diff --git a/src/Orchard.Tests.Modules/SimpleScripting/EvaluatorTests.cs b/src/Orchard.Tests.Modules/SimpleScripting/EvaluatorTests.cs new file mode 100644 index 000000000..b20e238ef --- /dev/null +++ b/src/Orchard.Tests.Modules/SimpleScripting/EvaluatorTests.cs @@ -0,0 +1,13 @@ +using NUnit.Framework; +using Orchard.Widgets.SimpleScripting; + +namespace Orchard.Tests.Modules.SimpleScriptingTests { + [TestFixture] + public class EvaluatorTests { + [Test] + public void EvaluateSimpleConstant() { + var tree = new Parser("1*2+3").Parse(); + + } + } +} diff --git a/src/Orchard.Tests.Modules/SimpleScriptingTests/ExpressionParserTests.cs b/src/Orchard.Tests.Modules/SimpleScripting/ParserTests.cs similarity index 56% rename from src/Orchard.Tests.Modules/SimpleScriptingTests/ExpressionParserTests.cs rename to src/Orchard.Tests.Modules/SimpleScripting/ParserTests.cs index 069f31e81..db7799788 100644 --- a/src/Orchard.Tests.Modules/SimpleScriptingTests/ExpressionParserTests.cs +++ b/src/Orchard.Tests.Modules/SimpleScripting/ParserTests.cs @@ -5,10 +5,10 @@ using Orchard.Widgets.SimpleScripting; namespace Orchard.Tests.Modules.SimpleScriptingTests { [TestFixture] - public class ExpressionParserTests { + public class ParserTests { [Test] public void ParserShouldUnderstandConstantExpressions() { - var tree = new ExpressionParser("true").Parse(); + var tree = new Parser("true").Parse(); CheckTree(tree, new object[] { "const", true, }); @@ -16,9 +16,9 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandBinaryExpressions() { - var tree = new ExpressionParser("true+true").Parse(); + var tree = new Parser("true+true").Parse(); CheckTree(tree, new object[] { - "binop", TokenKind.Plus, + "binop", TerminalKind.Plus, "const", true, "const", true, }); @@ -26,11 +26,11 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandOperatorPrecedence() { - var tree = new ExpressionParser("1+2*3").Parse(); + var tree = new Parser("1+2*3").Parse(); CheckTree(tree, new object[] { - "binop", TokenKind.Plus, + "binop", TerminalKind.Plus, "const", 1, - "binop", TokenKind.Mul, + "binop", TerminalKind.Mul, "const", 2, "const", 3, }); @@ -38,10 +38,10 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandOperatorPrecedence2() { - var tree = new ExpressionParser("1*2+3").Parse(); + var tree = new Parser("1*2+3").Parse(); CheckTree(tree, new object[] { - "binop", TokenKind.Plus, - "binop", TokenKind.Mul, + "binop", TerminalKind.Plus, + "binop", TerminalKind.Mul, "const", 1, "const", 2, "const", 3, @@ -50,10 +50,10 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandOperatorPrecedence3() { - var tree = new ExpressionParser("not true or true").Parse(); + var tree = new Parser("not true or true").Parse(); CheckTree(tree, new object[] { - "binop", TokenKind.Or, - "unop", TokenKind.Not, + "binop", TerminalKind.Or, + "unop", TerminalKind.Not, "const", true, "const", true, }); @@ -61,10 +61,10 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandOperatorPrecedence4() { - var tree = new ExpressionParser("not (true or true)").Parse(); + var tree = new Parser("not (true or true)").Parse(); CheckTree(tree, new object[] { - "unop", TokenKind.Not, - "binop", TokenKind.Or, + "unop", TerminalKind.Not, + "binop", TerminalKind.Or, "const", true, "const", true, }); @@ -72,11 +72,11 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandParenthesis() { - var tree = new ExpressionParser("1*(2+3)").Parse(); + var tree = new Parser("1*(2+3)").Parse(); CheckTree(tree, new object[] { - "binop", TokenKind.Mul, + "binop", TerminalKind.Mul, "const", 1, - "binop", TokenKind.Plus, + "binop", TerminalKind.Plus, "const", 2, "const", 3, }); @@ -84,15 +84,15 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldUnderstandComplexExpressions() { - var tree = new ExpressionParser("not 1 * (2 / 4 * 6 + (3))").Parse(); + var tree = new Parser("not 1 * (2 / 4 * 6 + (3))").Parse(); CheckTree(tree, new object[] { - "unop", TokenKind.Not, - "binop", TokenKind.Mul, + "unop", TerminalKind.Not, + "binop", TerminalKind.Mul, "const", 1, - "binop", TokenKind.Plus, - "binop", TokenKind.Div, + "binop", TerminalKind.Plus, + "binop", TerminalKind.Div, "const", 2, - "binop", TokenKind.Mul, + "binop", TerminalKind.Mul, "const", 4, "const", 6, "const", 3, @@ -101,15 +101,15 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { [Test] public void ParserShouldContainErrorExpressions() { - var tree = new ExpressionParser("1 + not 3").Parse(); + var tree = new Parser("1 + not 3").Parse(); CheckTree(tree, new object[] { - "binop", TokenKind.Plus, + "binop", TerminalKind.Plus, "const", 1, "error", }); } - private void CheckTree(ExpressionTree tree, object[] objects) { + private void CheckTree(AbstractSyntaxTree tree, object[] objects) { Assert.That(tree, Is.Not.Null); Assert.That(tree.Root, Is.Not.Null); @@ -118,39 +118,39 @@ namespace Orchard.Tests.Modules.SimpleScriptingTests { Assert.That(index, Is.EqualTo(objects.Length)); } - private void CheckExpression(ExpressionTree.Expression expression, int indent, object[] objects, ref int index) { + private void CheckExpression(AstNode astNode, int indent, object[] objects, ref int index) { var exprName = (string)objects[index++]; Type type = null; switch(exprName) { case "const": - type = typeof(ExpressionTree.ConstantExpression); + type = typeof(ConstantAstNode); break; case "binop": - type = typeof(ExpressionTree.BinaryExpression); + type = typeof(BinaryAstNode); break; case "unop": - type = typeof(ExpressionTree.UnaryExpression); + type = typeof(UnaryAstNode); break; case "error": - type = typeof(ExpressionTree.ErrorExpression); + type = typeof(ErrorAstNode); break; } - Trace.WriteLine(string.Format("{0}: {1}{2} (Current: {3})", indent, new string(' ', indent * 2), type.Name, expression)); + Trace.WriteLine(string.Format("{0}: {1}{2} (Current: {3})", indent, new string(' ', indent * 2), type.Name, astNode)); - Assert.That(expression.GetType(), Is.EqualTo(type)); + Assert.That(astNode.GetType(), Is.EqualTo(type)); if (exprName == "const") { - Assert.That((expression as ExpressionTree.ConstantExpression).Value, Is.EqualTo(objects[index++])); + Assert.That((astNode as ConstantAstNode).Value, Is.EqualTo(objects[index++])); } else if (exprName == "binop") { - Assert.That((expression as ExpressionTree.BinaryExpression).Operator.Kind, Is.EqualTo(objects[index++])); + Assert.That((astNode as BinaryAstNode).Operator.Kind, Is.EqualTo(objects[index++])); } else if (exprName == "unop") { - Assert.That((expression as ExpressionTree.UnaryExpression).Operator.Kind, Is.EqualTo(objects[index++])); + Assert.That((astNode as UnaryAstNode).Operator.Kind, Is.EqualTo(objects[index++])); } - foreach(var child in expression.Children) { + foreach(var child in astNode.Children) { CheckExpression(child, indent + 1, objects, ref index); } } diff --git a/src/Orchard.Tests.Modules/SimpleScripting/SimpleScriptingTests.cs b/src/Orchard.Tests.Modules/SimpleScripting/SimpleScriptingTests.cs new file mode 100644 index 000000000..c82eac293 --- /dev/null +++ b/src/Orchard.Tests.Modules/SimpleScripting/SimpleScriptingTests.cs @@ -0,0 +1,16 @@ +using System.Linq; +using NUnit.Framework; +using Orchard.Tests.Stubs; +using Orchard.Widgets.Services; +using Orchard.Widgets.SimpleScripting; + +namespace Orchard.Tests.Modules.SimpleScripting { + [TestFixture] + public class SimpleScriptingTests { + [Test] + public void EngineUnderstandsPrimitiveValues() { + //var engine = new ScriptingEngine(Enumerable.Empty(), new StubCacheManager()); + //Assert.That(engine.Matches("true"), Is.True); + } + } +} diff --git a/src/Orchard.Tests.Modules/SimpleScripting/TokenizerTests.cs b/src/Orchard.Tests.Modules/SimpleScripting/TokenizerTests.cs new file mode 100644 index 000000000..0a8d127b1 --- /dev/null +++ b/src/Orchard.Tests.Modules/SimpleScripting/TokenizerTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using Orchard.Widgets.SimpleScripting; + +namespace Orchard.Tests.Modules.SimpleScriptingTests { + [TestFixture] + public class TokenizerTests { + + [Test] + public void LexerShouldProcessSingleQuotedStringLiteral() { + TestStringLiteral(@"'toto'", @"toto", TerminalKind.SingleQuotedStringLiteral); + TestStringLiteral(@"'to\'to'", @"to'to", TerminalKind.SingleQuotedStringLiteral); + TestStringLiteral(@"'to\\to'", @"to\to", TerminalKind.SingleQuotedStringLiteral); + TestStringLiteral(@"'to\ato'", @"to\ato", TerminalKind.SingleQuotedStringLiteral); + } + + [Test] + public void LexerShouldProcessStringLiteral() { + TestStringLiteral(@"""toto""", @"toto", TerminalKind.StringLiteral); + TestStringLiteral(@"""to\'to""", @"to'to", TerminalKind.StringLiteral); + TestStringLiteral(@"""to\\to""", @"to\to", TerminalKind.StringLiteral); + TestStringLiteral(@"""to\ato""", @"toato", TerminalKind.StringLiteral); + } + + private void TestStringLiteral(string value, string expected, TerminalKind expectedTerminalKind) { + var lexer = new Tokenizer(value); + var token1 = lexer.NextToken(); + Assert.That(token1.Kind, Is.EqualTo(expectedTerminalKind)); + Assert.That(token1.Value, Is.EqualTo(expected)); + + var token2 = lexer.NextToken(); + Assert.That(token2.Kind, Is.EqualTo(TerminalKind.Eof)); + } + + [Test] + public void LexerShouldProcessReservedWords() { + TestReservedWord("true", true, TerminalKind.True); + TestReservedWord("false", false, TerminalKind.False); + TestReservedWord("not", null, TerminalKind.Not); + TestReservedWord("and", null, TerminalKind.And); + TestReservedWord("or", null, TerminalKind.Or); + } + + private void TestReservedWord(string expression, object value, TerminalKind expectedTerminalKind) { + var lexer = new Tokenizer(expression); + var token1 = lexer.NextToken(); + Assert.That(token1.Kind, Is.EqualTo(expectedTerminalKind)); + Assert.That(token1.Value, Is.EqualTo(value)); + + var token2 = lexer.NextToken(); + Assert.That(token2.Kind, Is.EqualTo(TerminalKind.Eof)); + } + + [Test] + public void LexerShouldProcesSequenceOfTokens() { + CheckTokenSequence("true false", TerminalKind.True, TerminalKind.False); + CheckTokenSequence("true toto false", TerminalKind.True, TerminalKind.Identifier, TerminalKind.False); + } + + [Test] + public void LexerShouldProcesSequenceOfTokens2() { + CheckTokenSequence("1+2*3", TerminalKind.Integer, TerminalKind.Plus, TerminalKind.Integer, TerminalKind.Mul, TerminalKind.Integer); + } + + + private void CheckTokenSequence(string expression, params TerminalKind[] terminalKinds) { + var lexer = new Tokenizer(expression); + foreach (var kind in terminalKinds) { + var token = lexer.NextToken(); + Assert.That(token.Kind, Is.EqualTo(kind)); + } + + var token2 = lexer.NextToken(); + Assert.That(token2.Kind, Is.EqualTo(TerminalKind.Eof)); + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj b/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj index a0812cc8a..a377ed006 100644 --- a/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj +++ b/src/Orchard.Web/Modules/Orchard.Widgets/Orchard.Widgets.csproj @@ -75,12 +75,19 @@ - - - - + + + + + + + + + - + + + diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/AbstractSyntaxTree.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/AbstractSyntaxTree.cs new file mode 100644 index 000000000..db56fa4cc --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/AbstractSyntaxTree.cs @@ -0,0 +1,5 @@ +namespace Orchard.Widgets.SimpleScripting { + public class AbstractSyntaxTree { + public AstNode Root { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/AstNode.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/AstNode.cs new file mode 100644 index 000000000..5fc73fbc3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/AstNode.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Orchard.Widgets.SimpleScripting { + public class AstNode { + public virtual IEnumerable Children { + get { + return Enumerable.Empty(); + } + } + + public override string ToString() { + var sb = new StringBuilder(); + sb.Append(this.GetType().Name); + var ewt = (this as IAstNodeWithToken); + if (ewt != null) { + sb.Append(" - "); + sb.Append(ewt.Terminal); + } + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/BinaryAstNode.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/BinaryAstNode.cs new file mode 100644 index 000000000..51d359e8a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/BinaryAstNode.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Orchard.Widgets.SimpleScripting { + public class BinaryAstNode : AstNode, IAstNodeWithToken { + private readonly AstNode _left; + private readonly Terminal _terminal; + private readonly AstNode _right; + + public BinaryAstNode(AstNode left, Terminal terminal, AstNode right) { + _left = left; + _terminal = terminal; + _right = right; + } + + public Terminal Terminal { + get { return _terminal; } + } + + public Terminal Operator { + get { return _terminal; } + } + + public override IEnumerable Children { + get { + yield return _left; + yield return _right; + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ConstantAstNode.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ConstantAstNode.cs new file mode 100644 index 000000000..b074de4a3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ConstantAstNode.cs @@ -0,0 +1,15 @@ +namespace Orchard.Widgets.SimpleScripting { + public class ConstantAstNode : AstNode, IAstNodeWithToken { + private readonly Terminal _terminal; + + public ConstantAstNode(Terminal terminal) { + _terminal = terminal; + } + + public Terminal Terminal { + get { return _terminal; } + } + + public object Value { get { return _terminal.Value; } } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ErrorAstNode.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ErrorAstNode.cs new file mode 100644 index 000000000..e24bcfc64 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ErrorAstNode.cs @@ -0,0 +1,25 @@ +using System; + +namespace Orchard.Widgets.SimpleScripting { + public class ErrorAstNode : AstNode, IAstNodeWithToken { + private readonly Terminal _terminal; + private readonly string _message; + + public ErrorAstNode(Terminal terminal, string message) { + _terminal = terminal; + _message = message; + } + + public Terminal Terminal { + get { return _terminal; } + } + + public string Message { + get { return _message; } + } + + public override string ToString() { + return String.Format("{0} - {1}", GetType().Name, Message); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/IAstNodeWithToken.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/IAstNodeWithToken.cs new file mode 100644 index 000000000..55a91b5e9 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/IAstNodeWithToken.cs @@ -0,0 +1,5 @@ +namespace Orchard.Widgets.SimpleScripting { + public interface IAstNodeWithToken { + Terminal Terminal { get; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Lexer.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Lexer.cs new file mode 100644 index 000000000..f0a31e255 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Lexer.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Orchard.Widgets.SimpleScripting { + public class Lexer { + private readonly Tokenizer _tokenizer; + private readonly List _tokens= new List(); + private int _tokenIndex; + + public Lexer(Tokenizer tokenizer) { + _tokenizer = tokenizer; + } + + public Terminal Token() { + if (_tokenIndex == _tokens.Count) { + _tokens.Add(_tokenizer.NextToken()); + } + return _tokens[_tokenIndex]; + } + + public void NextToken() { + _tokenIndex++; + } + + public Marker Mark() { + return new Marker(_tokens.Count); + } + + public void Mark(Marker marker) { + _tokenIndex = marker.TokenIndex; + } + + public struct Marker { + private readonly int _tokenIndex; + + public Marker(int tokenIndex) { + _tokenIndex = tokenIndex; + } + + public int TokenIndex { + get { return _tokenIndex; } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Parser.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Parser.cs new file mode 100644 index 000000000..8ab359a54 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Parser.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; + +namespace Orchard.Widgets.SimpleScripting { + public class Parser { + private readonly string _expression; + private readonly Lexer _lexer; + private readonly List _errors = new List(); + + public Parser(string expression) { + _expression = expression; + _lexer = new Lexer(new Tokenizer(_expression)); + } + + public AbstractSyntaxTree Parse() { + var node = ParseExpression(); + return new AbstractSyntaxTree { Root = node }; + } + + private AstNode ParseExpression() { + return ParseKeywordLogicalExpression(); + } + + private AstNode ParseKeywordLogicalExpression() { + return ParseKeywordOrExpression(); + } + + private AstNode ParseKeywordOrExpression() { + var expr = ParseKeywordAndExpression(); + + var token = IsMatch(TerminalKind.Or); + if (token != null) + { + var right = ParseKeywordOrExpression(); + + expr = new BinaryAstNode(expr, token, right); + } + + return expr; + } + + private AstNode ParseKeywordAndExpression() { + var expr = ParseKeywordNotExpression(); + + var token = IsMatch(TerminalKind.And); + if (token != null) { + var right = ParseKeywordAndExpression(); + + expr = new BinaryAstNode(expr, token, right); + } + + return expr; + } + + private AstNode ParseKeywordNotExpression() { + var token = IsMatch(TerminalKind.Not); + if (token != null) { + var expr = ParseKeywordNotExpression(); + + return new UnaryAstNode(expr, token); + } + + return ParseRelationalExpression(); + } + + private AstNode ParseRelationalExpression() { + var expr = ParseAdditiveExpression(); + //TODO + //var Terminal = IsMatch(TokenKind.Not); + //if (Terminal != null) { + return expr; + } + + private AstNode ParseAdditiveExpression() { + var expr = ParseMultiplicativeExpression(); + + var token = IsMatch(TerminalKind.Plus, TerminalKind.Minus); + if (token != null) { + var right = ParseAdditiveExpression(); + + expr = new BinaryAstNode(expr, token, right); + } + + return expr; + } + + private AstNode ParseMultiplicativeExpression() { + var expr = ParseUnaryExpression(); + + var token = IsMatch(TerminalKind.Mul, TerminalKind.Div); + if (token != null) { + var right = ParseMultiplicativeExpression(); + + expr = new BinaryAstNode(expr, token, right); + } + + return expr; + } + + private AstNode ParseUnaryExpression() { + //TODO + return ParsePrimaryExpression(); + } + + private AstNode ParsePrimaryExpression() { + var token = _lexer.Token(); + switch(_lexer.Token().Kind) { + case TerminalKind.True: + case TerminalKind.False: + case TerminalKind.SingleQuotedStringLiteral: + case TerminalKind.StringLiteral: + case TerminalKind.Integer: + return ProduceConstant(token); + case TerminalKind.OpenParen: + return ParseParenthesizedExpression(); + default: + return ProduceError(token); + } + } + + private AstNode ProduceConstant(Terminal terminal) { + _lexer.NextToken(); + return new ConstantAstNode(terminal); + } + + private AstNode ProduceError(Terminal terminal) { + _lexer.NextToken(); + return new ErrorAstNode(terminal, + string.Format("Unexptected Terminal in primary expression ({0})", terminal)); + } + + private AstNode ParseParenthesizedExpression() { + Match(TerminalKind.OpenParen); + var expr = ParseExpression(); + Match(TerminalKind.CloseParen); + return expr; + } + + private void Match(TerminalKind kind) { + var token = _lexer.Token(); + if (token.Kind == kind) { + _lexer.NextToken(); + return; + } + AddError(token, string.Format("Expected Terminal {0}", kind)); + } + + private Terminal IsMatch(TerminalKind kind) { + var token = _lexer.Token(); + if (token.Kind == kind) { + _lexer.NextToken(); + return token; + } + return null; + } + + private Terminal IsMatch(TerminalKind kind, TerminalKind kind2) { + var token = _lexer.Token(); + if (token.Kind == kind || token.Kind == kind2) { + _lexer.NextToken(); + return token; + } + return null; + } + + private void AddError(Terminal terminal, string message) { + _errors.Add(message); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ScriptingManager.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ScriptingManager.cs index 8bf16b9b4..94406fa01 100644 --- a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ScriptingManager.cs +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/ScriptingManager.cs @@ -28,11 +28,11 @@ namespace Orchard.Widgets.SimpleScripting { return (bool)Convert.ChangeType(result, typeof (bool)); } - private ExpressionTree ParseExpression(string expression) { - return new ExpressionParser(expression).Parse(); + private AbstractSyntaxTree ParseExpression(string expression) { + return new Parser(expression).Parse(); } - private object EvaluateExpression(ExpressionTree.Expression root) { + private object EvaluateExpression(AstNode root) { throw new NotImplementedException(); } diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Terminal.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Terminal.cs new file mode 100644 index 000000000..2801f35f3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Terminal.cs @@ -0,0 +1,13 @@ +using System; + +namespace Orchard.Widgets.SimpleScripting { + public class Terminal { + public TerminalKind Kind { get; set; } + public int Position { get; set; } + public object Value { get; set; } + + public override string ToString() { + return String.Format("{0} ({1}) at position {2}", Kind, Value ?? "", Position); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/TerminalKind.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/TerminalKind.cs new file mode 100644 index 000000000..c2ad6e3a7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/TerminalKind.cs @@ -0,0 +1,21 @@ +namespace Orchard.Widgets.SimpleScripting { + public enum TerminalKind { + Eof, + OpenParen, + CloseParen, + StringLiteral, + SingleQuotedStringLiteral, + Integer, + Plus, + Minus, + Mul, + Div, + True, + False, + Identifier, + And, + Or, + Not, + Invalid + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Tokenizer.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Tokenizer.cs new file mode 100644 index 000000000..dc5d77d0c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/Tokenizer.cs @@ -0,0 +1,215 @@ +using System; +using System.Text; + +namespace Orchard.Widgets.SimpleScripting { + public class Tokenizer { + private readonly string _expression; + private readonly StringBuilder _stringBuilder; + private int _index; + private int _startTokenIndex; + + public Tokenizer(string expression) { + _expression = expression; + _stringBuilder = new StringBuilder(); + } + + public Terminal NextToken() { + if (Eof()) + return CreateToken(TerminalKind.Eof); + + LexAgain: + _startTokenIndex = _index; + char ch = Character(); + switch (ch) { + case '(': + NextCharacter(); + return CreateToken(TerminalKind.OpenParen); + case ')': + NextCharacter(); + return CreateToken(TerminalKind.CloseParen); + case '+': + NextCharacter(); + return CreateToken(TerminalKind.Plus); + case '-': + NextCharacter(); + return CreateToken(TerminalKind.Minus); + case '*': + NextCharacter(); + return CreateToken(TerminalKind.Mul); + case '/': + NextCharacter(); + return CreateToken(TerminalKind.Div); + case '"': + return LexStringLiteral(); + case '\'': + return LexSingleQuotedStringLiteral(); + } + + if (IsDigitCharacter(ch)) { + return LexInteger(); + } + else if (IsIdentifierCharacter(ch)) { + return LexIdentifierOrKeyword(); + } + else if (IsWhitespaceCharacter(ch)) { + NextCharacter(); + goto LexAgain; + } + + return CreateToken(TerminalKind.Invalid, "Unrecognized character"); + } + + private Terminal LexIdentifierOrKeyword() { + _stringBuilder.Clear(); + + _stringBuilder.Append(Character()); + while (true) { + NextCharacter(); + + if (!Eof() && (IsIdentifierCharacter(Character()) || IsDigitCharacter(Character()))) { + _stringBuilder.Append(Character()); + } + else { + return CreateIdentiferOrKeyword(_stringBuilder.ToString()); + } + } + } + + private Terminal LexInteger() { + _stringBuilder.Clear(); + + _stringBuilder.Append(Character()); + while (true) { + NextCharacter(); + + if (!Eof() && IsDigitCharacter(Character())) { + _stringBuilder.Append(Character()); + } + else { + return CreateToken(TerminalKind.Integer, Int32.Parse(_stringBuilder.ToString())); + } + } + } + + private Terminal CreateIdentiferOrKeyword(string identifier) { + switch (identifier) { + case "true": + return CreateToken(TerminalKind.True, true); + case "false": + return CreateToken(TerminalKind.False, false); + case "or": + return CreateToken(TerminalKind.Or, null); + case "and": + return CreateToken(TerminalKind.And, null); + case "not": + return CreateToken(TerminalKind.Not, null); + default: + return CreateToken(TerminalKind.Identifier, identifier); + } + } + + private bool IsWhitespaceCharacter(char character) { + return char.IsWhiteSpace(character); + } + + private bool IsIdentifierCharacter(char ch) { + return + (ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch == '_'); + } + + private bool IsDigitCharacter(char ch) { + return ch >= '0' && ch <= '9'; + } + + private Terminal LexSingleQuotedStringLiteral() { + _stringBuilder.Clear(); + + while (true) { + NextCharacter(); + + if (Eof()) + return CreateToken(TerminalKind.Invalid, "Unterminated string literal"); + + // Termination + if (Character() == '\'') { + NextCharacter(); + return CreateToken(TerminalKind.SingleQuotedStringLiteral, _stringBuilder.ToString()); + } + // backslash notation + else if (Character() == '\\') { + NextCharacter(); + + if (Eof()) + return CreateToken(TerminalKind.Invalid, "Unterminated string literal"); + + if (Character() == '\\') { + _stringBuilder.Append('\\'); + } + else if (Character() == '\'') { + _stringBuilder.Append('\''); + } + else { + _stringBuilder.Append('\\'); + _stringBuilder.Append(Character()); + } + } + // Regular character in string + else { + _stringBuilder.Append(Character()); + } + } + } + + private Terminal LexStringLiteral() { + _stringBuilder.Clear(); + + while (true) { + NextCharacter(); + + if (Eof()) + return CreateToken(TerminalKind.Invalid, "Unterminated string literal"); + + // Termination + if (Character() == '"') { + NextCharacter(); + return CreateToken(TerminalKind.StringLiteral, _stringBuilder.ToString()); + } + // backslash notation + else if (Character() == '\\') { + NextCharacter(); + + if (Eof()) + return CreateToken(TerminalKind.Invalid, "Unterminated string literal"); + + _stringBuilder.Append(Character()); + } + // Regular character in string + else { + _stringBuilder.Append(Character()); + } + } + } + + private void NextCharacter() { + _index++; + } + + private char Character() { + return _expression[_index]; + } + + private Terminal CreateToken(TerminalKind kind, object value = null) { + return new Terminal { + Kind = kind, + Position = _startTokenIndex, + Value = value + }; + } + + private bool Eof() { + return (_index >= _expression.Length); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/UnaryAstNode.cs b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/UnaryAstNode.cs new file mode 100644 index 000000000..7c0c87eb4 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Widgets/SimpleScripting/UnaryAstNode.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Orchard.Widgets.SimpleScripting { + public class UnaryAstNode : AstNode, IAstNodeWithToken { + private readonly AstNode _expr; + private readonly Terminal _terminal; + + public UnaryAstNode(AstNode expr, Terminal terminal) { + _expr = expr; + _terminal = terminal; + } + + public Terminal Terminal { + get { return _terminal; } + } + + + public Terminal Operator { + get { return _terminal; } + } + + public override IEnumerable Children { + get { + yield return _expr; + } + } + } +} \ No newline at end of file