diff --git a/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj b/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj index f452ba8..8d4a2aa 100644 --- a/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj +++ b/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj @@ -51,7 +51,7 @@ - + True True diff --git a/Ficdown.Parser.Tests/IntegrationTests.cs b/Ficdown.Parser.Tests/IntegrationTests.cs index 5ab902a..d225f3a 100644 --- a/Ficdown.Parser.Tests/IntegrationTests.cs +++ b/Ficdown.Parser.Tests/IntegrationTests.cs @@ -1,9 +1,8 @@ namespace Ficdown.Parser.Tests { - using System; using System.Linq; using System.Text; - using ServiceStack.Text; + using Player; using TestStories; using Xunit; @@ -18,7 +17,9 @@ Assert.NotNull(story); Assert.Equal("The Robot King", story.Name); Assert.Equal("Robot Cave", story.Scenes[story.FirstScene].First().Name); - Console.WriteLine(story.Dump()); + + var player = new GameTraverser(); + player.ExportStaticStory(story, @"C:\Users\Rudis\Desktop\template.html", @"C:\Users\Rudis\Desktop\output"); } } } diff --git a/Ficdown.Parser.Tests/SceneLinkerTests.cs b/Ficdown.Parser.Tests/SceneLinkerTests.cs deleted file mode 100644 index 18585c8..0000000 --- a/Ficdown.Parser.Tests/SceneLinkerTests.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace Ficdown.Parser.Tests -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Engine; - using Model.Story; - using ServiceStack.Text; - using Xunit; - - public class SceneLinkerTests - { - private Story MockStoryWithScenes(IEnumerable scenes) - { - var sceneDict = new Dictionary>(); - foreach (var scene in scenes) - { - var key = Utilities.NormalizeString(scene.Name); - if(!sceneDict.ContainsKey(key)) sceneDict.Add(key, new List()); - sceneDict[key].Add(scene); - } - return new Story - { - Name = "Test Story", - Description = "Story description.", - FirstScene = sceneDict.First().Key, - Scenes = sceneDict - }; - } - - [Fact] - public void ConditionalAnchorGetsReplacedCorrectly() - { - var sl = new SceneLinker(); - var story = MockStoryWithScenes(new[] - { - new Scene - { - Name = "Test Scene", - Description = "Test [passed|failed](?test-condition) text." - } - }); - sl.ExpandScenes(story); - Console.WriteLine(story.Dump()); - Assert.Equal(2, story.Scenes["test-scene"].Count); - Scene passed = null, failed = null; - Assert.DoesNotThrow(() => - passed = - story.Scenes["test-scene"].SingleOrDefault( - s => - s.Conditions != null && s.Conditions.Contains("test-condition") && - s.Description.Equals("Test passed text."))); - Assert.DoesNotThrow(() => - failed = - story.Scenes["test-scene"].SingleOrDefault( - s => s.Conditions == null && s.Description.Equals("Test failed text."))); - Assert.NotNull(passed); - Assert.NotNull(failed); - } - - [Fact] - public void MultipleConditionalAnchorsGetReplacedCorrectly() - { - var sl = new SceneLinker(); - var story = MockStoryWithScenes(new[] - { - new Scene - { - Name = "Test Scene", - Description = - "Test1 [passed1|failed1](?test1-condition). Test2 [passed2|failed2](?test2-condition)." - } - }); - sl.ExpandScenes(story); - Console.WriteLine(story.Dump()); - Assert.Equal(4, story.Scenes["test-scene"].Count); - Assert.Equal( - story.Scenes["test-scene"].Select(s => s.Conditions == null ? null : s.Conditions.ToArray()).ToArray(), - new[] - { - null, new[] {"test1-condition"}, new[] {"test2-condition"}, - new[] {"test1-condition", "test2-condition"} - }); - Assert.False( - story.Scenes["test-scene"].Any( - s => - s.Conditions != null && s.Conditions.Contains("test1-condition") && - s.Description.Contains("Test1 failed1."))); - Assert.False( - story.Scenes["test-scene"].Any( - s => - s.Conditions != null && s.Conditions.Contains("test2-condition") && - s.Description.Contains("Test2 failed2."))); - } - } -} diff --git a/Ficdown.Parser.Tests/StateResolverTests.cs b/Ficdown.Parser.Tests/StateResolverTests.cs new file mode 100644 index 0000000..bc609ab --- /dev/null +++ b/Ficdown.Parser.Tests/StateResolverTests.cs @@ -0,0 +1,6 @@ +namespace Ficdown.Parser.Tests +{ + public class StateResolverTests + { + } +} diff --git a/Ficdown.Parser.Tests/TestStories/TheRobotKing.md b/Ficdown.Parser.Tests/TestStories/TheRobotKing.md index 6cc29a9..b3f95b8 100644 --- a/Ficdown.Parser.Tests/TestStories/TheRobotKing.md +++ b/Ficdown.Parser.Tests/TestStories/TheRobotKing.md @@ -109,7 +109,7 @@ There is a grand double staircase leading up to the throne room, a hallway strai You walk into the hall that leads to the living quarters, and find a gate blocking your way. There is a robot scanner installed on the gate. I guess it only opens for robots who live or work here. Maybe the Master Janitor Robot will have a way for you to get through. -- [Go back to the palace entrance.](/palace-entrance#tried-gate) +[Go back to the palace entrance.](/palace-entrance#tried-gate) ## Palace Basement @@ -149,19 +149,21 @@ He walks to a box in the corner and pulls out a blue janitor's uniform, then han The Master Janitor Robot scratches his chin for a moment, then resumes pacing back and forth and muttering to himself. -## [Living Quarters](?) +## [Living Quarters](?uniform) You head into the hallway that leads to the living quarters and come to a large gate. A scanner attached to the gate lights up and beeps a few times. After a moment, you hear a click and a soft hiss as the gate opens to let you pass. Once you walk through, the gate hisses and clicks shut behind you. You notice with some alarm that there's no scanner on the inside of the gate. You don't know how to get back out! -## [Living Quarters](?uniform) +[Continue...](/living-quarters-2) + +## [Living Quarters 2]("Living Quarters") That's when you realize that you never asked the Master Janitor Bot what your job here was. You just took your uniform and left! **You have failed to perform your new job because you never found out what it was.** -## [Living Quarters](?uniform&job-started) +## [Living Quarters 2](?job-started "Living Quarters") That's no problem though, because you already know what your job is. You continue down the hall, looking at and passing all of the doors until you come to the one marked with a "13." Right next to it is another door labeled "Janitor's Closet." diff --git a/Ficdown.Parser.Tests/UtilityTests.cs b/Ficdown.Parser.Tests/UtilityTests.cs index 1990800..678ec58 100644 --- a/Ficdown.Parser.Tests/UtilityTests.cs +++ b/Ficdown.Parser.Tests/UtilityTests.cs @@ -1,11 +1,28 @@ namespace Ficdown.Parser.Tests { - using System.Collections.Generic; - using Engine; + using System; + using Parser; using Xunit; public class UtilityTests { + [Fact] + public void FullAnchorMatches() + { + Console.WriteLine(RegexLib.Href.ToString()); + var anchorStr = @"[Link text](/target-scene)"; + var anchor = RegexLib.Anchors.Match(anchorStr); + Assert.Equal(anchorStr, anchor.Groups["anchor"].Value); + + anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")"; + anchor = RegexLib.Anchors.Match(anchorStr); + Assert.Equal(anchorStr, anchor.Groups["anchor"].Value); + + anchorStr = @"[Link text](""Title text"")"; + anchor = RegexLib.Anchors.Match(anchorStr); + Assert.Equal(anchorStr, anchor.Groups["anchor"].Value); + } + [Fact] public void AnchorWithTargetMatches() { @@ -46,15 +63,30 @@ Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("#toggle-1+toggle-2", anchor.Groups["href"].Value); + + anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-1+!toggle-2)"); + Assert.True(anchor.Success); + Assert.Equal("Link text", anchor.Groups["text"].Value); + Assert.Equal("#toggle-1+!toggle-2", anchor.Groups["href"].Value); + } + + [Fact] + public void AnchorsWithTitlesMatch() + { + var anchor = RegexLib.Anchors.Match(@"[Link text](""Title text"")"); + Assert.True(anchor.Success); + Assert.Equal("Link text", anchor.Groups["text"].Value); + Assert.Equal("Title text", anchor.Groups["title"].Value); } [Fact] public void ComplexAnchorsMatch() { - var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state#toggle-state)"); + var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")"); Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Groups["href"].Value); + Assert.Equal("Title text", anchor.Groups["title"].Value); anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene#toggle-state)"); Assert.True(anchor.Success); @@ -75,84 +107,76 @@ [Fact] public void HrefWithTargetParses() { - string target; - IList conditions, toggles; - Utilities.ParseHref("/target-scene", out target, out conditions, out toggles); - Assert.Equal("target-scene", target); - Assert.Null(conditions); - Assert.Null(toggles); + var anchors = Utilities.ParseAnchors("[Anchor](/target-scene)"); + Assert.Equal("target-scene", anchors[0].Href.Target); + Assert.Null(anchors[0].Href.Conditions); + Assert.Null(anchors[0].Href.Toggles); } [Fact] public void HrefsWithConditionsParse() { - string target; - IList conditions, toggles; - Utilities.ParseHref("?condition-state", out target, out conditions, out toggles); - Assert.Null(target); - Assert.Equal(1, conditions.Count); - Assert.Contains("condition-state", conditions); - Assert.Null(toggles); + var anchors = Utilities.ParseAnchors("[Anchor](?condition-state)"); + Assert.Null(anchors[0].Href.Target); + Assert.Equal(1, anchors[0].Href.Conditions.Count); + Assert.True(anchors[0].Href.Conditions["condition-state"]); + Assert.Null(anchors[0].Href.Toggles); - Utilities.ParseHref("?condition-1&condition-2", out target, out conditions, out toggles); - Assert.Null(target); - Assert.Equal(2, conditions.Count); - Assert.Contains("condition-1", conditions); - Assert.Contains("condition-2", conditions); - Assert.Null(toggles); + anchors = Utilities.ParseAnchors("[Anchor](?condition-1&!condition-2)"); + Assert.Null(anchors[0].Href.Target); + Assert.Equal(2, anchors[0].Href.Conditions.Count); + Assert.True(anchors[0].Href.Conditions["condition-1"]); + Assert.False(anchors[0].Href.Conditions["condition-2"]); + Assert.Null(anchors[0].Href.Toggles); } [Fact] public void HrefsWithTogglesParse() { - string target; - IList conditions, toggles; - Utilities.ParseHref("#toggle-state", out target, out conditions, out toggles); - Assert.Null(target); - Assert.Null(conditions); - Assert.Equal(1, toggles.Count); - Assert.Contains("toggle-state", toggles); + var anchors = Utilities.ParseAnchors("[Anchor](#toggle-state)"); + Assert.Null(anchors[0].Href.Target); + Assert.Null(anchors[0].Href.Conditions); + Assert.Equal(1, anchors[0].Href.Toggles.Count); + Assert.True(anchors[0].Href.Toggles["toggle-state"]); - Utilities.ParseHref("#toggle-1+toggle-2", out target, out conditions, out toggles); - Assert.Null(target); - Assert.Null(conditions); - Assert.Equal(2, toggles.Count); - Assert.Contains("toggle-1", toggles); - Assert.Contains("toggle-2", toggles); + anchors = Utilities.ParseAnchors("[Anchor](#toggle-1+!toggle-2)"); + Assert.Null(anchors[0].Href.Target); + Assert.Null(anchors[0].Href.Conditions); + Assert.Equal(2, anchors[0].Href.Toggles.Count); + Assert.True(anchors[0].Href.Toggles["toggle-1"]); + Assert.False(anchors[0].Href.Toggles["toggle-2"]); } [Fact] public void ComplexHrefsParse() { - string target; - IList conditions, toggles; - Utilities.ParseHref("/target-scene?condition-state#toggle-state", out target, out conditions, out toggles); - Assert.Equal("target-scene", target); - Assert.Equal(1, conditions.Count); - Assert.Contains("condition-state", conditions); - Assert.Equal(1, toggles.Count); - Assert.Contains("toggle-state", toggles); + var anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state#toggle-state)"); + Assert.Equal("target-scene", anchors[0].Href.Target); + Assert.Equal(1, anchors[0].Href.Conditions.Count); + Assert.True(anchors[0].Href.Conditions["condition-state"]); + Assert.Equal(1, anchors[0].Href.Toggles.Count); + Assert.True(anchors[0].Href.Toggles["toggle-state"]); - Utilities.ParseHref("/target-scene?condition-state", out target, out conditions, out toggles); - Assert.Equal("target-scene", target); - Assert.Equal(1, conditions.Count); - Assert.Contains("condition-state", conditions); - Assert.Null(toggles); + anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state)"); + Assert.Equal("target-scene", anchors[0].Href.Target); + Assert.Equal(1, anchors[0].Href.Conditions.Count); + Assert.True(anchors[0].Href.Conditions["condition-state"]); + Assert.Null(anchors[0].Href.Toggles); - Utilities.ParseHref("/target-scene#toggle-state", out target, out conditions, out toggles); - Assert.Equal("target-scene", target); - Assert.Null(conditions); - Assert.Equal(1, toggles.Count); - Assert.Contains("toggle-state", toggles); + anchors = Utilities.ParseAnchors("[Anchor](/target-scene#toggle-state)"); + Assert.Equal("target-scene", anchors[0].Href.Target); + Assert.Null(anchors[0].Href.Conditions); + Assert.Equal(1, anchors[0].Href.Toggles.Count); + Assert.True(anchors[0].Href.Toggles["toggle-state"]); - Utilities.ParseHref("?condition-one&condition-two#toggle-one+toggle-two", out target, out conditions, out toggles); - Assert.Null(target); - Assert.Equal(2, conditions.Count); - Assert.Contains("condition-one", conditions); - Assert.Contains("condition-two", conditions); - Assert.Equal(2, toggles.Count); - Assert.Contains("toggle-one", toggles); - Assert.Contains("toggle-two", toggles); + anchors = Utilities.ParseAnchors("[Anchor](?condition-one&!condition-two#toggle-one+!toggle-two)"); + Assert.Null(anchors[0].Href.Target); + Assert.Equal(2, anchors[0].Href.Conditions.Count); + Assert.True(anchors[0].Href.Conditions["condition-one"]); + Assert.False(anchors[0].Href.Conditions["condition-two"]); + Assert.Equal(2, anchors[0].Href.Toggles.Count); + Assert.True(anchors[0].Href.Toggles["toggle-one"]); + Assert.False(anchors[0].Href.Toggles["toggle-two"]); } } } diff --git a/Ficdown.Parser/Engine/ISceneLinker.cs b/Ficdown.Parser/Engine/ISceneLinker.cs deleted file mode 100644 index e547b77..0000000 --- a/Ficdown.Parser/Engine/ISceneLinker.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Ficdown.Parser.Engine -{ - using Model.Story; - - public interface ISceneLinker - { - void ExpandScenes(Story story); - } -} diff --git a/Ficdown.Parser/Engine/SceneLinker.cs b/Ficdown.Parser/Engine/SceneLinker.cs deleted file mode 100644 index 68e9787..0000000 --- a/Ficdown.Parser/Engine/SceneLinker.cs +++ /dev/null @@ -1,126 +0,0 @@ -namespace Ficdown.Parser.Engine -{ - using System; - using System.Collections.Generic; - using System.Collections.Specialized; - using System.Linq; - using System.Text.RegularExpressions; - using Model.Story; - using Model.Story.Extensions; - - public class SceneLinker : ISceneLinker - { - - public void ExpandScenes(Story story) - { - VerifySanity(story); - - var newScenes = new Dictionary>(); - foreach(var key in story.Scenes.Keys) - { - newScenes.Add(key, new List()); - foreach (var scene in story.Scenes[key]) - { - var anchors = RegexLib.Anchors.Matches(scene.Description); - // get a list of all unique condition combinations from the anchors - var uniques = new List>(); - foreach (Match anchor in anchors) - { - string target; - IList conditions, toggles; - Utilities.ParseHref(anchor.Groups["href"].Value, out target, out conditions, out toggles); - if (conditions != null) - { - // union with the conditions required to reach this scene, if any - if (scene.Conditions != null) - { - conditions = conditions.Union(scene.Conditions).ToList(); - if (conditions.Count == scene.Conditions.Count) - continue; //WARN this anchor will never resolve false - } - - AddUnique(uniques, conditions); - } - } - // resolve the current scene - var original = scene.Clone(); - newScenes[key].Add(ResolveScene(scene, anchors)); - // resolve the uniques - foreach (var unique in uniques) - { - var uscene = original.Clone(); - uscene.Conditions = unique; - newScenes[key].Add(ResolveScene(uscene, anchors)); - } - } - } - story.Scenes = newScenes; - } - - private void AddUnique(IList> uniques, IList conditions) - { - // ignore this combo if there's a contradiction - if (conditions.Where(c => !c.StartsWith("!")) - .Any(c => conditions.Contains(string.Format("!{0}", c)))) - return; // WARN this anchor will never resolve true - - // make sure this is actually unique - if (uniques.Any(u => u.Intersect(conditions).Count() == conditions.Count)) return; - - uniques.Add(conditions); - - // we need to treat this unioned with all other existing uniques as another potential unique - var existing = new List>(uniques); - foreach (var old in existing) - { - AddUnique(uniques, old.Union(conditions).ToList()); - } - } - - private Scene ResolveScene(Scene scene, MatchCollection anchors) - { - foreach (Match anchor in anchors) - { - string target; - IList conditions, toggles; - Utilities.ParseHref(anchor.Groups["href"].Value, out target, out conditions, out toggles); - if (conditions != null) - { - var satisfied = scene.Conditions != null && conditions.All(c => scene.Conditions.Contains(c)); - - var text = anchor.Groups["text"].Value; - var alts = RegexLib.ConditionalText.Match(text); - if (!alts.Success) - throw new FormatException(string.Format("Bad conditional anchor: {0}", - anchor.Groups["anchor"].Value)); - - var replace = - RegexLib.EscapeChar.Replace(satisfied ? alts.Groups["true"].Value : alts.Groups["false"].Value, - string.Empty); - - // if there's no target or toggles, or the replace text is an empty string, replace the whole anchor - if (string.IsNullOrEmpty(replace) || (target == null && toggles == null)) - { - scene.Description = scene.Description.Replace(anchor.Groups["anchor"].Value, replace); - } - // if there's a target or toggles, replace the text and remove the conditions on the anchor - else - { - var parsedHref = RegexLib.Href.Match(anchor.Groups["href"].Value); - scene.Description = scene.Description.Replace(anchor.Groups["anchor"].Value, - string.Format("[{0}]({1}{2})", replace, parsedHref.Groups["target"].Value, - parsedHref.Groups["toggles"].Value)); - } - - } - - } - return scene; - } - - private void VerifySanity(Story story) - { - - } - } -} diff --git a/Ficdown.Parser/Engine/Utilities.cs b/Ficdown.Parser/Engine/Utilities.cs deleted file mode 100644 index 6eb6768..0000000 --- a/Ficdown.Parser/Engine/Utilities.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")] - -namespace Ficdown.Parser.Engine -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text.RegularExpressions; - - internal static class Utilities - { - public static string NormalizeString(string raw) - { - return Regex.Replace(Regex.Replace(raw.ToLower(), @"^\W+|\W+$", string.Empty), @"\W+", "-"); - } - - public static void ParseHref(string href, out string target) - { - IList conditions, toggles; - ParseHref(href, out target, out conditions, out toggles); - if(conditions != null || toggles != null) throw new FormatException(); - } - - public static void ParseHref(string href, out IList conditions) - { - string target; - IList toggles; - ParseHref(href, out target, out conditions, out toggles); - if(target != null || toggles != null) throw new FormatException(); - } - - public static void ParseHref(string href, out string target, out IList conditions, out IList toggles) - { - target = null; - conditions = null; - toggles = null; - var match = RegexLib.Href.Match(href); - if (match.Success) - { - var ttstr = match.Groups["target"].Value; - var cstr = match.Groups["conditions"].Value; - var tstr = match.Groups["toggles"].Value; - if (!string.IsNullOrEmpty(ttstr)) - target = ttstr.TrimStart('/'); - if (!string.IsNullOrEmpty(cstr)) - conditions = new List(cstr.TrimStart('?').Split('&').Select(c => c.Trim().ToLower())); - if (!string.IsNullOrEmpty(tstr)) - toggles = new List(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower())); - } - else throw new FormatException(string.Format("Invalid href: {0}", href)); - } - } -} diff --git a/Ficdown.Parser/FicDownParser.cs b/Ficdown.Parser/FicDownParser.cs index b17247e..86a024a 100644 --- a/Ficdown.Parser/FicDownParser.cs +++ b/Ficdown.Parser/FicDownParser.cs @@ -1,8 +1,8 @@ namespace Ficdown.Parser { using System; - using Engine; using Model.Story; + using Parser; public class FicdownParser { @@ -13,12 +13,12 @@ set { _blockHandler = value; } } - private ISceneLinker _sceneLinker; + private IStateResolver _stateResolver; - public ISceneLinker SceneLinker + public IStateResolver StateResolver { - get { return _sceneLinker ?? (_sceneLinker = new SceneLinker()); } - set { _sceneLinker = value; } + get { return _stateResolver ?? (_stateResolver = new StateResolver()); } + set { _stateResolver = value; } } public Story ParseStory(string storyText) @@ -26,7 +26,6 @@ var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None); var blocks = BlockHandler.ExtractBlocks(lines); var story = BlockHandler.ParseBlocks(blocks); - SceneLinker.ExpandScenes(story); return story; } } diff --git a/Ficdown.Parser/Ficdown.Parser.csproj b/Ficdown.Parser/Ficdown.Parser.csproj index 9a87871..f211544 100644 --- a/Ficdown.Parser/Ficdown.Parser.csproj +++ b/Ficdown.Parser/Ficdown.Parser.csproj @@ -30,6 +30,9 @@ 4 + + ..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll + @@ -39,21 +42,30 @@ - - - - - + + + + + + + + - + + + + + + +