mirror of
https://github.com/Ratstail91/Mementos.git
synced 2025-11-29 02:24:28 +11:00
Committed everything
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59b3b9369a2a7481aab00ef369b7c4a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97d6f86dbb9994db6bd0572b69f53f25
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a2f15af905c94ac9bdfb197799eec41
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2383f2e1b977347c2b6fb596e9af6d97
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea21bfef8977a4907b49628d9b651ebf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ca27cda0c9364bfbaf37f8db278563f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 334d80c537ee2473ea6a7cbd20e09f14
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8af10c051a6c942b1a43cb31ceb18114
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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 ("<-");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7aec7c9fb87244b3a8c64753c8a2f9b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85c9eb24bda894235981df7a3689da51
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4b92fc2dbd664298b01416321678d61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 77e8119b05a284a889e4b2978c6b1617
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa9a6c9127e3f4eb8a46f532557ccd2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0e33e10232e5c4587a8c460cf9d99960
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42fdd6da57c584bf58ff578d093b8529
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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\\");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fed5929e88fa3480e8f02d3f686c0c2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fbdaec262d8f64dd684a79c235405a10
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user