diff --git a/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj b/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj index e95d607..1bf08cc 100644 --- a/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj +++ b/Ficdown.Parser.Tests/Ficdown.Parser.Tests.csproj @@ -49,6 +49,7 @@ + True diff --git a/Ficdown.Parser.Tests/IntegrationTests.cs b/Ficdown.Parser.Tests/IntegrationTests.cs new file mode 100644 index 0000000..500355f --- /dev/null +++ b/Ficdown.Parser.Tests/IntegrationTests.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ficdown.Parser.Tests +{ + class IntegrationTests + { + } +} diff --git a/Ficdown.Parser/Engine/BlockHandler.cs b/Ficdown.Parser/Engine/BlockHandler.cs index dced07c..bba148a 100644 --- a/Ficdown.Parser/Engine/BlockHandler.cs +++ b/Ficdown.Parser/Engine/BlockHandler.cs @@ -42,8 +42,19 @@ var storyBlock = blocks.Single(b => b.Type == BlockType.Story); var storyName = RegexLib.Anchors.Match(storyBlock.Name); + string storyTarget; + try + { + Utilities.ParseHref(storyName.Groups["href"].Value, out storyTarget); + } + catch (FormatException) + { + throw new FormatException(string.Format("Story href should only have target: {0}", + storyName.Groups["href"].Value)); + } + if (!storyName.Success) - throw new FormatException("Story name must be a link to the first scene."); + throw new FormatException("Story name must link to the first scene."); var story = new Story { @@ -53,7 +64,7 @@ States = new Dictionary>() }; - var scenes = blocks.Where(b => b.Type == BlockType.Scene).Select(b => BlockToScene(b)); + var scenes = blocks.Where(b => b.Type == BlockType.Scene).Select(BlockToScene); foreach (var scene in scenes) { var key = Utilities.NormalizeString(scene.Name); @@ -61,21 +72,18 @@ story.Scenes[key].Add(scene); } + if (!story.Scenes.ContainsKey(storyTarget)) + throw new FormatException(string.Format("Story targets non-existent scene: {0}", storyTarget)); + story.FirstScene = + story.Scenes[storyTarget].SingleOrDefault( + s => s.Conditions == null || s.Conditions.All(c => c.StartsWith("!"))); + if (story.FirstScene == null) + throw new FormatException(string.Format("Story targets scene with no unconditional definition: {0}", + storyTarget)); + return story; } - private void ParseHref(string href, out IList conditions, out IList toggles) - { - var match = RegexLib.Href.Match(href); - if (match.Success) - { - var cstr = match.Groups["conditions"].Value; - var tstr = match.Groups["toggles"].Value; - } - else throw new FormatException(string.Format("Invalid href: {0}", href)); - conditions = null; - toggles = null; - } private Scene BlockToScene(Block block) { @@ -88,10 +96,16 @@ if (sceneName.Success) { scene.Name = sceneName.Groups["text"].Value; - IList conditions, toggles; - ParseHref(sceneName.Groups["href"].Value, out conditions, out toggles); + IList conditions; + try + { + Utilities.ParseHref(sceneName.Groups["href"].Value, out conditions); + } + catch (FormatException) + { + throw new FormatException(string.Format("Scene href should only have conditions: {0}", block.Name)); + } scene.Conditions = conditions; - scene.Toggles = toggles; } else scene.Name = block.Name; diff --git a/Ficdown.Parser/Engine/ISceneLinker.cs b/Ficdown.Parser/Engine/ISceneLinker.cs new file mode 100644 index 0000000..e547b77 --- /dev/null +++ b/Ficdown.Parser/Engine/ISceneLinker.cs @@ -0,0 +1,9 @@ +namespace Ficdown.Parser.Engine +{ + using Model.Story; + + public interface ISceneLinker + { + void ExpandScenes(Story story); + } +} diff --git a/Ficdown.Parser/Engine/RegexLib.cs b/Ficdown.Parser/Engine/RegexLib.cs index 0603ea0..88e21cd 100644 --- a/Ficdown.Parser/Engine/RegexLib.cs +++ b/Ficdown.Parser/Engine/RegexLib.cs @@ -10,7 +10,16 @@ string.Format(@"(?\[(?{0})\]\([ ]*(?{1})[ ]*\))", GetNestedBracketsPattern(), GetNestedParensPattern()), RegexOptions.Singleline | RegexOptions.Compiled); - public static Regex Href = new Regex(@"^(\?(?[^#]+))?(#(?.+))?$", RegexOptions.Compiled); + private const string RegexValidName = @"[a-zA-Z](-?[a-zA-Z0-9])*"; + private static readonly string RegexHrefTarget = string.Format(@"\/({0})", RegexValidName); + private static readonly string RegexHrefConditions = string.Format(@"\?((!?{0})(&!?{0})*)?", RegexValidName); + private static readonly string RegexHrefToggles = string.Format(@"#({0})(\+{0})*", RegexValidName); + + public static Regex Href = + new Regex( + string.Format(@"^(?{0})?(?{1})?(?{2})?$", RegexHrefTarget, + RegexHrefConditions, RegexHrefToggles), RegexOptions.Compiled); + private const int _nestDepth = 6; diff --git a/Ficdown.Parser/Engine/SceneLinker.cs b/Ficdown.Parser/Engine/SceneLinker.cs new file mode 100644 index 0000000..3747716 --- /dev/null +++ b/Ficdown.Parser/Engine/SceneLinker.cs @@ -0,0 +1,28 @@ +namespace Ficdown.Parser.Engine +{ + using System.Collections.Generic; + using System.Text.RegularExpressions; + using Model.Story; + + public class SceneLinker : ISceneLinker + { + public void ExpandScenes(Story 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); + foreach (Match anchor in anchors) + { + string target; + IList conditions, toggles; + Utilities.ParseHref(anchor.Groups["href"].Value, out target, out conditions, out toggles); + } + } + } + } + } +} diff --git a/Ficdown.Parser/Engine/Utilities.cs b/Ficdown.Parser/Engine/Utilities.cs index 50c3c51..da7e58b 100644 --- a/Ficdown.Parser/Engine/Utilities.cs +++ b/Ficdown.Parser/Engine/Utilities.cs @@ -1,5 +1,8 @@ namespace Ficdown.Parser.Engine { + using System; + using System.Collections.Generic; + using System.Linq; using System.Text.RegularExpressions; internal static class Utilities @@ -8,5 +11,41 @@ { 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 4fc1016..44f37a1 100644 --- a/Ficdown.Parser/FicDownParser.cs +++ b/Ficdown.Parser/FicDownParser.cs @@ -7,13 +7,20 @@ public class FicdownParser { private IBlockHandler _blockHandler; - public IBlockHandler BlockHandler { get { return _blockHandler ?? (_blockHandler = new BlockHandler()); } set { _blockHandler = value; } } + private ISceneLinker _sceneLinker; + + public ISceneLinker SceneLinker + { + get { return _sceneLinker ?? (_sceneLinker = new SceneLinker()); } + set { _sceneLinker = value; } + } + public Story ParseStory(string storyFilePath) { var lines = File.ReadAllLines(storyFilePath); diff --git a/Ficdown.Parser/Ficdown.Parser.csproj b/Ficdown.Parser/Ficdown.Parser.csproj index d8203d3..9a87871 100644 --- a/Ficdown.Parser/Ficdown.Parser.csproj +++ b/Ficdown.Parser/Ficdown.Parser.csproj @@ -42,10 +42,13 @@ + + + diff --git a/Ficdown.Parser/Model/Story/Extensions/SceneExtensions.cs b/Ficdown.Parser/Model/Story/Extensions/SceneExtensions.cs new file mode 100644 index 0000000..cdb960d --- /dev/null +++ b/Ficdown.Parser/Model/Story/Extensions/SceneExtensions.cs @@ -0,0 +1,17 @@ +namespace Ficdown.Parser.Model.Story.Extensions +{ + using System.Collections.Generic; + + public static class SceneExtensions + { + public static Scene Clone(this Scene scene) + { + return new Scene + { + Name = scene.Name, + Description = scene.Description, + Conditions = new List(scene.Conditions) + }; + } + } +} diff --git a/Ficdown.Parser/Model/Story/Scene.cs b/Ficdown.Parser/Model/Story/Scene.cs index 49aae59..79feb97 100644 --- a/Ficdown.Parser/Model/Story/Scene.cs +++ b/Ficdown.Parser/Model/Story/Scene.cs @@ -7,6 +7,5 @@ public string Name { get; set; } public string Description { get; set; } public IList Conditions { get; set; } - public IList Toggles { get; set; } } -} +} \ No newline at end of file