diff --git a/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj b/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj
index 1bf08cc..8cc0931 100644
--- a/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj
+++ b/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj
@@ -51,11 +51,13 @@
+
True
True
Resources.resx
+
diff --git a/Ficdown.Parser.Tests/SceneLinkerTests.cs b/Ficdown.Parser.Tests/SceneLinkerTests.cs
new file mode 100644
index 0000000..0212e0d
--- /dev/null
+++ b/Ficdown.Parser.Tests/SceneLinkerTests.cs
@@ -0,0 +1,93 @@
+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);
+ 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 NegativeConditionalAnchorGetsReplacedCorrectly()
+ {
+ 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());
+ }
+
+ [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());
+ }
+ }
+}
diff --git a/Ficdown.Parser.Tests/TestStories/the-robot-king.md b/Ficdown.Parser.Tests/TestStories/the-robot-king.md
index 69c0585..6bb727a 100644
--- a/Ficdown.Parser.Tests/TestStories/the-robot-king.md
+++ b/Ficdown.Parser.Tests/TestStories/the-robot-king.md
@@ -15,8 +15,8 @@ Your cave only has one tiny window, and through it you can see [the sun shining
**What do you want to do?**
- [Go outside and start walking to the palace.](/outside)
-- [Wait for it to stop raining.](#stopped-raining)
-- [Put on your raincoat.](#raincoat)
+- [Wait for it to stop raining.](?!stopped-raining#stopped-raining)
+- [Put on your raincoat.](?!raincoat#raincoat)
### Raincoat
diff --git a/Ficdown.Parser.Tests/UtilityTests.cs b/Ficdown.Parser.Tests/UtilityTests.cs
new file mode 100644
index 0000000..5766598
--- /dev/null
+++ b/Ficdown.Parser.Tests/UtilityTests.cs
@@ -0,0 +1,163 @@
+namespace Ficdown.Parser.Tests
+{
+ using System.Collections.Generic;
+ using Engine;
+ using Xunit;
+
+ public class UtilityTests
+ {
+ [Fact]
+ public void AnchorWithTargetMatches()
+ {
+ var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("/target-scene", anchor.Groups["href"].Value);
+ }
+
+ [Fact]
+ public void AnchorsWithConditionsMatch()
+ {
+ var anchor = RegexLib.Anchors.Match(@"[Link text](?condition-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("?condition-state", anchor.Groups["href"].Value);
+
+ anchor = RegexLib.Anchors.Match(@"[Link text](?!condition-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("?!condition-state", anchor.Groups["href"].Value);
+
+ anchor = RegexLib.Anchors.Match(@"[Link text](?condition-1&!condition-2)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("?condition-1&!condition-2", anchor.Groups["href"].Value);
+ }
+
+ [Fact]
+ public void AnchorsWithTogglesMatch()
+ {
+ var anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("#toggle-state", 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 ComplexAnchorsMatch()
+ {
+ var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state#toggle-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Groups["href"].Value);
+
+ anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene#toggle-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("/target-scene#toggle-state", anchor.Groups["href"].Value);
+
+ anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("/target-scene?condition-state", anchor.Groups["href"].Value);
+
+ anchor = RegexLib.Anchors.Match(@"[Link text](?condition-state#toggle-state)");
+ Assert.True(anchor.Success);
+ Assert.Equal("Link text", anchor.Groups["text"].Value);
+ Assert.Equal("?condition-state#toggle-state", anchor.Groups["href"].Value);
+ }
+
+ [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);
+ }
+
+ [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);
+
+ 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);
+
+ 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);
+ }
+
+ [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);
+
+ 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);
+ }
+
+ [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);
+
+ 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);
+
+ 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);
+
+ Utilities.ParseHref("?!condition-one&condition-two#toggle-state", 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(1, toggles.Count);
+ Assert.Contains("toggle-state", toggles);
+ }
+ }
+}
diff --git a/Ficdown.Parser/Engine/RegexLib.cs b/Ficdown.Parser/Engine/RegexLib.cs
index 88e21cd..a995c6c 100644
--- a/Ficdown.Parser/Engine/RegexLib.cs
+++ b/Ficdown.Parser/Engine/RegexLib.cs
@@ -10,6 +10,11 @@
string.Format(@"(?\[(?{0})\]\([ ]*(?{1})[ ]*\))", GetNestedBracketsPattern(),
GetNestedParensPattern()), RegexOptions.Singleline | RegexOptions.Compiled);
+ public static Regex ConditionalText = new Regex(@"^(?([^|\\]|\\.)*)(\|(?([^|\\]|\\.)+))?$",
+ RegexOptions.Singleline | RegexOptions.Compiled);
+
+ public static Regex EscapeChar = new Regex(@"(? u.Intersect(conditions).Count() == conditions.Count)) return;
+
uniques.Add(conditions);
@@ -78,7 +82,42 @@
{
foreach (Match anchor in anchors)
{
- var satisfied = Utilities.ConditionsSatisfied(anchor.Groups["conditions"].Value, scene.Conditions);
+ 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 => c.StartsWith("!"))
+ : conditions.All(
+ c => scene.Conditions.Contains(c) ||
+ (c.StartsWith("!") && !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, replace the whole anchor
+ if (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
+ {
+ scene.Description = scene.Description.Replace(anchor.Groups["anchor"].Value,
+ string.Format("[{0}]({1}{2})", replace, anchor.Groups["target"].Value,
+ anchor.Groups["toggles"].Value));
+ }
+
+ }
+
}
return scene;
}
diff --git a/Ficdown.Parser/Engine/Utilities.cs b/Ficdown.Parser/Engine/Utilities.cs
index 7fc766e..6eb6768 100644
--- a/Ficdown.Parser/Engine/Utilities.cs
+++ b/Ficdown.Parser/Engine/Utilities.cs
@@ -1,4 +1,8 @@
-namespace Ficdown.Parser.Engine
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")]
+
+namespace Ficdown.Parser.Engine
{
using System;
using System.Collections.Generic;
@@ -47,10 +51,5 @@
}
else throw new FormatException(string.Format("Invalid href: {0}", href));
}
-
- public static bool ConditionsSatisfied(string cquery, IList conditions)
- {
- return false;
- }
}
}