diff --git a/Assets/Ink.meta b/Assets/Ink.meta
new file mode 100644
index 000000000..7d8a1b767
--- /dev/null
+++ b/Assets/Ink.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4db0e7e1caca41440b5b14f966d1b38c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler.meta b/Assets/Ink/compiler.meta
new file mode 100644
index 000000000..b008b0564
--- /dev/null
+++ b/Assets/Ink/compiler.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a220efa8dba4e9d4c957cbc085eabed2
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/CharacterRange.cs b/Assets/Ink/compiler/CharacterRange.cs
new file mode 100644
index 000000000..40ae68930
--- /dev/null
+++ b/Assets/Ink/compiler/CharacterRange.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Ink
+{
+ ///
+ /// A class representing a character range. Allows for lazy-loading a corresponding character set.
+ ///
+ public sealed class CharacterRange
+ {
+ public static CharacterRange Define(char start, char end, IEnumerable excludes = null)
+ {
+ return new CharacterRange (start, end, excludes);
+ }
+
+ ///
+ /// Returns a character set instance corresponding to the character range
+ /// represented by the current instance.
+ ///
+ ///
+ /// The internal character set is created once and cached in memory.
+ ///
+ /// The char set.
+ public CharacterSet ToCharacterSet ()
+ {
+ if (_correspondingCharSet.Count == 0)
+ {
+ for (char c = _start; c <= _end; c++)
+ {
+ if (!_excludes.Contains (c))
+ {
+ _correspondingCharSet.Add (c);
+ }
+ }
+ }
+ return _correspondingCharSet;
+ }
+
+ public char start { get { return _start; } }
+ public char end { get { return _end; } }
+
+ CharacterRange (char start, char end, IEnumerable excludes)
+ {
+ _start = start;
+ _end = end;
+ _excludes = excludes == null ? new HashSet() : new HashSet (excludes);
+ }
+
+ char _start;
+ char _end;
+ ICollection _excludes;
+ CharacterSet _correspondingCharSet = new CharacterSet();
+ }
+}
diff --git a/Assets/Ink/compiler/CharacterRange.cs.meta b/Assets/Ink/compiler/CharacterRange.cs.meta
new file mode 100644
index 000000000..ef8b75e13
--- /dev/null
+++ b/Assets/Ink/compiler/CharacterRange.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 72ae8a113e216e342af5643382c14802
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/CharacterSet.cs b/Assets/Ink/compiler/CharacterSet.cs
new file mode 100644
index 000000000..7e0eef2f6
--- /dev/null
+++ b/Assets/Ink/compiler/CharacterSet.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace Ink
+{
+
+ public class CharacterSet : HashSet
+ {
+ public static CharacterSet FromRange(char start, char end)
+ {
+ return new CharacterSet ().AddRange (start, end);
+ }
+
+ public CharacterSet ()
+ {
+ }
+
+ public CharacterSet(string str)
+ {
+ AddCharacters (str);
+ }
+
+ public CharacterSet(CharacterSet charSetToCopy)
+ {
+ AddCharacters (charSetToCopy);
+ }
+
+ public CharacterSet AddRange(char start, char end)
+ {
+ for(char c=start; c<=end; ++c) {
+ Add (c);
+ }
+ return this;
+ }
+
+ public CharacterSet AddCharacters(IEnumerable chars)
+ {
+ foreach (char c in chars) {
+ Add (c);
+ }
+ return this;
+ }
+
+ public CharacterSet AddCharacters (string chars)
+ {
+ foreach (char c in chars) {
+ Add (c);
+ }
+ return this;
+ }
+
+ }
+}
+
diff --git a/Assets/Ink/compiler/CharacterSet.cs.meta b/Assets/Ink/compiler/CharacterSet.cs.meta
new file mode 100644
index 000000000..921012437
--- /dev/null
+++ b/Assets/Ink/compiler/CharacterSet.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 61e245e371a3dd848a15f35d6e9f43ed
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/CommandLineInput.cs b/Assets/Ink/compiler/CommandLineInput.cs
new file mode 100644
index 000000000..88f1dfe57
--- /dev/null
+++ b/Assets/Ink/compiler/CommandLineInput.cs
@@ -0,0 +1,12 @@
+namespace Ink
+{
+ public class CommandLineInput
+ {
+ public bool isHelp;
+ public bool isExit;
+ public int? choiceInput;
+ public int? debugSource;
+ public string debugPathLookup;
+ public object userImmediateModeStatement;
+ }
+}
\ No newline at end of file
diff --git a/Assets/Ink/compiler/CommandLineInput.cs.meta b/Assets/Ink/compiler/CommandLineInput.cs.meta
new file mode 100644
index 000000000..9a9412567
--- /dev/null
+++ b/Assets/Ink/compiler/CommandLineInput.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 9ea826b6e6cf3c04cacb8f0967c91658
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/Compiler.cs b/Assets/Ink/compiler/Compiler.cs
new file mode 100644
index 000000000..f3e00321d
--- /dev/null
+++ b/Assets/Ink/compiler/Compiler.cs
@@ -0,0 +1,209 @@
+using System;
+using System.Collections.Generic;
+using Ink;
+
+namespace Ink
+{
+ public class Compiler
+ {
+ public class Options
+ {
+ public string sourceFilename;
+ public List pluginNames;
+ public bool countAllVisits;
+ public Ink.ErrorHandler errorHandler;
+ public Ink.IFileHandler fileHandler;
+ }
+
+ public Parsed.Story parsedStory {
+ get {
+ return _parsedStory;
+ }
+ }
+
+ public Compiler (string inkSource, Options options = null)
+ {
+ _inputString = inkSource;
+ _options = options ?? new Options();
+ if( _options.pluginNames != null )
+ _pluginManager = new PluginManager (_options.pluginNames);
+ }
+
+ public Parsed.Story Parse()
+ {
+ _parser = new InkParser(_inputString, _options.sourceFilename, OnParseError, _options.fileHandler);
+ _parsedStory = _parser.Parse();
+ return _parsedStory;
+ }
+
+ public Runtime.Story Compile ()
+ {
+ Parse();
+
+ if( _pluginManager != null )
+ _pluginManager.PostParse(_parsedStory);
+
+ if (_parsedStory != null && !_hadParseError) {
+
+ _parsedStory.countAllVisits = _options.countAllVisits;
+
+ _runtimeStory = _parsedStory.ExportRuntime (_options.errorHandler);
+
+ if( _pluginManager != null )
+ _pluginManager.PostExport (_parsedStory, _runtimeStory);
+ } else {
+ _runtimeStory = null;
+ }
+
+ return _runtimeStory;
+ }
+
+ public class CommandLineInputResult {
+ public bool requestsExit;
+ public int choiceIdx = -1;
+ public string divertedPath;
+ public string output;
+ }
+ public CommandLineInputResult HandleInput (CommandLineInput inputResult)
+ {
+ var result = new CommandLineInputResult ();
+
+ // Request for debug source line number
+ if (inputResult.debugSource != null) {
+ var offset = (int)inputResult.debugSource;
+ var dm = DebugMetadataForContentAtOffset (offset);
+ if (dm != null)
+ result.output = "DebugSource: " + dm.ToString ();
+ else
+ result.output = "DebugSource: Unknown source";
+ }
+
+ // Request for runtime path lookup (to line number)
+ else if (inputResult.debugPathLookup != null) {
+ var pathStr = inputResult.debugPathLookup;
+ var contentResult = _runtimeStory.ContentAtPath (new Runtime.Path (pathStr));
+ var dm = contentResult.obj.debugMetadata;
+ if( dm != null )
+ result.output = "DebugSource: " + dm.ToString ();
+ else
+ result.output = "DebugSource: Unknown source";
+ }
+
+ // User entered some ink
+ else if (inputResult.userImmediateModeStatement != null) {
+ var parsedObj = inputResult.userImmediateModeStatement as Parsed.Object;
+ return ExecuteImmediateStatement(parsedObj);
+
+ } else {
+ return null;
+ }
+
+ return result;
+ }
+
+ CommandLineInputResult ExecuteImmediateStatement(Parsed.Object parsedObj) {
+ var result = new CommandLineInputResult ();
+
+ // Variable assignment: create in Parsed.Story as well as the Runtime.Story
+ // so that we don't get an error message during reference resolution
+ if (parsedObj is Parsed.VariableAssignment) {
+ var varAssign = (Parsed.VariableAssignment)parsedObj;
+ if (varAssign.isNewTemporaryDeclaration) {
+ _parsedStory.TryAddNewVariableDeclaration (varAssign);
+ }
+ }
+
+ parsedObj.parent = _parsedStory;
+ var runtimeObj = parsedObj.runtimeObject;
+
+ parsedObj.ResolveReferences (_parsedStory);
+
+ if (!_parsedStory.hadError) {
+
+ // Divert
+ if (parsedObj is Parsed.Divert) {
+ var parsedDivert = parsedObj as Parsed.Divert;
+ result.divertedPath = parsedDivert.runtimeDivert.targetPath.ToString();
+ }
+
+ // Expression or variable assignment
+ else if (parsedObj is Parsed.Expression || parsedObj is Parsed.VariableAssignment) {
+ var evalResult = _runtimeStory.EvaluateExpression ((Runtime.Container)runtimeObj);
+ if (evalResult != null) {
+ result.output = evalResult.ToString ();
+ }
+ }
+ } else {
+ _parsedStory.ResetError ();
+ }
+
+ return result;
+ }
+
+ public void RetrieveDebugSourceForLatestContent ()
+ {
+ foreach (var outputObj in _runtimeStory.state.outputStream) {
+ var textContent = outputObj as Runtime.StringValue;
+ if (textContent != null) {
+ var range = new DebugSourceRange ();
+ range.length = textContent.value.Length;
+ range.debugMetadata = textContent.debugMetadata;
+ range.text = textContent.value;
+ _debugSourceRanges.Add (range);
+ }
+ }
+ }
+
+ Runtime.DebugMetadata DebugMetadataForContentAtOffset (int offset)
+ {
+ int currOffset = 0;
+
+ Runtime.DebugMetadata lastValidMetadata = null;
+ foreach (var range in _debugSourceRanges) {
+ if (range.debugMetadata != null)
+ lastValidMetadata = range.debugMetadata;
+
+ if (offset >= currOffset && offset < currOffset + range.length)
+ return lastValidMetadata;
+
+ currOffset += range.length;
+ }
+
+ return null;
+ }
+
+ public struct DebugSourceRange
+ {
+ public int length;
+ public Runtime.DebugMetadata debugMetadata;
+ public string text;
+ }
+
+ // Need to wrap the error handler so that we know
+ // when there was a critical error between parse and codegen stages
+ void OnParseError (string message, ErrorType errorType)
+ {
+ if( errorType == ErrorType.Error )
+ _hadParseError = true;
+
+ if (_options.errorHandler != null)
+ _options.errorHandler (message, errorType);
+ else
+ throw new System.Exception(message);
+ }
+
+ string _inputString;
+ Options _options;
+
+
+ InkParser _parser;
+ Parsed.Story _parsedStory;
+ Runtime.Story _runtimeStory;
+
+ PluginManager _pluginManager;
+
+ bool _hadParseError;
+
+ List _debugSourceRanges = new List ();
+ }
+}
diff --git a/Assets/Ink/compiler/Compiler.cs.meta b/Assets/Ink/compiler/Compiler.cs.meta
new file mode 100644
index 000000000..8406769a6
--- /dev/null
+++ b/Assets/Ink/compiler/Compiler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 02566d14814a1194ea64f24ac13aabbb
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/FileHandler.cs b/Assets/Ink/compiler/FileHandler.cs
new file mode 100644
index 000000000..51a60bb47
--- /dev/null
+++ b/Assets/Ink/compiler/FileHandler.cs
@@ -0,0 +1,24 @@
+using System.IO;
+
+namespace Ink
+{
+ public interface IFileHandler
+ {
+ string ResolveInkFilename (string includeName);
+ string LoadInkFileContents (string fullFilename);
+ }
+
+ public class DefaultFileHandler : Ink.IFileHandler {
+ public string ResolveInkFilename (string includeName)
+ {
+ var workingDir = Directory.GetCurrentDirectory ();
+ var fullRootInkPath = Path.Combine (workingDir, includeName);
+ return fullRootInkPath;
+ }
+
+ public string LoadInkFileContents (string fullFilename)
+ {
+ return File.ReadAllText (fullFilename);
+ }
+ }
+}
diff --git a/Assets/Ink/compiler/FileHandler.cs.meta b/Assets/Ink/compiler/FileHandler.cs.meta
new file mode 100644
index 000000000..98d42bf29
--- /dev/null
+++ b/Assets/Ink/compiler/FileHandler.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a4bc77a489a8348498429161a93a337a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser.meta b/Assets/Ink/compiler/InkParser.meta
new file mode 100644
index 000000000..5e4a0549d
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2ac8c073d5990034aa0cc05037439d4f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/CommentEliminator.cs b/Assets/Ink/compiler/InkParser/CommentEliminator.cs
new file mode 100644
index 000000000..9d0cfee59
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/CommentEliminator.cs
@@ -0,0 +1,93 @@
+
+namespace Ink
+{
+ ///
+ /// 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.
+ ///
+ 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(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 (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");
+ }
+}
+
diff --git a/Assets/Ink/compiler/InkParser/CommentEliminator.cs.meta b/Assets/Ink/compiler/InkParser/CommentEliminator.cs.meta
new file mode 100644
index 000000000..3e3f04a54
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/CommentEliminator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e650e9d0adfa13d40a23175367324716
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/InkParser.cs b/Assets/Ink/compiler/InkParser/InkParser.cs
new file mode 100644
index 000000000..4068edbcf
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser.cs
@@ -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 ();
+
+ if (inkFilename != null) {
+ var fullRootInkPath = _fileHandler.ResolveInkFilename (inkFilename);
+ _openFilenames.Add (fullRootInkPath);
+ }
+
+ } else {
+ _rootParser = rootParser;
+ }
+
+ }
+
+ // Main entry point
+ public Parsed.Story Parse()
+ {
+ List 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 SeparatedList (SpecificParseRule mainRule, ParseRule separatorRule) where T : class
+ {
+ T firstElement = Parse (mainRule);
+ if (firstElement == null) return null;
+
+ var allElements = new List ();
+ 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;
+ 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;
+ }
+}
+
diff --git a/Assets/Ink/compiler/InkParser/InkParser.cs.meta b/Assets/Ink/compiler/InkParser/InkParser.cs.meta
new file mode 100644
index 000000000..a02351aea
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 57456b83f66dcfb4dbdc06ae75d41958
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/InkParser_AuthorWarning.cs b/Assets/Ink/compiler/InkParser/InkParser_AuthorWarning.cs
new file mode 100644
index 000000000..6e73e5972
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_AuthorWarning.cs
@@ -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);
+ }
+
+ }
+}
+
diff --git a/Assets/Ink/compiler/InkParser/InkParser_AuthorWarning.cs.meta b/Assets/Ink/compiler/InkParser/InkParser_AuthorWarning.cs.meta
new file mode 100644
index 000000000..6a6c386ca
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_AuthorWarning.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0c9f8990d12640c40b17fdb3552c08f6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/InkParser_CharacterRanges.cs b/Assets/Ink/compiler/InkParser/InkParser_CharacterRanges.cs
new file mode 100644
index 000000000..3ac3962b6
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_CharacterRanges.cs
@@ -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());
+ }
+ }
+
+ ///
+ /// Gets an array of representing all of the currently supported
+ /// non-ASCII character ranges that can be used in identifier names.
+ ///
+ ///
+ /// An array of representing all of the currently supported
+ /// non-ASCII character ranges that can be used in identifier names.
+ ///
+ public static CharacterRange[] ListAllCharacterRanges() {
+ return new CharacterRange[] {
+ LatinBasic,
+ LatinExtendedA,
+ LatinExtendedB,
+ Arabic,
+ Armenian,
+ Cyrillic,
+ Greek,
+ Hebrew,
+ Korean,
+ Latin1Supplement,
+ };
+ }
+ }
+}
+
diff --git a/Assets/Ink/compiler/InkParser/InkParser_CharacterRanges.cs.meta b/Assets/Ink/compiler/InkParser/InkParser_CharacterRanges.cs.meta
new file mode 100644
index 000000000..9925778f2
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_CharacterRanges.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e1cc3ab7c74670a40936f5c282a505e7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/InkParser_Choices.cs b/Assets/Ink/compiler/InkParser/InkParser_Choices.cs
new file mode 100644
index 000000000..39c148af7
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_Choices.cs
@@ -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 (OptionalExclude(Whitespace), String("*") );
+ if (bullets == null) {
+
+ bullets = Interleave (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 (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;
+ }
+}
+
diff --git a/Assets/Ink/compiler/InkParser/InkParser_Choices.cs.meta b/Assets/Ink/compiler/InkParser/InkParser_Choices.cs.meta
new file mode 100644
index 000000000..698cee60f
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_Choices.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a2d978613bf868c48b75265740aad51e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/InkParser_CommandLineInput.cs b/Assets/Ink/compiler/InkParser/InkParser_CommandLineInput.cs
new file mode 100644
index 000000000..a04ef1766
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_CommandLineInput.cs
@@ -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;
+ }
+}
+
diff --git a/Assets/Ink/compiler/InkParser/InkParser_CommandLineInput.cs.meta b/Assets/Ink/compiler/InkParser/InkParser_CommandLineInput.cs.meta
new file mode 100644
index 000000000..0bf765fcb
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_CommandLineInput.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 69954d17756084b4bb0c82e2238d36bd
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Ink/compiler/InkParser/InkParser_Conditional.cs b/Assets/Ink/compiler/InkParser/InkParser_Conditional.cs
new file mode 100644
index 000000000..c1f60ec50
--- /dev/null
+++ b/Assets/Ink/compiler/InkParser/InkParser_Conditional.cs
@@ -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 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 soleContent = StatementsAtLevel (StatementLevel.InnerBlock);
+ if (soleContent != null) {
+ var soleBranch = new ConditionalSingleBranch (soleContent);
+ alternatives = new List ();
+ 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 InlineConditionalBranches()
+ {
+ var listOfLists = Interleave> (MixedTextAndLogic, Exclude (String ("|")), flatten: false);
+ if (listOfLists == null || listOfLists.Count == 0) {
+ return null;
+ }
+
+ var result = new List ();
+
+ 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 MultilineConditionalBranches()
+ {
+ MultilineWhitespace ();
+
+ List