diff --git a/InkBlot.Tests/ContentTextTest.cs b/InkBlot.Tests/ContentTextTest.cs new file mode 100644 index 0000000..06d7e1e --- /dev/null +++ b/InkBlot.Tests/ContentTextTest.cs @@ -0,0 +1,34 @@ +using InkBlot.ParseHierarchy; +using Shouldly; + +namespace InkBlot.Tests; + +public class ContentTextTest +{ + [Theory] + [InlineData("Hello!", "Hello!", 0)] + [InlineData("Hel-lo!", "Hel-lo!", 0)] + [InlineData("Hel\\lo!", "Hello!", 0)] + [InlineData("Hel), threads (<-) and tags (#) are supported + [InlineData("Hel->lo!", "", 1)] + [InlineData("Hel<-lo!", "", 1)] + [InlineData("Hel#lo!", "", 1)] + public void TestBaseContentSuccess(string inkInput, string result, int numErrors) + { + // parse the story + var fileReader = new PreMadeFileReader([ + ("main.ink", inkInput) + ]); + var parser = new InkBlotParser(); + var (story, diagnostics) = parser.Parse(fileReader, "main.ink"); + + // check the diagnostic counts match + diagnostics.Count().ShouldBe(numErrors); + + // check the contents match (only if there was no diagnostic) + if (numErrors != 0) return; + var contents = story.StoryNodes.ToArray(); + contents.ShouldBe([new Content(result)]); + } +} \ No newline at end of file diff --git a/InkBlot.Tests/Helpers.cs b/InkBlot.Tests/Helpers.cs new file mode 100644 index 0000000..b9cbdab --- /dev/null +++ b/InkBlot.Tests/Helpers.cs @@ -0,0 +1,15 @@ +using System.Text; + +namespace InkBlot.Tests; + +/// +/// A file reader where the contents are directly provided as strings. +/// +/// A map between file names and their contents. +internal class PreMadeFileReader((string, string)[] filesToContents) : IFileReader +{ + public Stream GetContents(string filename) + { + return new MemoryStream(Encoding.UTF8.GetBytes(filesToContents.First(e => e.Item1 == filename).Item2)); + } +} \ No newline at end of file diff --git a/InkBlot.Tests/InkBlot.Tests.csproj b/InkBlot.Tests/InkBlot.Tests.csproj new file mode 100644 index 0000000..62f9816 --- /dev/null +++ b/InkBlot.Tests/InkBlot.Tests.csproj @@ -0,0 +1,26 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/InkBlot.sln b/InkBlot.sln index fa4fb42..9229807 100644 --- a/InkBlot.sln +++ b/InkBlot.sln @@ -1,9 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlot", "InkBlot\InkBlot.csproj", "{EDFCA854-0AF5-4C8F-8820-C328915B0FFE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlotTestConsoleApp", "InkBlotTestConsoleApp\InkBlotTestConsoleApp.csproj", "{1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlot.Tests", "InkBlot.Tests\InkBlot.Tests.csproj", "{1B83421E-8A2D-4862-9E84-D8E5EDE13264}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +21,9 @@ Global {1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}.Release|Any CPU.Build.0 = Release|Any CPU + {1B83421E-8A2D-4862-9E84-D8E5EDE13264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B83421E-8A2D-4862-9E84-D8E5EDE13264}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B83421E-8A2D-4862-9E84-D8E5EDE13264}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B83421E-8A2D-4862-9E84-D8E5EDE13264}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/InkBlot/Assembly.cs b/InkBlot/Assembly.cs new file mode 100644 index 0000000..cf360e5 --- /dev/null +++ b/InkBlot/Assembly.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("InkBlot.Tests")] \ No newline at end of file diff --git a/InkBlot/Class1.cs b/InkBlot/Class1.cs deleted file mode 100644 index a22f19b..0000000 --- a/InkBlot/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace InkBlot; - -public class Class1 -{ -} \ No newline at end of file diff --git a/InkBlot/Error.cs b/InkBlot/Error.cs new file mode 100644 index 0000000..f4c4479 --- /dev/null +++ b/InkBlot/Error.cs @@ -0,0 +1,5 @@ +namespace InkBlot; + +public record Range(int StartLine, int StartColumn, int EndLine, int EndColumn); + +public record Diagnostic(string FileName, Range Range, string Message, string Context); \ No newline at end of file diff --git a/InkBlot/Generated/InkBlotAntlrGrammar.interp b/InkBlot/Generated/InkBlotAntlrGrammar.interp new file mode 100644 index 0000000..89afa26 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammar.interp @@ -0,0 +1,17 @@ +token literal names: +null +null +null + +token symbolic names: +null +Whitespace +CONTENT_TEXT_NO_ESCAPE_SIMPLE + +rule names: +story +contentText + + +atn: +[4, 1, 2, 12, 2, 0, 7, 0, 2, 1, 7, 1, 1, 0, 4, 0, 6, 8, 0, 11, 0, 12, 0, 7, 1, 1, 1, 1, 1, 1, 0, 0, 2, 0, 2, 0, 0, 10, 0, 5, 1, 0, 0, 0, 2, 9, 1, 0, 0, 0, 4, 6, 3, 2, 1, 0, 5, 4, 1, 0, 0, 0, 6, 7, 1, 0, 0, 0, 7, 5, 1, 0, 0, 0, 7, 8, 1, 0, 0, 0, 8, 1, 1, 0, 0, 0, 9, 10, 5, 2, 0, 0, 10, 3, 1, 0, 0, 0, 1, 7] \ No newline at end of file diff --git a/InkBlot/Generated/InkBlotAntlrGrammar.tokens b/InkBlot/Generated/InkBlotAntlrGrammar.tokens new file mode 100644 index 0000000..b9deb38 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammar.tokens @@ -0,0 +1,2 @@ +Whitespace=1 +CONTENT_TEXT_NO_ESCAPE_SIMPLE=2 diff --git a/InkBlot/Generated/InkBlotAntlrGrammarBaseListener.cs b/InkBlot/Generated/InkBlotAntlrGrammarBaseListener.cs new file mode 100644 index 0000000..47037b6 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarBaseListener.cs @@ -0,0 +1,75 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// ANTLR Version: 4.13.2 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// Generated from E:/ProgettiUnity/InkAntlr/InkBlot/InkBlot/InkBlotAntlrGrammar.g4 by ANTLR 4.13.2 + +// Unreachable code detected +#pragma warning disable 0162 +// The variable '...' is assigned but its value is never used +#pragma warning disable 0219 +// Missing XML comment for publicly visible type or member '...' +#pragma warning disable 1591 +// Ambiguous reference in cref attribute +#pragma warning disable 419 + + +using Antlr4.Runtime.Misc; +using IErrorNode = Antlr4.Runtime.Tree.IErrorNode; +using ITerminalNode = Antlr4.Runtime.Tree.ITerminalNode; +using IToken = Antlr4.Runtime.IToken; +using ParserRuleContext = Antlr4.Runtime.ParserRuleContext; + +/// +/// This class provides an empty implementation of , +/// which can be extended to create a listener which only needs to handle a subset +/// of the available methods. +/// +[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")] +[System.Diagnostics.DebuggerNonUserCode] +[System.CLSCompliant(false)] +public partial class InkBlotAntlrGrammarBaseListener : IInkBlotAntlrGrammarListener { + /// + /// Enter a parse tree produced by . + /// The default implementation does nothing. + /// + /// The parse tree. + public virtual void EnterStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context) { } + /// + /// Exit a parse tree produced by . + /// The default implementation does nothing. + /// + /// The parse tree. + public virtual void ExitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context) { } + /// + /// Enter a parse tree produced by . + /// The default implementation does nothing. + /// + /// The parse tree. + public virtual void EnterContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context) { } + /// + /// Exit a parse tree produced by . + /// The default implementation does nothing. + /// + /// The parse tree. + public virtual void ExitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context) { } + + /// + /// The default implementation does nothing. + public virtual void EnterEveryRule([NotNull] ParserRuleContext context) { } + /// + /// The default implementation does nothing. + public virtual void ExitEveryRule([NotNull] ParserRuleContext context) { } + /// + /// The default implementation does nothing. + public virtual void VisitTerminal([NotNull] ITerminalNode node) { } + /// + /// The default implementation does nothing. + public virtual void VisitErrorNode([NotNull] IErrorNode node) { } +} diff --git a/InkBlot/Generated/InkBlotAntlrGrammarBaseVisitor.cs b/InkBlot/Generated/InkBlotAntlrGrammarBaseVisitor.cs new file mode 100644 index 0000000..dd7c679 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarBaseVisitor.cs @@ -0,0 +1,57 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// ANTLR Version: 4.13.2 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// Generated from E:/ProgettiUnity/InkAntlr/InkBlot/InkBlot/InkBlotAntlrGrammar.g4 by ANTLR 4.13.2 + +// Unreachable code detected +#pragma warning disable 0162 +// The variable '...' is assigned but its value is never used +#pragma warning disable 0219 +// Missing XML comment for publicly visible type or member '...' +#pragma warning disable 1591 +// Ambiguous reference in cref attribute +#pragma warning disable 419 + +using Antlr4.Runtime.Misc; +using Antlr4.Runtime.Tree; +using IToken = Antlr4.Runtime.IToken; +using ParserRuleContext = Antlr4.Runtime.ParserRuleContext; + +/// +/// This class provides an empty implementation of , +/// which can be extended to create a visitor which only needs to handle a subset +/// of the available methods. +/// +/// The return type of the visit operation. +[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")] +[System.Diagnostics.DebuggerNonUserCode] +[System.CLSCompliant(false)] +public partial class InkBlotAntlrGrammarBaseVisitor : AbstractParseTreeVisitor, IInkBlotAntlrGrammarVisitor { + /// + /// Visit a parse tree produced by . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context) { return VisitChildren(context); } + /// + /// Visit a parse tree produced by . + /// + /// The default implementation returns the result of calling + /// on . + /// + /// + /// The parse tree. + /// The visitor result. + public virtual Result VisitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context) { return VisitChildren(context); } +} diff --git a/InkBlot/Generated/InkBlotAntlrGrammarLexer.cs b/InkBlot/Generated/InkBlotAntlrGrammarLexer.cs new file mode 100644 index 0000000..7809d29 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarLexer.cs @@ -0,0 +1,120 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// ANTLR Version: 4.13.2 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// Generated from E:/ProgettiUnity/InkAntlr/InkBlot/InkBlot/InkBlotAntlrGrammar.g4 by ANTLR 4.13.2 + +// Unreachable code detected +#pragma warning disable 0162 +// The variable '...' is assigned but its value is never used +#pragma warning disable 0219 +// Missing XML comment for publicly visible type or member '...' +#pragma warning disable 1591 +// Ambiguous reference in cref attribute +#pragma warning disable 419 + +using System; +using System.IO; +using System.Text; +using Antlr4.Runtime; +using Antlr4.Runtime.Atn; +using Antlr4.Runtime.Misc; +using DFA = Antlr4.Runtime.Dfa.DFA; + +[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")] +[System.CLSCompliant(false)] +public partial class InkBlotAntlrGrammarLexer : Lexer { + protected static DFA[] decisionToDFA; + protected static PredictionContextCache sharedContextCache = new PredictionContextCache(); + public const int + Whitespace=1, CONTENT_TEXT_NO_ESCAPE_SIMPLE=2; + public static string[] channelNames = { + "DEFAULT_TOKEN_CHANNEL", "HIDDEN" + }; + + public static string[] modeNames = { + "DEFAULT_MODE" + }; + + public static readonly string[] ruleNames = { + "Whitespace", "CONTENT_TEXT_NO_ESCAPE_SIMPLE" + }; + + + public InkBlotAntlrGrammarLexer(ICharStream input) + : this(input, Console.Out, Console.Error) { } + + public InkBlotAntlrGrammarLexer(ICharStream input, TextWriter output, TextWriter errorOutput) + : base(input, output, errorOutput) + { + Interpreter = new LexerATNSimulator(this, _ATN, decisionToDFA, sharedContextCache); + } + + private static readonly string[] _LiteralNames = { + }; + private static readonly string[] _SymbolicNames = { + null, "Whitespace", "CONTENT_TEXT_NO_ESCAPE_SIMPLE" + }; + public static readonly IVocabulary DefaultVocabulary = new Vocabulary(_LiteralNames, _SymbolicNames); + + [NotNull] + public override IVocabulary Vocabulary + { + get + { + return DefaultVocabulary; + } + } + + public override string GrammarFileName { get { return "InkBlotAntlrGrammar.g4"; } } + + public override string[] RuleNames { get { return ruleNames; } } + + public override string[] ChannelNames { get { return channelNames; } } + + public override string[] ModeNames { get { return modeNames; } } + + public override int[] SerializedAtn { get { return _serializedATN; } } + + static InkBlotAntlrGrammarLexer() { + decisionToDFA = new DFA[_ATN.NumberOfDecisions]; + for (int i = 0; i < _ATN.NumberOfDecisions; i++) { + decisionToDFA[i] = new DFA(_ATN.GetDecisionState(i), i); + } + } + public override bool Sempred(RuleContext _localctx, int ruleIndex, int predIndex) { + switch (ruleIndex) { + case 1 : return CONTENT_TEXT_NO_ESCAPE_SIMPLE_sempred(_localctx, predIndex); + } + return true; + } + private bool CONTENT_TEXT_NO_ESCAPE_SIMPLE_sempred(RuleContext _localctx, int predIndex) { + switch (predIndex) { + case 0: return InputStream.LA(1) != '>' ; + case 1: return InputStream.LA(1) != '-' && InputStream.LA(1) != '>' ; + } + return true; + } + + private static int[] _serializedATN = { + 4,0,2,21,6,-1,2,0,7,0,2,1,7,1,1,0,4,0,7,8,0,11,0,12,0,8,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,4,1,18,8,1,11,1,12,1,19,0,0,2,1,1,3,2,1,0,3,2,0,9,9,32,32, + 5,0,10,10,13,13,35,60,92,92,123,125,1,0,0,65535,25,0,1,1,0,0,0,0,3,1,0, + 0,0,1,6,1,0,0,0,3,17,1,0,0,0,5,7,7,0,0,0,6,5,1,0,0,0,7,8,1,0,0,0,8,6,1, + 0,0,0,8,9,1,0,0,0,9,2,1,0,0,0,10,18,8,1,0,0,11,12,5,92,0,0,12,18,7,2,0, + 0,13,14,5,45,0,0,14,18,4,1,0,0,15,16,5,60,0,0,16,18,4,1,1,0,17,10,1,0, + 0,0,17,11,1,0,0,0,17,13,1,0,0,0,17,15,1,0,0,0,18,19,1,0,0,0,19,17,1,0, + 0,0,19,20,1,0,0,0,20,4,1,0,0,0,4,0,8,17,19,0 + }; + + public static readonly ATN _ATN = + new ATNDeserializer().Deserialize(_serializedATN); + + +} diff --git a/InkBlot/Generated/InkBlotAntlrGrammarLexer.interp b/InkBlot/Generated/InkBlotAntlrGrammarLexer.interp new file mode 100644 index 0000000..d65f157 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarLexer.interp @@ -0,0 +1,23 @@ +token literal names: +null +null +null + +token symbolic names: +null +Whitespace +CONTENT_TEXT_NO_ESCAPE_SIMPLE + +rule names: +Whitespace +CONTENT_TEXT_NO_ESCAPE_SIMPLE + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[4, 0, 2, 21, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 1, 0, 4, 0, 7, 8, 0, 11, 0, 12, 0, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 1, 18, 8, 1, 11, 1, 12, 1, 19, 0, 0, 2, 1, 1, 3, 2, 1, 0, 3, 2, 0, 9, 9, 32, 32, 5, 0, 10, 10, 13, 13, 35, 60, 92, 92, 123, 125, 1, 0, 0, 65535, 25, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 1, 6, 1, 0, 0, 0, 3, 17, 1, 0, 0, 0, 5, 7, 7, 0, 0, 0, 6, 5, 1, 0, 0, 0, 7, 8, 1, 0, 0, 0, 8, 6, 1, 0, 0, 0, 8, 9, 1, 0, 0, 0, 9, 2, 1, 0, 0, 0, 10, 18, 8, 1, 0, 0, 11, 12, 5, 92, 0, 0, 12, 18, 7, 2, 0, 0, 13, 14, 5, 45, 0, 0, 14, 18, 4, 1, 0, 0, 15, 16, 5, 60, 0, 0, 16, 18, 4, 1, 1, 0, 17, 10, 1, 0, 0, 0, 17, 11, 1, 0, 0, 0, 17, 13, 1, 0, 0, 0, 17, 15, 1, 0, 0, 0, 18, 19, 1, 0, 0, 0, 19, 17, 1, 0, 0, 0, 19, 20, 1, 0, 0, 0, 20, 4, 1, 0, 0, 0, 4, 0, 8, 17, 19, 0] \ No newline at end of file diff --git a/InkBlot/Generated/InkBlotAntlrGrammarLexer.tokens b/InkBlot/Generated/InkBlotAntlrGrammarLexer.tokens new file mode 100644 index 0000000..b9deb38 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarLexer.tokens @@ -0,0 +1,2 @@ +Whitespace=1 +CONTENT_TEXT_NO_ESCAPE_SIMPLE=2 diff --git a/InkBlot/Generated/InkBlotAntlrGrammarListener.cs b/InkBlot/Generated/InkBlotAntlrGrammarListener.cs new file mode 100644 index 0000000..100d7fb --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarListener.cs @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// ANTLR Version: 4.13.2 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// Generated from E:/ProgettiUnity/InkAntlr/InkBlot/InkBlot/InkBlotAntlrGrammar.g4 by ANTLR 4.13.2 + +// Unreachable code detected +#pragma warning disable 0162 +// The variable '...' is assigned but its value is never used +#pragma warning disable 0219 +// Missing XML comment for publicly visible type or member '...' +#pragma warning disable 1591 +// Ambiguous reference in cref attribute +#pragma warning disable 419 + +using Antlr4.Runtime.Misc; +using IParseTreeListener = Antlr4.Runtime.Tree.IParseTreeListener; +using IToken = Antlr4.Runtime.IToken; + +/// +/// This interface defines a complete listener for a parse tree produced by +/// . +/// +[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")] +[System.CLSCompliant(false)] +public interface IInkBlotAntlrGrammarListener : IParseTreeListener { + /// + /// Enter a parse tree produced by . + /// + /// The parse tree. + void EnterStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context); + /// + /// Exit a parse tree produced by . + /// + /// The parse tree. + void ExitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context); + /// + /// Enter a parse tree produced by . + /// + /// The parse tree. + void EnterContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context); + /// + /// Exit a parse tree produced by . + /// + /// The parse tree. + void ExitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context); +} diff --git a/InkBlot/Generated/InkBlotAntlrGrammarParser.cs b/InkBlot/Generated/InkBlotAntlrGrammarParser.cs new file mode 100644 index 0000000..ece8885 --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarParser.cs @@ -0,0 +1,205 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// ANTLR Version: 4.13.2 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// Generated from E:/ProgettiUnity/InkAntlr/InkBlot/InkBlot/InkBlotAntlrGrammar.g4 by ANTLR 4.13.2 + +// Unreachable code detected +#pragma warning disable 0162 +// The variable '...' is assigned but its value is never used +#pragma warning disable 0219 +// Missing XML comment for publicly visible type or member '...' +#pragma warning disable 1591 +// Ambiguous reference in cref attribute +#pragma warning disable 419 + +using System; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Collections.Generic; +using Antlr4.Runtime; +using Antlr4.Runtime.Atn; +using Antlr4.Runtime.Misc; +using Antlr4.Runtime.Tree; +using DFA = Antlr4.Runtime.Dfa.DFA; + +[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")] +[System.CLSCompliant(false)] +public partial class InkBlotAntlrGrammarParser : Parser { + protected static DFA[] decisionToDFA; + protected static PredictionContextCache sharedContextCache = new PredictionContextCache(); + public const int + Whitespace=1, CONTENT_TEXT_NO_ESCAPE_SIMPLE=2; + public const int + RULE_story = 0, RULE_contentText = 1; + public static readonly string[] ruleNames = { + "story", "contentText" + }; + + private static readonly string[] _LiteralNames = { + }; + private static readonly string[] _SymbolicNames = { + null, "Whitespace", "CONTENT_TEXT_NO_ESCAPE_SIMPLE" + }; + public static readonly IVocabulary DefaultVocabulary = new Vocabulary(_LiteralNames, _SymbolicNames); + + [NotNull] + public override IVocabulary Vocabulary + { + get + { + return DefaultVocabulary; + } + } + + public override string GrammarFileName { get { return "InkBlotAntlrGrammar.g4"; } } + + public override string[] RuleNames { get { return ruleNames; } } + + public override int[] SerializedAtn { get { return _serializedATN; } } + + static InkBlotAntlrGrammarParser() { + decisionToDFA = new DFA[_ATN.NumberOfDecisions]; + for (int i = 0; i < _ATN.NumberOfDecisions; i++) { + decisionToDFA[i] = new DFA(_ATN.GetDecisionState(i), i); + } + } + + public InkBlotAntlrGrammarParser(ITokenStream input) : this(input, Console.Out, Console.Error) { } + + public InkBlotAntlrGrammarParser(ITokenStream input, TextWriter output, TextWriter errorOutput) + : base(input, output, errorOutput) + { + Interpreter = new ParserATNSimulator(this, _ATN, decisionToDFA, sharedContextCache); + } + + public partial class StoryContext : ParserRuleContext { + [System.Diagnostics.DebuggerNonUserCode] public ContentTextContext[] contentText() { + return GetRuleContexts(); + } + [System.Diagnostics.DebuggerNonUserCode] public ContentTextContext contentText(int i) { + return GetRuleContext(i); + } + public StoryContext(ParserRuleContext parent, int invokingState) + : base(parent, invokingState) + { + } + public override int RuleIndex { get { return RULE_story; } } + [System.Diagnostics.DebuggerNonUserCode] + public override void EnterRule(IParseTreeListener listener) { + IInkBlotAntlrGrammarListener typedListener = listener as IInkBlotAntlrGrammarListener; + if (typedListener != null) typedListener.EnterStory(this); + } + [System.Diagnostics.DebuggerNonUserCode] + public override void ExitRule(IParseTreeListener listener) { + IInkBlotAntlrGrammarListener typedListener = listener as IInkBlotAntlrGrammarListener; + if (typedListener != null) typedListener.ExitStory(this); + } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IInkBlotAntlrGrammarVisitor typedVisitor = visitor as IInkBlotAntlrGrammarVisitor; + if (typedVisitor != null) return typedVisitor.VisitStory(this); + else return visitor.VisitChildren(this); + } + } + + [RuleVersion(0)] + public StoryContext story() { + StoryContext _localctx = new StoryContext(Context, State); + EnterRule(_localctx, 0, RULE_story); + int _la; + try { + EnterOuterAlt(_localctx, 1); + { + State = 5; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + do { + { + { + State = 4; + contentText(); + } + } + State = 7; + ErrorHandler.Sync(this); + _la = TokenStream.LA(1); + } while ( _la==CONTENT_TEXT_NO_ESCAPE_SIMPLE ); + } + } + catch (RecognitionException re) { + _localctx.exception = re; + ErrorHandler.ReportError(this, re); + ErrorHandler.Recover(this, re); + } + finally { + ExitRule(); + } + return _localctx; + } + + public partial class ContentTextContext : ParserRuleContext { + [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CONTENT_TEXT_NO_ESCAPE_SIMPLE() { return GetToken(InkBlotAntlrGrammarParser.CONTENT_TEXT_NO_ESCAPE_SIMPLE, 0); } + public ContentTextContext(ParserRuleContext parent, int invokingState) + : base(parent, invokingState) + { + } + public override int RuleIndex { get { return RULE_contentText; } } + [System.Diagnostics.DebuggerNonUserCode] + public override void EnterRule(IParseTreeListener listener) { + IInkBlotAntlrGrammarListener typedListener = listener as IInkBlotAntlrGrammarListener; + if (typedListener != null) typedListener.EnterContentText(this); + } + [System.Diagnostics.DebuggerNonUserCode] + public override void ExitRule(IParseTreeListener listener) { + IInkBlotAntlrGrammarListener typedListener = listener as IInkBlotAntlrGrammarListener; + if (typedListener != null) typedListener.ExitContentText(this); + } + [System.Diagnostics.DebuggerNonUserCode] + public override TResult Accept(IParseTreeVisitor visitor) { + IInkBlotAntlrGrammarVisitor typedVisitor = visitor as IInkBlotAntlrGrammarVisitor; + if (typedVisitor != null) return typedVisitor.VisitContentText(this); + else return visitor.VisitChildren(this); + } + } + + [RuleVersion(0)] + public ContentTextContext contentText() { + ContentTextContext _localctx = new ContentTextContext(Context, State); + EnterRule(_localctx, 2, RULE_contentText); + try { + EnterOuterAlt(_localctx, 1); + { + State = 9; + Match(CONTENT_TEXT_NO_ESCAPE_SIMPLE); + } + } + catch (RecognitionException re) { + _localctx.exception = re; + ErrorHandler.ReportError(this, re); + ErrorHandler.Recover(this, re); + } + finally { + ExitRule(); + } + return _localctx; + } + + private static int[] _serializedATN = { + 4,1,2,12,2,0,7,0,2,1,7,1,1,0,4,0,6,8,0,11,0,12,0,7,1,1,1,1,1,1,0,0,2,0, + 2,0,0,10,0,5,1,0,0,0,2,9,1,0,0,0,4,6,3,2,1,0,5,4,1,0,0,0,6,7,1,0,0,0,7, + 5,1,0,0,0,7,8,1,0,0,0,8,1,1,0,0,0,9,10,5,2,0,0,10,3,1,0,0,0,1,7 + }; + + public static readonly ATN _ATN = + new ATNDeserializer().Deserialize(_serializedATN); + + +} diff --git a/InkBlot/Generated/InkBlotAntlrGrammarVisitor.cs b/InkBlot/Generated/InkBlotAntlrGrammarVisitor.cs new file mode 100644 index 0000000..bcca54b --- /dev/null +++ b/InkBlot/Generated/InkBlotAntlrGrammarVisitor.cs @@ -0,0 +1,46 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// ANTLR Version: 4.13.2 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +// Generated from E:/ProgettiUnity/InkAntlr/InkBlot/InkBlot/InkBlotAntlrGrammar.g4 by ANTLR 4.13.2 + +// Unreachable code detected +#pragma warning disable 0162 +// The variable '...' is assigned but its value is never used +#pragma warning disable 0219 +// Missing XML comment for publicly visible type or member '...' +#pragma warning disable 1591 +// Ambiguous reference in cref attribute +#pragma warning disable 419 + +using Antlr4.Runtime.Misc; +using Antlr4.Runtime.Tree; +using IToken = Antlr4.Runtime.IToken; + +/// +/// This interface defines a complete generic visitor for a parse tree produced +/// by . +/// +/// The return type of the visit operation. +[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")] +[System.CLSCompliant(false)] +public interface IInkBlotAntlrGrammarVisitor : IParseTreeVisitor { + /// + /// Visit a parse tree produced by . + /// + /// The parse tree. + /// The visitor result. + Result VisitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context); + /// + /// Visit a parse tree produced by . + /// + /// The parse tree. + /// The visitor result. + Result VisitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context); +} diff --git a/InkBlot/IFileReader.cs b/InkBlot/IFileReader.cs new file mode 100644 index 0000000..fbf8e34 --- /dev/null +++ b/InkBlot/IFileReader.cs @@ -0,0 +1,17 @@ +namespace InkBlot; + +/// +/// An object that can read files. +/// +public interface IFileReader +{ + /// + /// Get the contents of a file. + /// + /// + /// Name of the file to read. The name is relative to the main ink file. The name is normalized + /// (uses only single forward slashes between path elements). + /// + /// A stream with the contents of the file. + Stream GetContents(string filename); +} \ No newline at end of file diff --git a/InkBlot/InkBlot.csproj b/InkBlot/InkBlot.csproj index 17b910f..994ed32 100644 --- a/InkBlot/InkBlot.csproj +++ b/InkBlot/InkBlot.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/InkBlot/InkBlotAntlrGrammar.g4 b/InkBlot/InkBlotAntlrGrammar.g4 new file mode 100644 index 0000000..84d5509 --- /dev/null +++ b/InkBlot/InkBlotAntlrGrammar.g4 @@ -0,0 +1,7 @@ +grammar InkBlotAntlrGrammar; + +import InkBlotAntlrLexer; + +story: contentText+ ; + +contentText: CONTENT_TEXT_NO_ESCAPE_SIMPLE ; \ No newline at end of file diff --git a/InkBlot/InkBlotAntlrLexer.g4 b/InkBlot/InkBlotAntlrLexer.g4 new file mode 100644 index 0000000..de170d8 --- /dev/null +++ b/InkBlot/InkBlotAntlrLexer.g4 @@ -0,0 +1,22 @@ +lexer grammar InkBlotAntlrLexer; + +Whitespace: [ \t]+ ; + +// see InkParser_Content.cs, ContentTextNoEscape and ContentTextAllowingEcapeChar for the escape case +// this works for the base case where we're not parsing a string, nor a choice +CONTENT_TEXT_NO_ESCAPE_SIMPLE: + ( + // any character is valid, except for: + // - {} ==> identifies embedded logic + // - | ==> text alternatives, is forbidden even in non-logic text for some reason + // - \n\r ==> a new line of content + // - # ==> a tag + // - \, < and - with exceptions (see below) + ~[{}|\n\r\\#-<] + // any character can be escaped + | '\\' [\u0000-\uFFFF] // TODO: is there a better way to say "any character"? + // accept a - only if not followed by a > (->, a divert) + | '-' { InputStream.LA(1) != '>' }? + // same for threads (<-) and glue (<>) + | '<' { InputStream.LA(1) != '-' && InputStream.LA(1) != '>' }? + )+ ; \ No newline at end of file diff --git a/InkBlot/InkBlotParser.cs b/InkBlot/InkBlotParser.cs new file mode 100644 index 0000000..2465120 --- /dev/null +++ b/InkBlot/InkBlotParser.cs @@ -0,0 +1,34 @@ +using Antlr4.Runtime; +using Antlr4.Runtime.Atn; +using Antlr4.Runtime.Tree; +using InkBlot.ParseHierarchy; +using InkBlot.Visitor; + +namespace InkBlot; + +public class InkBlotParser +{ + public (Story, IEnumerable) Parse(IFileReader fileReader, string mainFileName) + { + var stream = fileReader.GetContents(mainFileName); + var inputStream = new AntlrInputStream(stream); + var lexer = new InkBlotAntlrGrammarLexer(inputStream); + var lexerErrorListener = new LexerErrorListener(); + lexer.RemoveErrorListeners(); + lexer.AddErrorListener(lexerErrorListener); + var tokens = new CommonTokenStream(lexer); + var parser = new InkBlotAntlrGrammarParser(tokens); + var parserErrorListener = new ParserErrorListener(); + parser.RemoveErrorListeners(); + parser.AddErrorListener(parserErrorListener); +#if DEBUG + parser.AddErrorListener(new DiagnosticErrorListener()); + parser.Interpreter.PredictionMode = PredictionMode.LL_EXACT_AMBIG_DETECTION; +#endif + var tree = parser.story(); + var listener = new Listener(); + var walker = new ParseTreeWalker(); + walker.Walk(listener, tree); + return (listener.Story, lexerErrorListener.Diagnostics.Concat(parserErrorListener.Diagnostics)); + } +} \ No newline at end of file diff --git a/InkBlot/LexerErrorListener.cs b/InkBlot/LexerErrorListener.cs new file mode 100644 index 0000000..d1debf3 --- /dev/null +++ b/InkBlot/LexerErrorListener.cs @@ -0,0 +1,16 @@ +using Antlr4.Runtime; + +namespace InkBlot; + +// from https://github.com/YarnSpinnerTool/YarnSpinner/blob/main/YarnSpinner.Compiler/ErrorListener.cs#L247 +internal sealed class LexerErrorListener : IAntlrErrorListener +{ + public readonly List Diagnostics = []; + + public void SyntaxError(TextWriter output, IRecognizer recognizer, int offendingSymbol, int line, + int charPositionInLine, string msg, RecognitionException e) + { + var range = new Range(line - 1, charPositionInLine, line - 1, charPositionInLine + 1); + Diagnostics.Add(new Diagnostic("", range, msg, "")); + } +} \ No newline at end of file diff --git a/InkBlot/ParseHierarchy/Content.cs b/InkBlot/ParseHierarchy/Content.cs new file mode 100644 index 0000000..7b13474 --- /dev/null +++ b/InkBlot/ParseHierarchy/Content.cs @@ -0,0 +1,3 @@ +namespace InkBlot.ParseHierarchy; + +public record Content(string Text) : StoryNode; \ No newline at end of file diff --git a/InkBlot/ParseHierarchy/Story.cs b/InkBlot/ParseHierarchy/Story.cs new file mode 100644 index 0000000..1aa5c61 --- /dev/null +++ b/InkBlot/ParseHierarchy/Story.cs @@ -0,0 +1,3 @@ +namespace InkBlot.ParseHierarchy; + +public record Story(IEnumerable StoryNodes) : StoryNode; \ No newline at end of file diff --git a/InkBlot/ParseHierarchy/StoryNode.cs b/InkBlot/ParseHierarchy/StoryNode.cs new file mode 100644 index 0000000..100aec2 --- /dev/null +++ b/InkBlot/ParseHierarchy/StoryNode.cs @@ -0,0 +1,6 @@ +namespace InkBlot.ParseHierarchy; + +/// +/// Any node in the parsed story +/// +public record StoryNode; \ No newline at end of file diff --git a/InkBlot/ParserErrorListener.cs b/InkBlot/ParserErrorListener.cs new file mode 100644 index 0000000..fac862d --- /dev/null +++ b/InkBlot/ParserErrorListener.cs @@ -0,0 +1,58 @@ +using System.Text; +using Antlr4.Runtime; + +namespace InkBlot; + +internal sealed class ParserErrorListener : BaseErrorListener +{ + public readonly List Diagnostics = []; + + public override void SyntaxError(TextWriter output, IRecognizer recognizer, IToken offendingSymbol, int line, + int charPositionInLine, + string msg, RecognitionException e) + { + // adapted from https://github.com/YarnSpinnerTool/YarnSpinner/blob/84e17d04fdf4ac2f824ed1d4d277bb3a1dde73f6/YarnSpinner.Compiler/ErrorListener.cs#L266 + var range = new Range(line - 1, charPositionInLine, line - 1, charPositionInLine + 1); + var context = ""; + + if (offendingSymbol.TokenSource != null) + { + var builder = new StringBuilder(); + + // the line with the error on it + var input = offendingSymbol.TokenSource.InputStream.ToString(); + if (input != null) + { + var lines = input.Split('\n'); + var errorLine = lines[line - 1]; + builder.AppendLine(errorLine); + + // adding indicator symbols pointing out where the error is + // on the line + var start = offendingSymbol.StartIndex; + var stop = offendingSymbol.StopIndex; + if (start >= 0 && stop >= 0) + { + // the end point of the error in "line space" + var end = stop - start + charPositionInLine + 1; + for (var i = 0; i < end; i++) + // move over until we are at the point we need to + // be + if (i >= charPositionInLine && i < end) + builder.Append('^'); + else + builder.Append(' '); + } + + context = builder.ToString(); + + range = new Range(offendingSymbol.Line - 1, offendingSymbol.Column, offendingSymbol.Line - 1, + offendingSymbol.Column + offendingSymbol.Text.Length); + } + } + + var diagnostic = new Diagnostic("", range, msg, context); + + Diagnostics.Add(diagnostic); + } +} \ No newline at end of file diff --git a/InkBlot/Visitor/Listener.cs b/InkBlot/Visitor/Listener.cs new file mode 100644 index 0000000..802836e --- /dev/null +++ b/InkBlot/Visitor/Listener.cs @@ -0,0 +1,5 @@ +namespace InkBlot.Visitor; + +public partial class Listener : InkBlotAntlrGrammarBaseListener +{ +} \ No newline at end of file diff --git a/InkBlot/Visitor/ListenerContentText.cs b/InkBlot/Visitor/ListenerContentText.cs new file mode 100644 index 0000000..1382b59 --- /dev/null +++ b/InkBlot/Visitor/ListenerContentText.cs @@ -0,0 +1,34 @@ +using System.Text.RegularExpressions; +using Antlr4.Runtime.Tree; + +namespace InkBlot.Visitor; + +public partial class Listener +{ + private readonly ParseTreeProperty _contentTextValue = new(); + + private readonly Regex _escapeRegex = MyRegex(); + + [GeneratedRegex(@"\\(.)")] + private static partial Regex MyRegex(); + + private string GetContentText(InkBlotAntlrGrammarParser.ContentTextContext context) + { + return _contentTextValue.Get(context); + } + + public override void ExitContentText(InkBlotAntlrGrammarParser.ContentTextContext context) + { + // escape sequences are captured by this node, but not interpreted + var contentWithEscapes = context.CONTENT_TEXT_NO_ESCAPE_SIMPLE().ToString(); + if (contentWithEscapes == null) + // when does this happen? :? + throw new InvalidOperationException(); + + // replace \X with just X + var content = _escapeRegex.Replace(contentWithEscapes, match => match.Groups[1].Value); + + // save the result + _contentTextValue.Put(context, content); + } +} \ No newline at end of file diff --git a/InkBlot/Visitor/ListenerStory.cs b/InkBlot/Visitor/ListenerStory.cs new file mode 100644 index 0000000..3d2a423 --- /dev/null +++ b/InkBlot/Visitor/ListenerStory.cs @@ -0,0 +1,20 @@ +using InkBlot.ParseHierarchy; + +namespace InkBlot.Visitor; + +public partial class Listener +{ + private Story? _story; + + public Story Story => _story ?? throw new InvalidOperationException("No story found yet."); + + public override void ExitStory(InkBlotAntlrGrammarParser.StoryContext context) + { + var storyNodes = context.children.Select(child => child switch + { + InkBlotAntlrGrammarParser.ContentTextContext contentText => new Content(GetContentText(contentText)), + _ => throw new InvalidOperationException($"unknown context of type {child.GetType()}") + }); + _story = new Story(storyNodes); + } +} \ No newline at end of file diff --git a/InkBlotTestConsoleApp/InkBlotTestConsoleApp.csproj b/InkBlotTestConsoleApp/InkBlotTestConsoleApp.csproj index 85b4959..030eedb 100644 --- a/InkBlotTestConsoleApp/InkBlotTestConsoleApp.csproj +++ b/InkBlotTestConsoleApp/InkBlotTestConsoleApp.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/InkBlotTestConsoleApp/Program.cs b/InkBlotTestConsoleApp/Program.cs index e5dff12..e654633 100644 --- a/InkBlotTestConsoleApp/Program.cs +++ b/InkBlotTestConsoleApp/Program.cs @@ -1,3 +1,19 @@ // See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); \ No newline at end of file +using System.Text; +using InkBlot; + +var parser = new InkBlotParser(); +parser.Parse(new PreMadeFileReader(new Dictionary +{ + { "main.ink", "Hel-\\lo!" } +}), "main.ink"); + + +internal class PreMadeFileReader(Dictionary filesToContents) : IFileReader +{ + public Stream GetContents(string filename) + { + return new MemoryStream(Encoding.UTF8.GetBytes(filesToContents[filename])); + } +} \ No newline at end of file