feat: parse content text (base case).

This commit is contained in:
mattia
2025-02-16 18:26:28 +01:00
parent b7aae9a04f
commit d386c50499
32 changed files with 940 additions and 6 deletions

View File

@@ -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<lo!", "Hel<lo!", 0)]
// TODO: check error situations better when diverts (->), 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)]);
}
}

15
InkBlot.Tests/Helpers.cs Normal file
View File

@@ -0,0 +1,15 @@
using System.Text;
namespace InkBlot.Tests;
/// <summary>
/// A file reader where the contents are directly provided as strings.
/// </summary>
/// <param name="filesToContents">A map between file names and their contents.</param>
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));
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/>
<PackageReference Include="Shouldly" Version="4.3.0"/>
<PackageReference Include="xunit" Version="2.9.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\InkBlot\InkBlot.csproj"/>
</ItemGroup>
</Project>

View File

@@ -1,9 +1,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlot", "InkBlot\InkBlot.csproj", "{EDFCA854-0AF5-4C8F-8820-C328915B0FFE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlot", "InkBlot\InkBlot.csproj", "{EDFCA854-0AF5-4C8F-8820-C328915B0FFE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlotTestConsoleApp", "InkBlotTestConsoleApp\InkBlotTestConsoleApp.csproj", "{1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlotTestConsoleApp", "InkBlotTestConsoleApp\InkBlotTestConsoleApp.csproj", "{1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InkBlot.Tests", "InkBlot.Tests\InkBlot.Tests.csproj", "{1B83421E-8A2D-4862-9E84-D8E5EDE13264}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{1BD86CDC-A93C-44EE-AA6B-E87E57FBC8AE}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

3
InkBlot/Assembly.cs Normal file
View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("InkBlot.Tests")]

View File

@@ -1,5 +0,0 @@
namespace InkBlot;
public class Class1
{
}

5
InkBlot/Error.cs Normal file
View File

@@ -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);

View File

@@ -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]

View File

@@ -0,0 +1,2 @@
Whitespace=1
CONTENT_TEXT_NO_ESCAPE_SIMPLE=2

View File

@@ -0,0 +1,75 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
// 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;
/// <summary>
/// This class provides an empty implementation of <see cref="IInkBlotAntlrGrammarListener"/>,
/// which can be extended to create a listener which only needs to handle a subset
/// of the available methods.
/// </summary>
[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")]
[System.Diagnostics.DebuggerNonUserCode]
[System.CLSCompliant(false)]
public partial class InkBlotAntlrGrammarBaseListener : IInkBlotAntlrGrammarListener {
/// <summary>
/// Enter a parse tree produced by <see cref="InkBlotAntlrGrammarParser.story"/>.
/// <para>The default implementation does nothing.</para>
/// </summary>
/// <param name="context">The parse tree.</param>
public virtual void EnterStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context) { }
/// <summary>
/// Exit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.story"/>.
/// <para>The default implementation does nothing.</para>
/// </summary>
/// <param name="context">The parse tree.</param>
public virtual void ExitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context) { }
/// <summary>
/// Enter a parse tree produced by <see cref="InkBlotAntlrGrammarParser.contentText"/>.
/// <para>The default implementation does nothing.</para>
/// </summary>
/// <param name="context">The parse tree.</param>
public virtual void EnterContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context) { }
/// <summary>
/// Exit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.contentText"/>.
/// <para>The default implementation does nothing.</para>
/// </summary>
/// <param name="context">The parse tree.</param>
public virtual void ExitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context) { }
/// <inheritdoc/>
/// <remarks>The default implementation does nothing.</remarks>
public virtual void EnterEveryRule([NotNull] ParserRuleContext context) { }
/// <inheritdoc/>
/// <remarks>The default implementation does nothing.</remarks>
public virtual void ExitEveryRule([NotNull] ParserRuleContext context) { }
/// <inheritdoc/>
/// <remarks>The default implementation does nothing.</remarks>
public virtual void VisitTerminal([NotNull] ITerminalNode node) { }
/// <inheritdoc/>
/// <remarks>The default implementation does nothing.</remarks>
public virtual void VisitErrorNode([NotNull] IErrorNode node) { }
}

View File

@@ -0,0 +1,57 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
// 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;
/// <summary>
/// This class provides an empty implementation of <see cref="IInkBlotAntlrGrammarVisitor{Result}"/>,
/// which can be extended to create a visitor which only needs to handle a subset
/// of the available methods.
/// </summary>
/// <typeparam name="Result">The return type of the visit operation.</typeparam>
[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")]
[System.Diagnostics.DebuggerNonUserCode]
[System.CLSCompliant(false)]
public partial class InkBlotAntlrGrammarBaseVisitor<Result> : AbstractParseTreeVisitor<Result>, IInkBlotAntlrGrammarVisitor<Result> {
/// <summary>
/// Visit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.story"/>.
/// <para>
/// The default implementation returns the result of calling <see cref="AbstractParseTreeVisitor{Result}.VisitChildren(IRuleNode)"/>
/// on <paramref name="context"/>.
/// </para>
/// </summary>
/// <param name="context">The parse tree.</param>
/// <return>The visitor result.</return>
public virtual Result VisitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context) { return VisitChildren(context); }
/// <summary>
/// Visit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.contentText"/>.
/// <para>
/// The default implementation returns the result of calling <see cref="AbstractParseTreeVisitor{Result}.VisitChildren(IRuleNode)"/>
/// on <paramref name="context"/>.
/// </para>
/// </summary>
/// <param name="context">The parse tree.</param>
/// <return>The visitor result.</return>
public virtual Result VisitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context) { return VisitChildren(context); }
}

View File

@@ -0,0 +1,120 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
// 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);
}

View File

@@ -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]

View File

@@ -0,0 +1,2 @@
Whitespace=1
CONTENT_TEXT_NO_ESCAPE_SIMPLE=2

View File

@@ -0,0 +1,53 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
// 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;
/// <summary>
/// This interface defines a complete listener for a parse tree produced by
/// <see cref="InkBlotAntlrGrammarParser"/>.
/// </summary>
[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")]
[System.CLSCompliant(false)]
public interface IInkBlotAntlrGrammarListener : IParseTreeListener {
/// <summary>
/// Enter a parse tree produced by <see cref="InkBlotAntlrGrammarParser.story"/>.
/// </summary>
/// <param name="context">The parse tree.</param>
void EnterStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context);
/// <summary>
/// Exit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.story"/>.
/// </summary>
/// <param name="context">The parse tree.</param>
void ExitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context);
/// <summary>
/// Enter a parse tree produced by <see cref="InkBlotAntlrGrammarParser.contentText"/>.
/// </summary>
/// <param name="context">The parse tree.</param>
void EnterContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context);
/// <summary>
/// Exit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.contentText"/>.
/// </summary>
/// <param name="context">The parse tree.</param>
void ExitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context);
}

View File

@@ -0,0 +1,205 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
// 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<ContentTextContext>();
}
[System.Diagnostics.DebuggerNonUserCode] public ContentTextContext contentText(int i) {
return GetRuleContext<ContentTextContext>(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<TResult>(IParseTreeVisitor<TResult> visitor) {
IInkBlotAntlrGrammarVisitor<TResult> typedVisitor = visitor as IInkBlotAntlrGrammarVisitor<TResult>;
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<TResult>(IParseTreeVisitor<TResult> visitor) {
IInkBlotAntlrGrammarVisitor<TResult> typedVisitor = visitor as IInkBlotAntlrGrammarVisitor<TResult>;
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);
}

View File

@@ -0,0 +1,46 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
// 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;
/// <summary>
/// This interface defines a complete generic visitor for a parse tree produced
/// by <see cref="InkBlotAntlrGrammarParser"/>.
/// </summary>
/// <typeparam name="Result">The return type of the visit operation.</typeparam>
[System.CodeDom.Compiler.GeneratedCode("ANTLR", "4.13.2")]
[System.CLSCompliant(false)]
public interface IInkBlotAntlrGrammarVisitor<Result> : IParseTreeVisitor<Result> {
/// <summary>
/// Visit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.story"/>.
/// </summary>
/// <param name="context">The parse tree.</param>
/// <return>The visitor result.</return>
Result VisitStory([NotNull] InkBlotAntlrGrammarParser.StoryContext context);
/// <summary>
/// Visit a parse tree produced by <see cref="InkBlotAntlrGrammarParser.contentText"/>.
/// </summary>
/// <param name="context">The parse tree.</param>
/// <return>The visitor result.</return>
Result VisitContentText([NotNull] InkBlotAntlrGrammarParser.ContentTextContext context);
}

17
InkBlot/IFileReader.cs Normal file
View File

@@ -0,0 +1,17 @@
namespace InkBlot;
/// <summary>
/// An object that can read files.
/// </summary>
public interface IFileReader
{
/// <summary>
/// Get the contents of a file.
/// </summary>
/// <param name="filename">
/// 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).
/// </param>
/// <returns>A stream with the contents of the file.</returns>
Stream GetContents(string filename);
}

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Antlr4.Runtime.Standard" Version="4.13.1"/>
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,7 @@
grammar InkBlotAntlrGrammar;
import InkBlotAntlrLexer;
story: contentText+ ;
contentText: CONTENT_TEXT_NO_ESCAPE_SIMPLE ;

View File

@@ -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) != '>' }?
)+ ;

34
InkBlot/InkBlotParser.cs Normal file
View File

@@ -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<Diagnostic>) 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));
}
}

View File

@@ -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<int>
{
public readonly List<Diagnostic> 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, ""));
}
}

View File

@@ -0,0 +1,3 @@
namespace InkBlot.ParseHierarchy;
public record Content(string Text) : StoryNode;

View File

@@ -0,0 +1,3 @@
namespace InkBlot.ParseHierarchy;
public record Story(IEnumerable<StoryNode> StoryNodes) : StoryNode;

View File

@@ -0,0 +1,6 @@
namespace InkBlot.ParseHierarchy;
/// <summary>
/// Any node in the parsed story
/// </summary>
public record StoryNode;

View File

@@ -0,0 +1,58 @@
using System.Text;
using Antlr4.Runtime;
namespace InkBlot;
internal sealed class ParserErrorListener : BaseErrorListener
{
public readonly List<Diagnostic> 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);
}
}

View File

@@ -0,0 +1,5 @@
namespace InkBlot.Visitor;
public partial class Listener : InkBlotAntlrGrammarBaseListener
{
}

View File

@@ -0,0 +1,34 @@
using System.Text.RegularExpressions;
using Antlr4.Runtime.Tree;
namespace InkBlot.Visitor;
public partial class Listener
{
private readonly ParseTreeProperty<string> _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);
}
}

View File

@@ -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);
}
}

View File

@@ -7,4 +7,8 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\InkBlot\InkBlot.csproj"/>
</ItemGroup>
</Project> </Project>

View File

@@ -1,3 +1,19 @@
// See https://aka.ms/new-console-template for more information // See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!"); using System.Text;
using InkBlot;
var parser = new InkBlotParser();
parser.Parse(new PreMadeFileReader(new Dictionary<string, string>
{
{ "main.ink", "Hel-\\lo!" }
}), "main.ink");
internal class PreMadeFileReader(Dictionary<string, string> filesToContents) : IFileReader
{
public Stream GetContents(string filename)
{
return new MemoryStream(Encoding.UTF8.GetBytes(filesToContents[filename]));
}
}