using System.Collections.Generic; using System.Linq; using Ink.Parsed; namespace Ink { public partial class InkParser { protected Parsed.Object LogicLine() { Whitespace (); if (ParseString ("~") == null) { return null; } Whitespace (); // Some example lines we need to be able to distinguish between: // ~ temp x = 5 -- var decl + assign // ~ temp x -- var decl // ~ x = 5 -- var assign // ~ x -- expr (not var decl or assign) // ~ f() -- expr // We don't treat variable decl/assign as an expression since we don't want an assignment // to have a return value, or to be used in compound expressions. ParseRule afterTilda = () => OneOf (ReturnStatement, TempDeclarationOrAssignment, Expression); var result = Expect(afterTilda, "expression after '~'", recoveryRule: SkipToNextLine) as Parsed.Object; // Prevent further errors, already reported expected expression and have skipped to next line. if (result == null) return new ContentList(); // Parse all expressions, but tell the writer off if they did something useless like: // ~ 5 + 4 // And even: // ~ false && myFunction() // ...since it's bad practice, and won't do what they expect if // they're expecting C's lazy evaluation. if (result is Expression && !(result is FunctionCall || result is IncDecExpression) ) { // TODO: Remove this specific error message when it has expired in usefulness var varRef = result as VariableReference; if (varRef && varRef.name == "include") { Error ("'~ include' is no longer the correct syntax - please use 'INCLUDE your_filename.ink', without the tilda, and in block capitals."); } else { Error ("Logic following a '~' can't be that type of expression. It can only be something like:\n\t~ return\n\t~ var x = blah\n\t~ x++\n\t~ myFunction()"); } } // Line is pure function call? e.g. // ~ f() // Add extra pop to make sure we tidy up after ourselves. // We no longer need anything on the evaluation stack. var funCall = result as FunctionCall; if (funCall) funCall.shouldPopReturnedValue = true; // If the expression contains a function call, then it could produce a text side effect, // in which case it needs a newline on the end. e.g. // ~ printMyName() // ~ x = 1 + returnAValueAndAlsoPrintStuff() // If no text gets printed, then the extra newline will have to be culled later. // Multiple newlines on the output will be removed, so there will be no "leak" for // long running calculations. It's disappointingly messy though :-/ if (result.Find<FunctionCall>() != null ) { result = new ContentList (result, new Parsed.Text ("\n")); } Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine); return result as Parsed.Object; } protected Parsed.Object VariableDeclaration() { Whitespace (); var id = Parse (Identifier); if (id != "VAR") return null; Whitespace (); var varName = Expect (IdentifierWithMetadata, "variable name") as Identifier; Whitespace (); Expect (String ("="), "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)"); Whitespace (); var definition = Expect (Expression, "initial value for "); var expr = definition as Parsed.Expression; if (expr) { if (!(expr is Number || expr is StringExpression || expr is DivertTarget || expr is VariableReference || expr is List)) { Error ("initial value for a variable must be a number, constant, list or divert target"); } if (Parse (ListElementDefinitionSeparator) != null) Error ("Unexpected ','. If you're trying to declare a new list, use the LIST keyword, not VAR"); // Ensure string expressions are simple else if (expr is StringExpression) { var strExpr = expr as StringExpression; if (!strExpr.isSingleString) Error ("Constant strings cannot contain any logic."); } var result = new VariableAssignment (varName, expr); result.isGlobalDeclaration = true; return result; } return null; } protected Parsed.VariableAssignment ListDeclaration () { Whitespace (); var id = Parse (Identifier); if (id != "LIST") return null; Whitespace (); var varName = Expect (IdentifierWithMetadata, "list name") as Identifier; Whitespace (); Expect (String ("="), "the '=' for an assignment of the list definition"); Whitespace (); var definition = Expect (ListDefinition, "list item names") as ListDefinition; if (definition) { definition.identifier = varName; return new VariableAssignment (varName, definition); } return null; } protected Parsed.ListDefinition ListDefinition () { AnyWhitespace (); var allElements = SeparatedList (ListElementDefinition, ListElementDefinitionSeparator); if (allElements == null) return null; return new ListDefinition (allElements); } protected string ListElementDefinitionSeparator () { AnyWhitespace (); if (ParseString (",") == null) return null; AnyWhitespace (); return ","; } protected Parsed.ListElementDefinition ListElementDefinition () { var inInitialList = ParseString ("(") != null; var needsToCloseParen = inInitialList; Whitespace (); var name = Parse (IdentifierWithMetadata); if (name == null) return null; Whitespace (); if (inInitialList) { if (ParseString (")") != null) { needsToCloseParen = false; Whitespace (); } } int? elementValue = null; if (ParseString ("=") != null) { Whitespace (); var elementValueNum = Expect (ExpressionInt, "value to be assigned to list item") as Number; if (elementValueNum != null) { elementValue = (int) elementValueNum.value; } if (needsToCloseParen) { Whitespace (); if (ParseString (")") != null) needsToCloseParen = false; } } if (needsToCloseParen) Error("Expected closing ')'"); return new ListElementDefinition (name, inInitialList, elementValue); } protected Parsed.Object ConstDeclaration() { Whitespace (); var id = Parse (Identifier); if (id != "CONST") return null; Whitespace (); var varName = Expect (IdentifierWithMetadata, "constant name") as Identifier; Whitespace (); Expect (String ("="), "the '=' for an assignment of a value, e.g. '= 5' (initial values are mandatory)"); Whitespace (); var expr = Expect (Expression, "initial value for ") as Parsed.Expression; if (!(expr is Number || expr is DivertTarget || expr is StringExpression)) { Error ("initial value for a constant must be a number or divert target"); } // Ensure string expressions are simple else if (expr is StringExpression) { var strExpr = expr as StringExpression; if (!strExpr.isSingleString) Error ("Constant strings cannot contain any logic."); } var result = new ConstantDeclaration (varName, expr); return result; } protected Parsed.Object InlineLogicOrGlueOrStartTag() { return (Parsed.Object) OneOf (InlineLogic, Glue, StartTag); } protected Parsed.Glue Glue() { // Don't want to parse whitespace, since it might be important // surrounding the glue. var glueStr = ParseString("<>"); if (glueStr != null) { return new Parsed.Glue (new Runtime.Glue ()); } else { return null; } } protected Parsed.Object InlineLogic() { if ( ParseString ("{") == null) { return null; } var wasParsingString = parsingStringExpression; var wasTagActive = tagActive; Whitespace (); var logic = (Parsed.Object) Expect(InnerLogic, "some kind of logic, conditional or sequence within braces: { ... }"); if (logic == null) { parsingStringExpression = wasParsingString; return null; } DisallowIncrement (logic); ContentList contentList = logic as ContentList; if (!contentList) { contentList = new ContentList (logic); } Whitespace (); Expect (String("}"), "closing brace '}' for inline logic"); // Allow nested strings and logic parsingStringExpression = wasParsingString; // Difference between: // // 1) A thing # {image}.jpg // 2) A {red #red|blue #blue} sequence. // // When logic ends in (1) we still want tag to continue. // When logic ends in (2) we want to auto-end the tag. // Side note: we simply disallow tags within strings. if( !wasTagActive ) EndTagIfNecessary(contentList); return contentList; } protected Parsed.Object InnerLogic() { Whitespace (); // Explicitly try the combinations of inner logic // that could potentially have conflicts first. // Explicit sequence annotation? SequenceType? explicitSeqType = (SequenceType?) ParseObject(SequenceTypeAnnotation); if (explicitSeqType != null) { var contentLists = (List<ContentList>) Expect(InnerSequenceObjects, "sequence elements (for cycle/stoping etc)"); if (contentLists == null) return null; return new Sequence (contentLists, (SequenceType) explicitSeqType); } // Conditional with expression? var initialQueryExpression = Parse(ConditionExpression); if (initialQueryExpression) { var conditional = (Conditional) Expect(() => InnerConditionalContent (initialQueryExpression), "conditional content following query"); return conditional; } // Now try to evaluate each of the "full" rules in turn ParseRule[] rules = { // Conditional still necessary, since you can have a multi-line conditional // without an initial query expression: // { // - true: this is true // - false: this is false // } InnerConditionalContent, InnerSequence, InnerExpression, }; bool wasTagActiveAtStartOfScope = tagActive; // Adapted from "OneOf" structuring rule except that in // order for the rule to succeed, it has to maximally // cover the entire string within the { }. Used to // differentiate between: // {myVar} -- Expression (try first) // {my content is jolly} -- sequence with single element foreach (ParseRule rule in rules) { int ruleId = BeginRule (); Parsed.Object result = ParseObject(rule) as Parsed.Object; if (result) { // Not yet at end? if (Peek (Spaced (String ("}"))) == null) FailRule (ruleId); // Full parse of content within braces else { return (Parsed.Object) SucceedRule (ruleId, result); } } else { FailRule (ruleId); } } return null; } protected Parsed.Object InnerExpression() { var expr = Parse(Expression); if (expr) { expr.outputWhenComplete = true; } return expr; } protected Identifier IdentifierWithMetadata() { var id = Identifier(); if( id == null ) return null; // InkParser.RuleDidSucceed will add DebugMetadata return new Identifier { name = id, debugMetadata = null }; } // Note: we allow identifiers that start with a number, // but not if they *only* comprise numbers protected string Identifier() { // Parse remaining characters (if any) var name = ParseCharactersFromCharSet (identifierCharSet); if (name == null) return null; // Reject if it's just a number bool isNumberCharsOnly = true; foreach (var c in name) { if ( !(c >= '0' && c <= '9') ) { isNumberCharsOnly = false; break; } } if (isNumberCharsOnly) { return null; } return name; } CharacterSet identifierCharSet { get { if (_identifierCharSet == null) { (_identifierCharSet = new CharacterSet ()) .AddRange ('A', 'Z') .AddRange ('a', 'z') .AddRange ('0', '9') .Add ('_'); // Enable non-ASCII characters for story identifiers. ExtendIdentifierCharacterRanges (_identifierCharSet); } return _identifierCharSet; } } private CharacterSet _identifierCharSet; } }