Committed everything

This commit is contained in:
2021-06-30 21:39:19 +10:00
commit fcfa8e7213
525 changed files with 49440 additions and 0 deletions

View File

@@ -0,0 +1,93 @@

namespace Ink
{
/// <summary>
/// Pre-pass before main ink parser runs. It actually performs two main tasks:
/// - comment elimination to simplify the parse rules in the main parser
/// - Conversion of Windows line endings (\r\n) to the simpler Unix style (\n), so
/// we don't have to worry about them later.
/// </summary>
public class CommentEliminator : StringParser
{
public CommentEliminator (string input) : base(input)
{
}
public string Process()
{
// Make both comments and non-comments optional to handle trivial empty file case (or *only* comments)
var stringList = Interleave<string>(Optional (CommentsAndNewlines), Optional(MainInk));
if (stringList != null) {
return string.Join("", stringList.ToArray());
} else {
return null;
}
}
string MainInk()
{
return ParseUntil (CommentsAndNewlines, _commentOrNewlineStartCharacter, null);
}
string CommentsAndNewlines()
{
var newlines = Interleave<string> (Optional (ParseNewline), Optional (ParseSingleComment));
if (newlines != null) {
return string.Join ("", newlines.ToArray());
} else {
return null;
}
}
// Valid comments always return either an empty string or pure newlines,
// which we want to keep so that line numbers stay the same
string ParseSingleComment()
{
return (string) OneOf (EndOfLineComment, BlockComment);
}
string EndOfLineComment()
{
if (ParseString ("//") == null) {
return null;
}
ParseUntilCharactersFromCharSet (_newlineCharacters);
return "";
}
string BlockComment()
{
if (ParseString ("/*") == null) {
return null;
}
int startLineIndex = lineIndex;
var commentResult = ParseUntil (String("*/"), _commentBlockEndCharacter, null);
if (!endOfInput) {
ParseString ("*/");
}
// Count the number of lines that were inside the block, and replicate them as newlines
// so that the line indexing still works from the original source
if (commentResult != null) {
return new string ('\n', lineIndex - startLineIndex);
}
// No comment at all
else {
return null;
}
}
CharacterSet _commentOrNewlineStartCharacter = new CharacterSet ("/\r\n");
CharacterSet _commentBlockEndCharacter = new CharacterSet("*");
CharacterSet _newlineCharacters = new CharacterSet ("\n\r");
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 59b3b9369a2a7481aab00ef369b7c4a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace Ink
{
public partial class InkParser : StringParser
{
public InkParser(string str, string filenameForMetadata = null, Ink.ErrorHandler externalErrorHandler = null, IFileHandler fileHandler = null)
: this(str, filenameForMetadata, externalErrorHandler, null, fileHandler)
{ }
InkParser(string str, string inkFilename = null, Ink.ErrorHandler externalErrorHandler = null, InkParser rootParser = null, IFileHandler fileHandler = null) : base(str) {
_filename = inkFilename;
RegisterExpressionOperators ();
GenerateStatementLevelRules ();
// Built in handler for all standard parse errors and warnings
this.errorHandler = OnStringParserError;
// The above parse errors are then formatted as strings and passed
// to the Ink.ErrorHandler, or it throws an exception
_externalErrorHandler = externalErrorHandler;
_fileHandler = fileHandler ?? new DefaultFileHandler();
if (rootParser == null) {
_rootParser = this;
_openFilenames = new HashSet<string> ();
if (inkFilename != null) {
var fullRootInkPath = _fileHandler.ResolveInkFilename (inkFilename);
_openFilenames.Add (fullRootInkPath);
}
} else {
_rootParser = rootParser;
}
}
// Main entry point
public Parsed.Story Parse()
{
List<Parsed.Object> topLevelContent = StatementsAtLevel (StatementLevel.Top);
// Note we used to return null if there were any errors, but this would mean
// that include files would return completely empty rather than attempting to
// continue with errors. Returning an empty include files meant that anything
// that *did* compile successfully would otherwise be ignored, generating way
// more errors than necessary.
return new Parsed.Story (topLevelContent, isInclude:_rootParser != this);
}
protected List<T> SeparatedList<T> (SpecificParseRule<T> mainRule, ParseRule separatorRule) where T : class
{
T firstElement = Parse (mainRule);
if (firstElement == null) return null;
var allElements = new List<T> ();
allElements.Add (firstElement);
do {
int nextElementRuleId = BeginRule ();
var sep = separatorRule ();
if (sep == null) {
FailRule (nextElementRuleId);
break;
}
var nextElement = Parse (mainRule);
if (nextElement == null) {
FailRule (nextElementRuleId);
break;
}
SucceedRule (nextElementRuleId);
allElements.Add (nextElement);
} while (true);
return allElements;
}
protected override string PreProcessInputString(string str)
{
var inputWithCommentsRemoved = (new CommentEliminator (str)).Process();
return inputWithCommentsRemoved;
}
protected Runtime.DebugMetadata CreateDebugMetadata(StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd)
{
var md = new Runtime.DebugMetadata ();
md.startLineNumber = stateAtStart.lineIndex + 1;
md.endLineNumber = stateAtEnd.lineIndex + 1;
md.startCharacterNumber = stateAtStart.characterInLineIndex + 1;
md.endCharacterNumber = stateAtEnd.characterInLineIndex + 1;
md.fileName = _filename;
return md;
}
protected override void RuleDidSucceed(object result, StringParserState.Element stateAtStart, StringParserState.Element stateAtEnd)
{
// Apply DebugMetadata based on the state at the start of the rule
// (i.e. use line number as it was at the start of the rule)
var parsedObj = result as Parsed.Object;
if ( parsedObj) {
parsedObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
return;
}
// A list of objects that doesn't already have metadata?
var parsedListObjs = result as List<Parsed.Object>;
if (parsedListObjs != null) {
foreach (var parsedListObj in parsedListObjs) {
if (!parsedListObj.hasOwnDebugMetadata) {
parsedListObj.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
}
}
}
var id = result as Parsed.Identifier;
if (id != null) {
id.debugMetadata = CreateDebugMetadata(stateAtStart, stateAtEnd);
}
}
protected bool parsingStringExpression
{
get {
return GetFlag ((uint)CustomFlags.ParsingString);
}
set {
SetFlag ((uint)CustomFlags.ParsingString, value);
}
}
protected enum CustomFlags {
ParsingString = 0x1
}
void OnStringParserError(string message, int index, int lineIndex, bool isWarning)
{
var warningType = isWarning ? "WARNING:" : "ERROR:";
string fullMessage;
if (_filename != null) {
fullMessage = string.Format(warningType+" '{0}' line {1}: {2}", _filename, (lineIndex+1), message);
} else {
fullMessage = string.Format(warningType+" line {0}: {1}", (lineIndex+1), message);
}
if (_externalErrorHandler != null) {
_externalErrorHandler (fullMessage, isWarning ? ErrorType.Warning : ErrorType.Error);
} else {
throw new System.Exception (fullMessage);
}
}
IFileHandler _fileHandler;
Ink.ErrorHandler _externalErrorHandler;
string _filename;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97d6f86dbb9994db6bd0572b69f53f25
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected AuthorWarning AuthorWarning()
{
Whitespace ();
var identifier = Parse (IdentifierWithMetadata);
if (identifier == null || identifier.name != "TODO")
return null;
Whitespace ();
ParseString (":");
Whitespace ();
var message = ParseUntilCharactersFromString ("\n\r");
return new AuthorWarning (message);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2f15af905c94ac9bdfb197799eec41
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
using Ink.Parsed;
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
public static readonly CharacterRange LatinBasic =
CharacterRange.Define ('\u0041', '\u007A', excludes: new CharacterSet().AddRange('\u005B', '\u0060'));
public static readonly CharacterRange LatinExtendedA = CharacterRange.Define('\u0100', '\u017F'); // no excludes here
public static readonly CharacterRange LatinExtendedB = CharacterRange.Define('\u0180', '\u024F'); // no excludes here
public static readonly CharacterRange Greek =
CharacterRange.Define('\u0370', '\u03FF', excludes: new CharacterSet().AddRange('\u0378','\u0385').AddCharacters("\u0374\u0375\u0378\u0387\u038B\u038D\u03A2"));
public static readonly CharacterRange Cyrillic =
CharacterRange.Define('\u0400', '\u04FF', excludes: new CharacterSet().AddRange('\u0482', '\u0489'));
public static readonly CharacterRange Armenian =
CharacterRange.Define('\u0530', '\u058F', excludes: new CharacterSet().AddCharacters("\u0530").AddRange('\u0557', '\u0560').AddRange('\u0588', '\u058E'));
public static readonly CharacterRange Hebrew =
CharacterRange.Define('\u0590', '\u05FF', excludes: new CharacterSet());
public static readonly CharacterRange Arabic =
CharacterRange.Define('\u0600', '\u06FF', excludes: new CharacterSet());
public static readonly CharacterRange Korean =
CharacterRange.Define('\uAC00', '\uD7AF', excludes: new CharacterSet());
public static readonly CharacterRange Latin1Supplement =
CharacterRange.Define('\u0080', '\u00FF', excludes: new CharacterSet());
private void ExtendIdentifierCharacterRanges(CharacterSet identifierCharSet)
{
var characterRanges = ListAllCharacterRanges();
foreach (var charRange in characterRanges)
{
identifierCharSet.AddCharacters(charRange.ToCharacterSet());
}
}
/// <summary>
/// Gets an array of <see cref="CharacterRange" /> representing all of the currently supported
/// non-ASCII character ranges that can be used in identifier names.
/// </summary>
/// <returns>
/// An array of <see cref="CharacterRange" /> representing all of the currently supported
/// non-ASCII character ranges that can be used in identifier names.
/// </returns>
public static CharacterRange[] ListAllCharacterRanges() {
return new CharacterRange[] {
LatinBasic,
LatinExtendedA,
LatinExtendedB,
Arabic,
Armenian,
Cyrillic,
Greek,
Hebrew,
Korean,
Latin1Supplement,
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2383f2e1b977347c2b6fb596e9af6d97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
using Ink.Parsed;
using System.Diagnostics;
namespace Ink
{
public partial class InkParser
{
protected Choice Choice()
{
bool onceOnlyChoice = true;
var bullets = Interleave <string>(OptionalExclude(Whitespace), String("*") );
if (bullets == null) {
bullets = Interleave <string>(OptionalExclude(Whitespace), String("+") );
if (bullets == null) {
return null;
}
onceOnlyChoice = false;
}
// Optional name for the choice
Identifier optionalName = Parse(BracketedName);
Whitespace ();
// Optional condition for whether the choice should be shown to the player
Expression conditionExpr = Parse(ChoiceCondition);
Whitespace ();
// Ordinarily we avoid parser state variables like these, since
// nesting would require us to store them in a stack. But since you should
// never be able to nest choices within choice content, it's fine here.
Debug.Assert(_parsingChoice == false, "Already parsing a choice - shouldn't have nested choices");
_parsingChoice = true;
ContentList startContent = null;
var startTextAndLogic = Parse (MixedTextAndLogic);
if (startTextAndLogic != null)
startContent = new ContentList (startTextAndLogic);
ContentList optionOnlyContent = null;
ContentList innerContent = null;
// Check for a the weave style format:
// * "Hello[."]," he said.
bool hasWeaveStyleInlineBrackets = ParseString("[") != null;
if (hasWeaveStyleInlineBrackets) {
var optionOnlyTextAndLogic = Parse (MixedTextAndLogic);
if (optionOnlyTextAndLogic != null)
optionOnlyContent = new ContentList (optionOnlyTextAndLogic);
Expect (String("]"), "closing ']' for weave-style option");
var innerTextAndLogic = Parse (MixedTextAndLogic);
if( innerTextAndLogic != null )
innerContent = new ContentList (innerTextAndLogic);
}
Whitespace ();
// Finally, now we know we're at the end of the main choice body, parse
// any diverts separately.
var diverts = Parse(MultiDivert);
_parsingChoice = false;
Whitespace ();
// Completely empty choice without even an empty divert?
bool emptyContent = !startContent && !innerContent && !optionOnlyContent;
if (emptyContent && diverts == null)
Warning ("Choice is completely empty. Interpretting as a default fallback choice. Add a divert arrow to remove this warning: * ->");
// * [] some text
else if (!startContent && hasWeaveStyleInlineBrackets && !optionOnlyContent)
Warning ("Blank choice - if you intended a default fallback choice, use the `* ->` syntax");
if (!innerContent) innerContent = new ContentList ();
var tags = Parse (Tags);
if (tags != null) {
innerContent.AddContent(tags);
}
// Normal diverts on the end of a choice - simply add to the normal content
if (diverts != null) {
foreach (var divObj in diverts) {
// may be TunnelOnwards
var div = divObj as Divert;
// Empty divert serves no purpose other than to say
// "this choice is intentionally left blank"
// (as an invisible default choice)
if (div && div.isEmpty) continue;
innerContent.AddContent (divObj);
}
}
// Terminate main content with a newline since this is the end of the line
// Note that this will be redundant if the diverts above definitely take
// the flow away permanently.
innerContent.AddContent (new Text ("\n"));
var choice = new Choice (startContent, optionOnlyContent, innerContent);
choice.identifier = optionalName;
choice.indentationDepth = bullets.Count;
choice.hasWeaveStyleInlineBrackets = hasWeaveStyleInlineBrackets;
choice.condition = conditionExpr;
choice.onceOnly = onceOnlyChoice;
choice.isInvisibleDefault = emptyContent;
return choice;
}
protected Expression ChoiceCondition()
{
var conditions = Interleave<Expression> (ChoiceSingleCondition, ChoiceConditionsSpace);
if (conditions == null)
return null;
else if (conditions.Count == 1)
return conditions [0];
else {
return new MultipleConditionExpression (conditions);
}
}
protected object ChoiceConditionsSpace()
{
// Both optional
// Newline includes initial end of line whitespace
Newline ();
Whitespace ();
return ParseSuccess;
}
protected Expression ChoiceSingleCondition()
{
if (ParseString ("{") == null)
return null;
var condExpr = Expect(Expression, "choice condition inside { }") as Expression;
DisallowIncrement (condExpr);
Expect (String ("}"), "closing '}' for choice condition");
return condExpr;
}
protected Gather Gather()
{
object gatherDashCountObj = Parse(GatherDashes);
if (gatherDashCountObj == null) {
return null;
}
int gatherDashCount = (int)gatherDashCountObj;
// Optional name for the gather
Identifier optionalName = Parse(BracketedName);
var gather = new Gather (optionalName, gatherDashCount);
// Optional newline before gather's content begins
Newline ();
return gather;
}
protected object GatherDashes()
{
Whitespace ();
int gatherDashCount = 0;
while (ParseDashNotArrow () != null) {
gatherDashCount++;
Whitespace ();
}
if (gatherDashCount == 0)
return null;
return gatherDashCount;
}
protected object ParseDashNotArrow()
{
var ruleId = BeginRule ();
if (ParseString ("->") == null && ParseSingleCharacter () == '-') {
return SucceedRule (ruleId);
} else {
return FailRule (ruleId);
}
}
protected Identifier BracketedName()
{
if (ParseString ("(") == null)
return null;
Whitespace ();
Identifier name = Parse(IdentifierWithMetadata);
if (name == null)
return null;
Whitespace ();
Expect (String (")"), "closing ')' for bracketed name");
return name;
}
bool _parsingChoice;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ea21bfef8977a4907b49628d9b651ebf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,129 @@
namespace Ink
{
public partial class InkParser
{
// Valid returned objects:
// - "help"
// - int: for choice number
// - Parsed.Divert
// - Variable declaration/assignment
// - Epression
// - Lookup debug source for character offset
// - Lookup debug source for runtime path
public CommandLineInput CommandLineUserInput()
{
CommandLineInput result = new CommandLineInput ();
Whitespace ();
if (ParseString ("help") != null) {
result.isHelp = true;
return result;
}
if (ParseString ("exit") != null || ParseString ("quit") != null) {
result.isExit = true;
return result;
}
return (CommandLineInput) OneOf (
DebugSource,
DebugPathLookup,
UserChoiceNumber,
UserImmediateModeStatement
);
}
CommandLineInput DebugSource ()
{
Whitespace ();
if (ParseString ("DebugSource") == null)
return null;
Whitespace ();
var expectMsg = "character offset in parentheses, e.g. DebugSource(5)";
if (Expect (String ("("), expectMsg) == null)
return null;
Whitespace ();
int? characterOffset = ParseInt ();
if (characterOffset == null) {
Error (expectMsg);
return null;
}
Whitespace ();
Expect (String (")"), "closing parenthesis");
var inputStruct = new CommandLineInput ();
inputStruct.debugSource = characterOffset;
return inputStruct;
}
CommandLineInput DebugPathLookup ()
{
Whitespace ();
if (ParseString ("DebugPath") == null)
return null;
if (Whitespace () == null)
return null;
var pathStr = Expect (RuntimePath, "path") as string;
var inputStruct = new CommandLineInput ();
inputStruct.debugPathLookup = pathStr;
return inputStruct;
}
string RuntimePath ()
{
if (_runtimePathCharacterSet == null) {
_runtimePathCharacterSet = new CharacterSet (identifierCharSet);
_runtimePathCharacterSet.Add ('-'); // for c-0, g-0 etc
_runtimePathCharacterSet.Add ('.');
}
return ParseCharactersFromCharSet (_runtimePathCharacterSet);
}
CommandLineInput UserChoiceNumber()
{
Whitespace ();
int? number = ParseInt ();
if (number == null) {
return null;
}
Whitespace ();
if (Parse(EndOfLine) == null) {
return null;
}
var inputStruct = new CommandLineInput ();
inputStruct.choiceInput = number;
return inputStruct;
}
CommandLineInput UserImmediateModeStatement()
{
var statement = OneOf (SingleDivert, TempDeclarationOrAssignment, Expression);
var inputStruct = new CommandLineInput ();
inputStruct.userImmediateModeStatement = statement;
return inputStruct;
}
CharacterSet _runtimePathCharacterSet;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ca27cda0c9364bfbaf37f8db278563f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,288 @@
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected Conditional InnerConditionalContent()
{
var initialQueryExpression = Parse(ConditionExpression);
var conditional = Parse(() => InnerConditionalContent (initialQueryExpression));
if (conditional == null)
return null;
return conditional;
}
protected Conditional InnerConditionalContent(Expression initialQueryExpression)
{
List<ConditionalSingleBranch> alternatives;
bool canBeInline = initialQueryExpression != null;
bool isInline = Parse(Newline) == null;
if (isInline && !canBeInline) {
return null;
}
// Inline innards
if (isInline) {
alternatives = InlineConditionalBranches ();
}
// Multiline innards
else {
alternatives = MultilineConditionalBranches ();
if (alternatives == null) {
// Allow single piece of content within multi-line expression, e.g.:
// { true:
// Some content that isn't preceded by '-'
// }
if (initialQueryExpression) {
List<Parsed.Object> soleContent = StatementsAtLevel (StatementLevel.InnerBlock);
if (soleContent != null) {
var soleBranch = new ConditionalSingleBranch (soleContent);
alternatives = new List<ConditionalSingleBranch> ();
alternatives.Add (soleBranch);
// Also allow a final "- else:" clause
var elseBranch = Parse (SingleMultilineCondition);
if (elseBranch) {
if (!elseBranch.isElse) {
ErrorWithParsedObject ("Expected an '- else:' clause here rather than an extra condition", elseBranch);
elseBranch.isElse = true;
}
alternatives.Add (elseBranch);
}
}
}
// Still null?
if (alternatives == null) {
return null;
}
}
// Empty true branch - didn't get parsed, but should insert one for semantic correctness,
// and to make sure that any evaluation stack values get tidied up correctly.
else if (alternatives.Count == 1 && alternatives [0].isElse && initialQueryExpression) {
var emptyTrueBranch = new ConditionalSingleBranch (null);
emptyTrueBranch.isTrueBranch = true;
alternatives.Insert (0, emptyTrueBranch);
}
// Like a switch statement
// { initialQueryExpression:
// ... match the expression
// }
if (initialQueryExpression) {
bool earlierBranchesHaveOwnExpression = false;
for (int i = 0; i < alternatives.Count; ++i) {
var branch = alternatives [i];
bool isLast = (i == alternatives.Count - 1);
// Matching equality with initial query expression
// We set this flag even for the "else" clause so that
// it knows to tidy up the evaluation stack at the end
// Match query
if (branch.ownExpression) {
branch.matchingEquality = true;
earlierBranchesHaveOwnExpression = true;
}
// Else (final branch)
else if (earlierBranchesHaveOwnExpression && isLast) {
branch.matchingEquality = true;
branch.isElse = true;
}
// Binary condition:
// { trueOrFalse:
// - when true
// - when false
// }
else {
if (!isLast && alternatives.Count > 2) {
ErrorWithParsedObject ("Only final branch can be an 'else'. Did you miss a ':'?", branch);
} else {
if (i == 0)
branch.isTrueBranch = true;
else
branch.isElse = true;
}
}
}
}
// No initial query, so just a multi-line conditional. e.g.:
// {
// - x > 3: greater than three
// - x == 3: equal to three
// - x < 3: less than three
// }
else {
for (int i = 0; i < alternatives.Count; ++i) {
var alt = alternatives [i];
bool isLast = (i == alternatives.Count - 1);
if (alt.ownExpression == null) {
if (isLast) {
alt.isElse = true;
} else {
if (alt.isElse) {
// Do we ALSO have a valid "else" at the end? Let's report the error there.
var finalClause = alternatives [alternatives.Count - 1];
if (finalClause.isElse) {
ErrorWithParsedObject ("Multiple 'else' cases. Can have a maximum of one, at the end.", finalClause);
} else {
ErrorWithParsedObject ("'else' case in conditional should always be the final one", alt);
}
} else {
ErrorWithParsedObject ("Branch doesn't have condition. Are you missing a ':'? ", alt);
}
}
}
}
if (alternatives.Count == 1 && alternatives [0].ownExpression == null) {
ErrorWithParsedObject ("Condition block with no conditions", alternatives [0]);
}
}
}
// TODO: Come up with water-tight error conditions... it's quite a flexible system!
// e.g.
// - inline conditionals must have exactly 1 or 2 alternatives
// - multiline expression shouldn't have mixed existence of branch-conditions?
if (alternatives == null)
return null;
foreach (var branch in alternatives) {
branch.isInline = isInline;
}
var cond = new Conditional (initialQueryExpression, alternatives);
return cond;
}
protected List<ConditionalSingleBranch> InlineConditionalBranches()
{
var listOfLists = Interleave<List<Parsed.Object>> (MixedTextAndLogic, Exclude (String ("|")), flatten: false);
if (listOfLists == null || listOfLists.Count == 0) {
return null;
}
var result = new List<ConditionalSingleBranch> ();
if (listOfLists.Count > 2) {
Error ("Expected one or two alternatives separated by '|' in inline conditional");
} else {
var trueBranch = new ConditionalSingleBranch (listOfLists[0]);
trueBranch.isTrueBranch = true;
result.Add (trueBranch);
if (listOfLists.Count > 1) {
var elseBranch = new ConditionalSingleBranch (listOfLists[1]);
elseBranch.isElse = true;
result.Add (elseBranch);
}
}
return result;
}
protected List<ConditionalSingleBranch> MultilineConditionalBranches()
{
MultilineWhitespace ();
List<object> multipleConditions = OneOrMore (SingleMultilineCondition);
if (multipleConditions == null)
return null;
MultilineWhitespace ();
return multipleConditions.Cast<ConditionalSingleBranch>().ToList();
}
protected ConditionalSingleBranch SingleMultilineCondition()
{
Whitespace ();
// Make sure we're not accidentally parsing a divert
if (ParseString ("->") != null)
return null;
if (ParseString ("-") == null)
return null;
Whitespace ();
Expression expr = null;
bool isElse = Parse(ElseExpression) != null;
if( !isElse )
expr = Parse(ConditionExpression);
List<Parsed.Object> content = StatementsAtLevel (StatementLevel.InnerBlock);
if (expr == null && content == null) {
Error ("expected content for the conditional branch following '-'");
// Recover
content = new List<Ink.Parsed.Object> ();
content.Add (new Text (""));
}
// Allow additional multiline whitespace, if the statements were empty (valid)
// then their surrounding multiline whitespacce needs to be handled manually.
// e.g.
// { x:
// - 1: // intentionally left blank, but newline needs to be parsed
// - 2: etc
// }
MultilineWhitespace ();
var branch = new ConditionalSingleBranch (content);
branch.ownExpression = expr;
branch.isElse = isElse;
return branch;
}
protected Expression ConditionExpression()
{
var expr = Parse(Expression);
if (expr == null)
return null;
DisallowIncrement (expr);
Whitespace ();
if (ParseString (":") == null)
return null;
return expr;
}
protected object ElseExpression()
{
if (ParseString ("else") == null)
return null;
Whitespace ();
if (ParseString (":") == null)
return null;
return ParseSuccess;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 334d80c537ee2473ea6a7cbd20e09f14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,217 @@
using Ink.Parsed;
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
void TrimEndWhitespace(List<Parsed.Object> mixedTextAndLogicResults, bool terminateWithSpace)
{
// Trim whitespace from end
if (mixedTextAndLogicResults.Count > 0) {
var lastObjIdx = mixedTextAndLogicResults.Count - 1;
var lastObj = mixedTextAndLogicResults[lastObjIdx];
if (lastObj is Text) {
var text = (Text)lastObj;
text.text = text.text.TrimEnd (' ', '\t');
if (terminateWithSpace)
text.text += " ";
// No content left at all? trim the whole object
else if( text.text.Length == 0 ) {
mixedTextAndLogicResults.RemoveAt(lastObjIdx);
// Recurse in case there's more whitespace
TrimEndWhitespace(mixedTextAndLogicResults, terminateWithSpace:false);
}
}
}
}
protected List<Parsed.Object> LineOfMixedTextAndLogic()
{
// Consume any whitespace at the start of the line
// (Except for escaped whitespace)
Parse (Whitespace);
var result = Parse(MixedTextAndLogic);
// Terminating tag
bool onlyTags = false;
var tags = Parse (Tags);
if (tags != null) {
if (result == null) {
result = tags.Cast<Parsed.Object> ().ToList ();
onlyTags = true;
} else {
foreach (var tag in tags) {
result.Add(tag);
}
}
}
if (result == null || result.Count == 0)
return null;
// Warn about accidentally writing "return" without "~"
var firstText = result[0] as Text;
if (firstText) {
if (firstText.text.StartsWith ("return")) {
Warning ("Do you need a '~' before 'return'? If not, perhaps use a glue: <> (since it's lowercase) or rewrite somehow?");
}
}
if (result.Count == 0)
return null;
var lastObj = result [result.Count - 1];
if (!(lastObj is Divert)) {
TrimEndWhitespace (result, terminateWithSpace:false);
}
// Add newline since it's the end of the line
// (so long as it's a line with only tags)
if( !onlyTags )
result.Add (new Text ("\n"));
Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
return result;
}
protected List<Parsed.Object> MixedTextAndLogic()
{
// Check for disallowed "~" within this context
var disallowedTilda = ParseObject(Spaced(String("~")));
if (disallowedTilda != null)
Error ("You shouldn't use a '~' here - tildas are for logic that's on its own line. To do inline logic, use { curly braces } instead");
// Either, or both interleaved
var results = Interleave<Parsed.Object>(Optional (ContentText), Optional (InlineLogicOrGlue));
// Terminating divert?
// (When parsing content for the text of a choice, diverts aren't allowed.
// The divert on the end of the body of a choice is handled specially.)
if (!_parsingChoice) {
var diverts = Parse (MultiDivert);
if (diverts != null) {
// May not have had any results at all if there's *only* a divert!
if (results == null)
results = new List<Parsed.Object> ();
TrimEndWhitespace (results, terminateWithSpace:true);
results.AddRange (diverts);
}
}
if (results == null)
return null;
return results;
}
protected Parsed.Text ContentText()
{
return ContentTextAllowingEcapeChar ();
}
protected Parsed.Text ContentTextAllowingEcapeChar()
{
StringBuilder sb = null;
do {
var str = Parse(ContentTextNoEscape);
bool gotEscapeChar = ParseString(@"\") != null;
if( gotEscapeChar || str != null ) {
if( sb == null ) {
sb = new StringBuilder();
}
if( str != null ) {
sb.Append(str);
}
if( gotEscapeChar ) {
char c = ParseSingleCharacter();
sb.Append(c);
}
} else {
break;
}
} while(true);
if (sb != null ) {
return new Parsed.Text (sb.ToString ());
} else {
return null;
}
}
// Content text is an unusual parse rule compared with most since it's
// less about saying "this is is the small selection of stuff that we parse"
// and more "we parse ANYTHING except this small selection of stuff".
protected string ContentTextNoEscape()
{
// Eat through text, pausing at the following characters, and
// attempt to parse the nonTextRule.
// "-": possible start of divert or start of gather
// "<": possible start of glue
if (_nonTextPauseCharacters == null) {
_nonTextPauseCharacters = new CharacterSet ("-<");
}
// If we hit any of these characters, we stop *immediately* without bothering to even check the nonTextRule
// "{" for start of logic
// "|" for mid logic branch
if (_nonTextEndCharacters == null) {
_nonTextEndCharacters = new CharacterSet ("{}|\n\r\\#");
_notTextEndCharactersChoice = new CharacterSet (_nonTextEndCharacters);
_notTextEndCharactersChoice.AddCharacters ("[]");
_notTextEndCharactersString = new CharacterSet (_nonTextEndCharacters);
_notTextEndCharactersString.AddCharacters ("\"");
}
// When the ParseUntil pauses, check these rules in case they evaluate successfully
ParseRule nonTextRule = () => OneOf (ParseDivertArrow, ParseThreadArrow, EndOfLine, Glue);
CharacterSet endChars = null;
if (parsingStringExpression) {
endChars = _notTextEndCharactersString;
}
else if (_parsingChoice) {
endChars = _notTextEndCharactersChoice;
}
else {
endChars = _nonTextEndCharacters;
}
string pureTextContent = ParseUntil (nonTextRule, _nonTextPauseCharacters, endChars);
if (pureTextContent != null ) {
return pureTextContent;
} else {
return null;
}
}
CharacterSet _nonTextPauseCharacters;
CharacterSet _nonTextEndCharacters;
CharacterSet _notTextEndCharactersChoice;
CharacterSet _notTextEndCharactersString;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8af10c051a6c942b1a43cb31ceb18114
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,206 @@
using System.Collections.Generic;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected List<Parsed.Object> MultiDivert()
{
Whitespace ();
List<Parsed.Object> diverts = null;
// Try single thread first
var threadDivert = Parse(StartThread);
if (threadDivert) {
diverts = new List<Object> ();
diverts.Add (threadDivert);
return diverts;
}
// Normal diverts and tunnels
var arrowsAndDiverts = Interleave<object> (
ParseDivertArrowOrTunnelOnwards,
DivertIdentifierWithArguments);
if (arrowsAndDiverts == null)
return null;
diverts = new List<Parsed.Object> ();
// Possible patterns:
// -> -- explicit gather
// ->-> -- tunnel onwards
// -> div -- normal divert
// ->-> div -- tunnel onwards, followed by override divert
// -> div -> -- normal tunnel
// -> div ->-> -- tunnel then tunnel continue
// -> div -> div -- tunnel then divert
// -> div -> div -> -- tunnel then tunnel
// -> div -> div ->->
// -> div -> div ->-> div (etc)
// Look at the arrows and diverts
for (int i = 0; i < arrowsAndDiverts.Count; ++i) {
bool isArrow = (i % 2) == 0;
// Arrow string
if (isArrow) {
// Tunnel onwards
if ((string)arrowsAndDiverts [i] == "->->") {
bool tunnelOnwardsPlacementValid = (i == 0 || i == arrowsAndDiverts.Count - 1 || i == arrowsAndDiverts.Count - 2);
if (!tunnelOnwardsPlacementValid)
Error ("Tunnel onwards '->->' must only come at the begining or the start of a divert");
var tunnelOnwards = new TunnelOnwards ();
if (i < arrowsAndDiverts.Count - 1) {
var tunnelOnwardDivert = arrowsAndDiverts [i+1] as Parsed.Divert;
tunnelOnwards.divertAfter = tunnelOnwardDivert;
}
diverts.Add (tunnelOnwards);
// Not allowed to do anything after a tunnel onwards.
// If we had anything left it would be caused in the above Error for
// the positioning of a ->->
break;
}
}
// Divert
else {
var divert = arrowsAndDiverts [i] as Divert;
// More to come? (further arrows) Must be tunnelling.
if (i < arrowsAndDiverts.Count - 1) {
divert.isTunnel = true;
}
diverts.Add (divert);
}
}
// Single -> (used for default choices)
if (diverts.Count == 0 && arrowsAndDiverts.Count == 1) {
var gatherDivert = new Divert ((Parsed.Object)null);
gatherDivert.isEmpty = true;
diverts.Add (gatherDivert);
if (!_parsingChoice)
Error ("Empty diverts (->) are only valid on choices");
}
return diverts;
}
protected Divert StartThread()
{
Whitespace ();
if (ParseThreadArrow() == null)
return null;
Whitespace ();
var divert = Expect(DivertIdentifierWithArguments, "target for new thread", () => new Divert(null)) as Divert;
divert.isThread = true;
return divert;
}
protected Divert DivertIdentifierWithArguments()
{
Whitespace ();
List<Identifier> targetComponents = Parse (DotSeparatedDivertPathComponents);
if (targetComponents == null)
return null;
Whitespace ();
var optionalArguments = Parse(ExpressionFunctionCallArguments);
Whitespace ();
var targetPath = new Path (targetComponents);
return new Divert (targetPath, optionalArguments);
}
protected Divert SingleDivert()
{
var diverts = Parse (MultiDivert);
if (diverts == null)
return null;
// Ideally we'd report errors if we get the
// wrong kind of divert, but unfortunately we
// have to hack around the fact that sequences use
// a very similar syntax.
// i.e. if you have a multi-divert at the start
// of a sequence, it initially tries to parse it
// as a divert target (part of an expression of
// a conditional) and gives errors. So instead
// we just have to blindly reject it as a single
// divert, and give a slightly less nice error
// when you DO use a multi divert as a divert taret.
if (diverts.Count != 1) {
return null;
}
var singleDivert = diverts [0];
if (singleDivert is TunnelOnwards) {
return null;
}
var divert = diverts [0] as Divert;
if (divert.isTunnel) {
return null;
}
return divert;
}
List<Identifier> DotSeparatedDivertPathComponents()
{
return Interleave<Identifier> (Spaced (IdentifierWithMetadata), Exclude (String (".")));
}
protected string ParseDivertArrowOrTunnelOnwards()
{
int numArrows = 0;
while (ParseString ("->") != null)
numArrows++;
if (numArrows == 0)
return null;
else if (numArrows == 1)
return "->";
else if (numArrows == 2)
return "->->";
else {
Error ("Unexpected number of arrows in divert. Should only have '->' or '->->'");
return "->->";
}
}
protected string ParseDivertArrow()
{
return ParseString ("->");
}
protected string ParseThreadArrow()
{
return ParseString ("<-");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7aec7c9fb87244b3a8c64753c8a2f9b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,512 @@
using System;
using Ink.Parsed;
using System.Collections.Generic;
namespace Ink
{
public partial class InkParser
{
protected class InfixOperator
{
public string type;
public int precedence;
public bool requireWhitespace;
public InfixOperator(string type, int precedence, bool requireWhitespace) {
this.type = type;
this.precedence = precedence;
this.requireWhitespace = requireWhitespace;
}
public override string ToString ()
{
return type;
}
}
protected Parsed.Object TempDeclarationOrAssignment()
{
Whitespace ();
bool isNewDeclaration = ParseTempKeyword();
Whitespace ();
Identifier varIdentifier = null;
if (isNewDeclaration) {
varIdentifier = (Identifier)Expect (IdentifierWithMetadata, "variable name");
} else {
varIdentifier = Parse(IdentifierWithMetadata);
}
if (varIdentifier == null) {
return null;
}
Whitespace();
// += -=
bool isIncrement = ParseString ("+") != null;
bool isDecrement = ParseString ("-") != null;
if (isIncrement && isDecrement) Error ("Unexpected sequence '+-'");
if (ParseString ("=") == null) {
// Definitely in an assignment expression?
if (isNewDeclaration) Error ("Expected '='");
return null;
}
Expression assignedExpression = (Expression)Expect (Expression, "value expression to be assigned");
if (isIncrement || isDecrement) {
var result = new IncDecExpression (varIdentifier, assignedExpression, isIncrement);
return result;
} else {
var result = new VariableAssignment (varIdentifier, assignedExpression);
result.isNewTemporaryDeclaration = isNewDeclaration;
return result;
}
}
protected void DisallowIncrement (Parsed.Object expr)
{
if (expr is Parsed.IncDecExpression)
Error ("Can't use increment/decrement here. It can only be used on a ~ line");
}
protected bool ParseTempKeyword()
{
var ruleId = BeginRule ();
if (Parse (Identifier) == "temp") {
SucceedRule (ruleId);
return true;
} else {
FailRule (ruleId);
return false;
}
}
protected Parsed.Return ReturnStatement()
{
Whitespace ();
var returnOrDone = Parse(Identifier);
if (returnOrDone != "return") {
return null;
}
Whitespace ();
var expr = Parse(Expression);
var returnObj = new Return (expr);
return returnObj;
}
protected Expression Expression() {
return Expression(minimumPrecedence:0);
}
// Pratt Parser
// aka "Top down operator precedence parser"
// http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
// Algorithm overview:
// The two types of precedence are handled in two different ways:
// ((((a . b) . c) . d) . e) #1
// (a . (b . (c . (d . e)))) #2
// Where #1 is automatically handled by successive loops within the main 'while' in this function,
// so long as continuing operators have lower (or equal) precedence (e.g. imagine some series of "*"s then "+" above.
// ...and #2 is handled by recursion of the right hand term in the binary expression parser.
// (see link for advice on how to extend for postfix and mixfix operators)
protected Expression Expression(int minimumPrecedence)
{
Whitespace ();
// First parse a unary expression e.g. "-a" or parethensised "(1 + 2)"
var expr = ExpressionUnary ();
if (expr == null) {
return null;
}
Whitespace ();
// Attempt to parse (possibly multiple) continuing infix expressions (e.g. 1 + 2 + 3)
while(true) {
var ruleId = BeginRule ();
// Operator
var infixOp = ParseInfixOperator ();
if (infixOp != null && infixOp.precedence > minimumPrecedence) {
// Expect right hand side of operator
var expectationMessage = string.Format("right side of '{0}' expression", infixOp.type);
var multiaryExpr = Expect (() => ExpressionInfixRight (left: expr, op: infixOp), expectationMessage);
if (multiaryExpr == null) {
// Fail for operator and right-hand side of multiary expression
FailRule (ruleId);
return null;
}
expr = SucceedRule(ruleId, multiaryExpr) as Parsed.Expression;
continue;
}
FailRule (ruleId);
break;
}
Whitespace ();
return expr;
}
protected Expression ExpressionUnary()
{
// Divert target is a special case - it can't have any other operators
// applied to it, and we also want to check for it first so that we don't
// confuse "->" for subtraction.
var divertTarget = Parse (ExpressionDivertTarget);
if (divertTarget != null) {
return divertTarget;
}
var prefixOp = (string) OneOf (String ("-"), String ("!"));
// Don't parse like the string rules above, in case its actually
// a variable that simply starts with "not", e.g. "notable".
// This rule uses the Identifier rule, which will scan as much text
// as possible before returning.
if (prefixOp == null) {
prefixOp = Parse(ExpressionNot);
}
Whitespace ();
// - Since we allow numbers at the start of variable names, variable names are checked before literals
// - Function calls before variable names in case we see parentheses
var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral) as Expression;
// Only recurse immediately if we have one of the (usually optional) unary ops
if (expr == null && prefixOp != null) {
expr = ExpressionUnary ();
}
if (expr == null)
return null;
if (prefixOp != null) {
expr = UnaryExpression.WithInner(expr, prefixOp);
}
Whitespace ();
var postfixOp = (string) OneOf (String ("++"), String ("--"));
if (postfixOp != null) {
bool isInc = postfixOp == "++";
if (!(expr is VariableReference)) {
Error ("can only increment and decrement variables, but saw '" + expr + "'");
// Drop down and succeed without the increment after reporting error
} else {
// TODO: Language Server - (Identifier combined into one vs. list of Identifiers)
var varRef = (VariableReference)expr;
expr = new IncDecExpression(varRef.identifier, isInc);
}
}
return expr;
}
protected string ExpressionNot()
{
var id = Identifier ();
if (id == "not") {
return id;
}
return null;
}
protected Expression ExpressionLiteral()
{
return (Expression) OneOf (ExpressionFloat, ExpressionInt, ExpressionBool, ExpressionString);
}
protected Expression ExpressionDivertTarget()
{
Whitespace ();
var divert = Parse(SingleDivert);
if (divert == null)
return null;
if (divert.isThread)
return null;
Whitespace ();
return new DivertTarget (divert);
}
protected Number ExpressionInt()
{
int? intOrNull = ParseInt ();
if (intOrNull == null) {
return null;
} else {
return new Number (intOrNull.Value);
}
}
protected Number ExpressionFloat()
{
float? floatOrNull = ParseFloat ();
if (floatOrNull == null) {
return null;
} else {
return new Number (floatOrNull.Value);
}
}
protected StringExpression ExpressionString()
{
var openQuote = ParseString ("\"");
if (openQuote == null)
return null;
// Set custom parser state flag so that within the text parser,
// it knows to treat the quote character (") as an end character
parsingStringExpression = true;
List<Parsed.Object> textAndLogic = Parse (MixedTextAndLogic);
Expect (String ("\""), "close quote for string expression");
parsingStringExpression = false;
if (textAndLogic == null) {
textAndLogic = new List<Ink.Parsed.Object> ();
textAndLogic.Add (new Parsed.Text (""));
}
else if (textAndLogic.Exists (c => c is Divert))
Error ("String expressions cannot contain diverts (->)");
return new StringExpression (textAndLogic);
}
protected Number ExpressionBool()
{
var id = Parse(Identifier);
if (id == "true") {
return new Number (true);
} else if (id == "false") {
return new Number (false);
}
return null;
}
protected Expression ExpressionFunctionCall()
{
var iden = Parse(IdentifierWithMetadata);
if (iden == null)
return null;
Whitespace ();
var arguments = Parse(ExpressionFunctionCallArguments);
if (arguments == null) {
return null;
}
return new FunctionCall(iden, arguments);
}
protected List<Expression> ExpressionFunctionCallArguments()
{
if (ParseString ("(") == null)
return null;
// "Exclude" requires the rule to succeed, but causes actual comma string to be excluded from the list of results
ParseRule commas = Exclude (String (","));
var arguments = Interleave<Expression>(Expression, commas);
if (arguments == null) {
arguments = new List<Expression> ();
}
Whitespace ();
Expect (String (")"), "closing ')' for function call");
return arguments;
}
protected Expression ExpressionVariableName()
{
List<Identifier> path = Interleave<Identifier> (IdentifierWithMetadata, Exclude (Spaced (String ("."))));
if (path == null || Story.IsReservedKeyword (path[0].name) )
return null;
return new VariableReference (path);
}
protected Expression ExpressionParen()
{
if (ParseString ("(") == null)
return null;
var innerExpr = Parse(Expression);
if (innerExpr == null)
return null;
Whitespace ();
Expect (String(")"), "closing parenthesis ')' for expression");
return innerExpr;
}
protected Expression ExpressionInfixRight(Parsed.Expression left, InfixOperator op)
{
Whitespace ();
var right = Parse(() => Expression (op.precedence));
if (right) {
// We assume that the character we use for the operator's type is the same
// as that used internally by e.g. Runtime.Expression.Add, Runtime.Expression.Multiply etc
var expr = new BinaryExpression (left, right, op.type);
return expr;
}
return null;
}
private InfixOperator ParseInfixOperator()
{
foreach (var op in _binaryOperators) {
int ruleId = BeginRule ();
if (ParseString (op.type) != null) {
if (op.requireWhitespace) {
if (Whitespace () == null) {
FailRule (ruleId);
continue;
}
}
return (InfixOperator) SucceedRule(ruleId, op);
}
FailRule (ruleId);
}
return null;
}
protected Parsed.List ExpressionList ()
{
Whitespace ();
if (ParseString ("(") == null)
return null;
Whitespace ();
// When list has:
// - 0 elements (null list) - this is okay, it's an empty list: "()"
// - 1 element - it could be confused for a single non-list related
// identifier expression in brackets, but this is a useless thing
// to do, so we reserve that syntax for a list with one item.
// - 2 or more elements - normal!
List<Identifier> memberNames = SeparatedList (ListMember, Spaced (String (",")));
Whitespace ();
// May have failed to parse the inner list - the parentheses may
// be for a normal expression
if (ParseString (")") == null)
return null;
return new List (memberNames);
}
protected Identifier ListMember ()
{
Whitespace ();
Identifier identifier = Parse (IdentifierWithMetadata);
if (identifier == null)
return null;
var dot = ParseString (".");
if (dot != null) {
Identifier identifier2 = Expect (IdentifierWithMetadata, "element name within the set " + identifier) as Identifier;
identifier.name = identifier.name + "." + identifier2?.name;
}
Whitespace ();
return identifier;
}
void RegisterExpressionOperators()
{
_maxBinaryOpLength = 0;
_binaryOperators = new List<InfixOperator> ();
// These will be tried in order, so we need "<=" before "<"
// for correctness
RegisterBinaryOperator ("&&", precedence:1);
RegisterBinaryOperator ("||", precedence:1);
RegisterBinaryOperator ("and", precedence:1, requireWhitespace: true);
RegisterBinaryOperator ("or", precedence:1, requireWhitespace: true);
RegisterBinaryOperator ("==", precedence:2);
RegisterBinaryOperator (">=", precedence:2);
RegisterBinaryOperator ("<=", precedence:2);
RegisterBinaryOperator ("<", precedence:2);
RegisterBinaryOperator (">", precedence:2);
RegisterBinaryOperator ("!=", precedence:2);
// (apples, oranges) + cabbages has (oranges, cabbages) == true
RegisterBinaryOperator ("?", precedence: 3);
RegisterBinaryOperator ("has", precedence: 3, requireWhitespace:true);
RegisterBinaryOperator ("!?", precedence: 3);
RegisterBinaryOperator ("hasnt", precedence: 3, requireWhitespace: true);
RegisterBinaryOperator ("^", precedence: 3);
RegisterBinaryOperator ("+", precedence:4);
RegisterBinaryOperator ("-", precedence:5);
RegisterBinaryOperator ("*", precedence:6);
RegisterBinaryOperator ("/", precedence:7);
RegisterBinaryOperator ("%", precedence:8);
RegisterBinaryOperator ("mod", precedence:8, requireWhitespace:true);
}
void RegisterBinaryOperator(string op, int precedence, bool requireWhitespace = false)
{
_binaryOperators.Add(new InfixOperator (op, precedence, requireWhitespace));
_maxBinaryOpLength = Math.Max (_maxBinaryOpLength, op.Length);
}
List<InfixOperator> _binaryOperators;
int _maxBinaryOpLength;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 85c9eb24bda894235981df7a3689da51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using Ink.Parsed;
using System.Collections.Generic;
using System.IO;
namespace Ink
{
public partial class InkParser
{
protected object IncludeStatement()
{
Whitespace ();
if (ParseString ("INCLUDE") == null)
return null;
Whitespace ();
var filename = (string) Expect(() => ParseUntilCharactersFromString ("\n\r"), "filename for include statement");
filename = filename.TrimEnd (' ', '\t');
// Working directory should already have been set up relative to the root ink file.
var fullFilename = _rootParser._fileHandler.ResolveInkFilename (filename);
if (FilenameIsAlreadyOpen (fullFilename)) {
Error ("Recursive INCLUDE detected: '" + fullFilename + "' is already open.");
ParseUntilCharactersFromString("\r\n");
return new IncludedFile(null);
} else {
AddOpenFilename (fullFilename);
}
Parsed.Story includedStory = null;
string includedString = null;
try {
includedString = _rootParser._fileHandler.LoadInkFileContents(fullFilename);
}
catch {
Error ("Failed to load: '"+filename+"'");
}
if (includedString != null ) {
InkParser parser = new InkParser(includedString, filename, _externalErrorHandler, _rootParser);
includedStory = parser.Parse();
}
RemoveOpenFilename (fullFilename);
// Return valid IncludedFile object even if there were errors when parsing.
// We don't want to attempt to re-parse the include line as something else,
// and we want to include the bits that *are* valid, so we don't generate
// more errors than necessary.
return new IncludedFile (includedStory);
}
bool FilenameIsAlreadyOpen(string fullFilename)
{
return _rootParser._openFilenames.Contains (fullFilename);
}
void AddOpenFilename(string fullFilename)
{
_rootParser._openFilenames.Add (fullFilename);
}
void RemoveOpenFilename(string fullFilename)
{
_rootParser._openFilenames.Remove (fullFilename);
}
InkParser _rootParser;
HashSet<string> _openFilenames;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f4b92fc2dbd664298b01416321678d61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,239 @@
using System.Collections.Generic;
using Ink.Parsed;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
protected class NameWithMetadata {
public string name;
public Runtime.DebugMetadata metadata;
}
protected class FlowDecl
{
public Identifier name;
public List<FlowBase.Argument> arguments;
public bool isFunction;
}
protected Knot KnotDefinition()
{
var knotDecl = Parse(KnotDeclaration);
if (knotDecl == null)
return null;
Expect(EndOfLine, "end of line after knot name definition", recoveryRule: SkipToNextLine);
ParseRule innerKnotStatements = () => StatementsAtLevel (StatementLevel.Knot);
var content = Expect (innerKnotStatements, "at least one line within the knot", recoveryRule: KnotStitchNoContentRecoveryRule) as List<Parsed.Object>;
return new Knot (knotDecl.name, content, knotDecl.arguments, knotDecl.isFunction);
}
protected FlowDecl KnotDeclaration()
{
Whitespace ();
if (KnotTitleEquals () == null)
return null;
Whitespace ();
Identifier identifier = Parse(IdentifierWithMetadata);
Identifier knotName;
bool isFunc = identifier?.name == "function";
if (isFunc) {
Expect (Whitespace, "whitespace after the 'function' keyword");
knotName = Parse(IdentifierWithMetadata);
} else {
knotName = identifier;
}
if (knotName == null) {
Error ("Expected the name of the " + (isFunc ? "function" : "knot"));
knotName = new Identifier { name = "" }; // prevent later null ref
}
Whitespace ();
List<FlowBase.Argument> parameterNames = Parse (BracketedKnotDeclArguments);
Whitespace ();
// Optional equals after name
Parse(KnotTitleEquals);
return new FlowDecl () { name = knotName, arguments = parameterNames, isFunction = isFunc };
}
protected string KnotTitleEquals()
{
// 2+ "=" starts a knot
var multiEquals = ParseCharactersFromString ("=");
if (multiEquals == null || multiEquals.Length <= 1) {
return null;
} else {
return multiEquals;
}
}
protected object StitchDefinition()
{
var decl = Parse(StitchDeclaration);
if (decl == null)
return null;
Expect(EndOfLine, "end of line after stitch name", recoveryRule: SkipToNextLine);
ParseRule innerStitchStatements = () => StatementsAtLevel (StatementLevel.Stitch);
var content = Expect(innerStitchStatements, "at least one line within the stitch", recoveryRule: KnotStitchNoContentRecoveryRule) as List<Parsed.Object>;
return new Stitch (decl.name, content, decl.arguments, decl.isFunction );
}
protected FlowDecl StitchDeclaration()
{
Whitespace ();
// Single "=" to define a stitch
if (ParseString ("=") == null)
return null;
// If there's more than one "=", that's actually a knot definition (or divert), so this rule should fail
if (ParseString ("=") != null)
return null;
Whitespace ();
// Stitches aren't allowed to be functions, but we parse it anyway and report the error later
bool isFunc = ParseString ("function") != null;
if ( isFunc ) {
Whitespace ();
}
Identifier stitchName = Parse(IdentifierWithMetadata);
if (stitchName == null)
return null;
Whitespace ();
List<FlowBase.Argument> flowArgs = Parse(BracketedKnotDeclArguments);
Whitespace ();
return new FlowDecl () { name = stitchName, arguments = flowArgs, isFunction = isFunc };
}
protected object KnotStitchNoContentRecoveryRule()
{
// Jump ahead to the next knot or the end of the file
ParseUntil (KnotDeclaration, new CharacterSet ("="), null);
var recoveredFlowContent = new List<Parsed.Object>();
recoveredFlowContent.Add( new Parsed.Text("<ERROR IN FLOW>" ) );
return recoveredFlowContent;
}
protected List<FlowBase.Argument> BracketedKnotDeclArguments()
{
if (ParseString ("(") == null)
return null;
var flowArguments = Interleave<FlowBase.Argument>(Spaced(FlowDeclArgument), Exclude (String(",")));
Expect (String (")"), "closing ')' for parameter list");
// If no parameters, create an empty list so that this method is type safe and
// doesn't attempt to return the ParseSuccess object
if (flowArguments == null) {
flowArguments = new List<FlowBase.Argument> ();
}
return flowArguments;
}
protected FlowBase.Argument FlowDeclArgument()
{
// Possible forms:
// name
// -> name (variable divert target argument
// ref name
// ref -> name (variable divert target by reference)
var firstIden = Parse(IdentifierWithMetadata);
Whitespace ();
var divertArrow = ParseDivertArrow ();
Whitespace ();
var secondIden = Parse(IdentifierWithMetadata);
if (firstIden == null && secondIden == null)
return null;
var flowArg = new FlowBase.Argument ();
if (divertArrow != null) {
flowArg.isDivertTarget = true;
}
// Passing by reference
if (firstIden != null && firstIden.name == "ref") {
if (secondIden == null) {
Error ("Expected an parameter name after 'ref'");
}
flowArg.identifier = secondIden;
flowArg.isByReference = true;
}
// Simple argument name
else {
if (flowArg.isDivertTarget) {
flowArg.identifier = secondIden;
} else {
flowArg.identifier = firstIden;
}
if (flowArg.identifier == null) {
Error ("Expected an parameter name");
}
flowArg.isByReference = false;
}
return flowArg;
}
protected ExternalDeclaration ExternalDeclaration()
{
Whitespace ();
Identifier external = Parse(IdentifierWithMetadata);
if (external == null || external.name != "EXTERNAL")
return null;
Whitespace ();
var funcIdentifier = Expect(IdentifierWithMetadata, "name of external function") as Identifier ?? new Identifier();
Whitespace ();
var parameterNames = Expect (BracketedKnotDeclArguments, "declaration of arguments for EXTERNAL, even if empty, i.e. 'EXTERNAL "+funcIdentifier+"()'") as List<FlowBase.Argument>;
if (parameterNames == null)
parameterNames = new List<FlowBase.Argument> ();
var argNames = parameterNames.Select (arg => arg.identifier?.name).ToList();
return new ExternalDeclaration (funcIdentifier, argNames);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 77e8119b05a284a889e4b2978c6b1617
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,421 @@
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 InlineLogicOrGlue()
{
return (Parsed.Object) OneOf (InlineLogic, Glue);
}
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;
}
Whitespace ();
var logic = (Parsed.Object) Expect(InnerLogic, "some kind of logic, conditional or sequence within braces: { ... }");
if (logic == null)
return null;
DisallowIncrement (logic);
ContentList contentList = logic as ContentList;
if (!contentList) {
contentList = new ContentList (logic);
}
Whitespace ();
Expect (String("}"), "closing brace '}' for inline logic");
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,
};
// 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;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aa9a6c9127e3f4eb8a46f532557ccd2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,231 @@
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected Sequence InnerSequence()
{
Whitespace ();
// Default sequence type
SequenceType seqType = SequenceType.Stopping;
// Optional explicit sequence type
SequenceType? parsedSeqType = (SequenceType?) Parse(SequenceTypeAnnotation);
if (parsedSeqType != null)
seqType = parsedSeqType.Value;
var contentLists = Parse(InnerSequenceObjects);
if (contentLists == null || contentLists.Count <= 1) {
return null;
}
return new Sequence (contentLists, seqType);
}
protected object SequenceTypeAnnotation()
{
var annotation = (SequenceType?) Parse(SequenceTypeSymbolAnnotation);
if(annotation == null)
annotation = (SequenceType?) Parse(SequenceTypeWordAnnotation);
if (annotation == null)
return null;
switch (annotation.Value)
{
case SequenceType.Once:
case SequenceType.Cycle:
case SequenceType.Stopping:
case SequenceType.Shuffle:
case (SequenceType.Shuffle | SequenceType.Stopping):
case (SequenceType.Shuffle | SequenceType.Once):
break;
default:
Error("Sequence type combination not supported: " + annotation.Value);
return SequenceType.Stopping;
}
return annotation;
}
protected object SequenceTypeSymbolAnnotation()
{
if(_sequenceTypeSymbols == null )
_sequenceTypeSymbols = new CharacterSet("!&~$ ");
var sequenceType = (SequenceType)0;
var sequenceAnnotations = ParseCharactersFromCharSet(_sequenceTypeSymbols);
if (sequenceAnnotations == null)
return null;
foreach(char symbolChar in sequenceAnnotations) {
switch(symbolChar) {
case '!': sequenceType |= SequenceType.Once; break;
case '&': sequenceType |= SequenceType.Cycle; break;
case '~': sequenceType |= SequenceType.Shuffle; break;
case '$': sequenceType |= SequenceType.Stopping; break;
}
}
if (sequenceType == (SequenceType)0)
return null;
return sequenceType;
}
CharacterSet _sequenceTypeSymbols = new CharacterSet("!&~$");
protected object SequenceTypeWordAnnotation()
{
var sequenceTypes = Interleave<SequenceType?>(SequenceTypeSingleWord, Exclude(Whitespace));
if (sequenceTypes == null || sequenceTypes.Count == 0)
return null;
if (ParseString (":") == null)
return null;
var combinedSequenceType = (SequenceType)0;
foreach(var seqType in sequenceTypes) {
combinedSequenceType |= seqType.Value;
}
return combinedSequenceType;
}
protected object SequenceTypeSingleWord()
{
SequenceType? seqType = null;
var word = Parse(IdentifierWithMetadata);
if (word != null)
{
switch (word.name)
{
case "once":
seqType = SequenceType.Once;
break;
case "cycle":
seqType = SequenceType.Cycle;
break;
case "shuffle":
seqType = SequenceType.Shuffle;
break;
case "stopping":
seqType = SequenceType.Stopping;
break;
}
}
if (seqType == null)
return null;
return seqType;
}
protected List<ContentList> InnerSequenceObjects()
{
var multiline = Parse(Newline) != null;
List<ContentList> result = null;
if (multiline) {
result = Parse(InnerMultilineSequenceObjects);
} else {
result = Parse(InnerInlineSequenceObjects);
}
return result;
}
protected List<ContentList> InnerInlineSequenceObjects()
{
var interleavedContentAndPipes = Interleave<object> (Optional (MixedTextAndLogic), String ("|"), flatten:false);
if (interleavedContentAndPipes == null)
return null;
var result = new List<ContentList> ();
// The content and pipes won't necessarily be perfectly interleaved in the sense that
// the content can be missing, but in that case it's intended that there's blank content.
bool justHadContent = false;
foreach (object contentOrPipe in interleavedContentAndPipes) {
// Pipe/separator
if (contentOrPipe as string == "|") {
// Expected content, saw pipe - need blank content now
if (!justHadContent) {
// Add blank content
result.Add (new ContentList ());
}
justHadContent = false;
}
// Real content
else {
var content = contentOrPipe as List<Parsed.Object>;
if (content == null) {
Error ("Expected content, but got " + contentOrPipe + " (this is an ink compiler bug!)");
} else {
result.Add (new ContentList (content));
}
justHadContent = true;
}
}
// Ended in a pipe? Need to insert final blank content
if (!justHadContent)
result.Add (new ContentList ());
return result;
}
protected List<ContentList> InnerMultilineSequenceObjects()
{
MultilineWhitespace ();
var contentLists = OneOrMore (SingleMultilineSequenceElement);
if (contentLists == null)
return null;
return contentLists.Cast<ContentList> ().ToList();
}
protected ContentList SingleMultilineSequenceElement()
{
Whitespace ();
// Make sure we're not accidentally parsing a divert
if (ParseString ("->") != null)
return null;
if (ParseString ("-") == null)
return null;
Whitespace ();
List<Parsed.Object> content = StatementsAtLevel (StatementLevel.InnerBlock);
if (content == null)
MultilineWhitespace ();
// Add newline at the start of each branch
else {
content.Insert (0, new Parsed.Text ("\n"));
}
return new ContentList (content);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0e33e10232e5c4587a8c460cf9d99960
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Ink.Parsed;
namespace Ink
{
public partial class InkParser
{
protected enum StatementLevel
{
InnerBlock,
Stitch,
Knot,
Top
}
protected List<Parsed.Object> StatementsAtLevel(StatementLevel level)
{
// Check for error: Should not be allowed gather dashes within an inner block
if (level == StatementLevel.InnerBlock) {
object badGatherDashCount = Parse(GatherDashes);
if (badGatherDashCount != null) {
Error ("You can't use a gather (the dashes) within the { curly braces } context. For multi-line sequences and conditions, you should only use one dash.");
}
}
return Interleave<Parsed.Object>(
Optional (MultilineWhitespace),
() => StatementAtLevel (level),
untilTerminator: () => StatementsBreakForLevel(level));
}
protected object StatementAtLevel(StatementLevel level)
{
ParseRule[] rulesAtLevel = _statementRulesAtLevel[(int)level];
var statement = OneOf (rulesAtLevel);
// For some statements, allow them to parse, but create errors, since
// writers may think they can use the statement, so it's useful to have
// the error message.
if (level == StatementLevel.Top) {
if( statement is Return )
Error ("should not have return statement outside of a knot");
}
return statement;
}
protected object StatementsBreakForLevel(StatementLevel level)
{
Whitespace ();
ParseRule[] breakRules = _statementBreakRulesAtLevel[(int)level];
var breakRuleResult = OneOf (breakRules);
if (breakRuleResult == null)
return null;
return breakRuleResult;
}
void GenerateStatementLevelRules()
{
var levels = Enum.GetValues (typeof(StatementLevel)).Cast<StatementLevel> ().ToList();
_statementRulesAtLevel = new ParseRule[levels.Count][];
_statementBreakRulesAtLevel = new ParseRule[levels.Count][];
foreach (var level in levels) {
List<ParseRule> rulesAtLevel = new List<ParseRule> ();
List<ParseRule> breakingRules = new List<ParseRule> ();
// Diverts can go anywhere
rulesAtLevel.Add(Line(MultiDivert));
// Knots can only be parsed at Top/Global scope
if (level >= StatementLevel.Top)
rulesAtLevel.Add (KnotDefinition);
rulesAtLevel.Add(Line(Choice));
rulesAtLevel.Add(Line(AuthorWarning));
// Gather lines would be confused with multi-line block separators, like
// within a multi-line if statement
if (level > StatementLevel.InnerBlock) {
rulesAtLevel.Add (Gather);
}
// Stitches (and gathers) can (currently) only go in Knots and top level
if (level >= StatementLevel.Knot) {
rulesAtLevel.Add (StitchDefinition);
}
// Global variable declarations can go anywhere
rulesAtLevel.Add(Line(ListDeclaration));
rulesAtLevel.Add(Line(VariableDeclaration));
rulesAtLevel.Add(Line(ConstDeclaration));
rulesAtLevel.Add(Line(ExternalDeclaration));
// Global include can go anywhere
rulesAtLevel.Add(Line(IncludeStatement));
// Normal logic / text can go anywhere
rulesAtLevel.Add(LogicLine);
rulesAtLevel.Add(LineOfMixedTextAndLogic);
// --------
// Breaking rules
// Break current knot with a new knot
if (level <= StatementLevel.Knot) {
breakingRules.Add (KnotDeclaration);
}
// Break current stitch with a new stitch
if (level <= StatementLevel.Stitch) {
breakingRules.Add (StitchDeclaration);
}
// Breaking an inner block (like a multi-line condition statement)
if (level <= StatementLevel.InnerBlock) {
breakingRules.Add (ParseDashNotArrow);
breakingRules.Add (String ("}"));
}
_statementRulesAtLevel [(int)level] = rulesAtLevel.ToArray ();
_statementBreakRulesAtLevel [(int)level] = breakingRules.ToArray ();
}
}
protected object SkipToNextLine()
{
ParseUntilCharactersFromString ("\n\r");
ParseNewline ();
return ParseSuccess;
}
// Modifier to turn a rule into one that expects a newline on the end.
// e.g. anywhere you can use "MixedTextAndLogic" as a rule, you can use
// "Line(MixedTextAndLogic)" to specify that it expects a newline afterwards.
protected ParseRule Line(ParseRule inlineRule)
{
return () => {
object result = ParseObject(inlineRule);
if (result == null) {
return null;
}
Expect(EndOfLine, "end of line", recoveryRule: SkipToNextLine);
return result;
};
}
ParseRule[][] _statementRulesAtLevel;
ParseRule[][] _statementBreakRulesAtLevel;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42fdd6da57c584bf58ff578d093b8529
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace Ink
{
public partial class InkParser
{
protected Parsed.Tag Tag ()
{
Whitespace ();
if (ParseString ("#") == null)
return null;
Whitespace ();
var sb = new StringBuilder ();
do {
// Read up to another #, end of input or newline
string tagText = ParseUntilCharactersFromCharSet (_endOfTagCharSet);
sb.Append (tagText);
// Escape character
if (ParseString ("\\") != null) {
char c = ParseSingleCharacter ();
if( c != (char)0 ) sb.Append(c);
continue;
}
break;
} while ( true );
var fullTagText = sb.ToString ().Trim();
return new Parsed.Tag (new Runtime.Tag (fullTagText));
}
protected List<Parsed.Tag> Tags ()
{
var tags = OneOrMore (Tag);
if (tags == null) return null;
return tags.Cast<Parsed.Tag>().ToList();
}
CharacterSet _endOfTagCharSet = new CharacterSet ("#\n\r\\");
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fed5929e88fa3480e8f02d3f686c0c2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,112 @@
using System.Collections.Generic;
namespace Ink
{
public partial class InkParser
{
// Handles both newline and endOfFile
protected object EndOfLine()
{
return OneOf(Newline, EndOfFile);
}
// Allow whitespace before the actual newline
protected object Newline()
{
Whitespace();
bool gotNewline = ParseNewline () != null;
// Optional \r, definite \n to support Windows (\r\n) and Mac/Unix (\n)
if( !gotNewline ) {
return null;
} else {
return ParseSuccess;
}
}
protected object EndOfFile()
{
Whitespace();
if (!endOfInput)
return null;
return ParseSuccess;
}
// General purpose space, returns N-count newlines (fails if no newlines)
protected object MultilineWhitespace()
{
List<object> newlines = OneOrMore(Newline);
if (newlines == null)
return null;
// Use content field of Token to say how many newlines there were
// (in most circumstances it's unimportant)
int numNewlines = newlines.Count;
if (numNewlines >= 1) {
return ParseSuccess;
} else {
return null;
}
}
protected object Whitespace()
{
if( ParseCharactersFromCharSet(_inlineWhitespaceChars) != null ) {
return ParseSuccess;
}
return null;
}
protected ParseRule Spaced(ParseRule rule)
{
return () => {
Whitespace ();
var result = ParseObject(rule);
if (result == null) {
return null;
}
Whitespace ();
return result;
};
}
protected object AnyWhitespace ()
{
bool anyWhitespace = false;
while (OneOf (Whitespace, MultilineWhitespace) != null) {
anyWhitespace = true;
}
return anyWhitespace ? ParseSuccess : null;
}
protected ParseRule MultiSpaced (ParseRule rule)
{
return () => {
AnyWhitespace ();
var result = ParseObject (rule);
if (result == null) {
return null;
}
AnyWhitespace ();
return result;
};
}
private CharacterSet _inlineWhitespaceChars = new CharacterSet(" \t");
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fbdaec262d8f64dd684a79c235405a10
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: