From 4ff4693fa3cd84d13f736b1277a57eed5bd88e15 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Tue, 25 Sep 2018 16:29:53 -0500 Subject: [PATCH 1/4] improved error handling and reporting, plus lint mode for checking files for errors --- Ficdown.Console/Program.cs | 166 ++++++++++-------- Ficdown.Parser.Tests/UtilityTests.cs | 32 ++-- Ficdown.Parser/FicDownParser.cs | 17 ++ Ficdown.Parser/Model/Parser/Anchor.cs | 2 + .../Model/Parser/FicdownException.cs | 17 +- Ficdown.Parser/Model/Story/Action.cs | 1 + Ficdown.Parser/Model/Story/Scene.cs | 1 + Ficdown.Parser/Parser/BlockHandler.cs | 20 ++- Ficdown.Parser/Parser/ParserExtensions.cs | 1 - Ficdown.Parser/Parser/StateResolver.cs | 6 +- Ficdown.Parser/Parser/Utilities.cs | 54 ++++-- Ficdown.Parser/Player/GameTraverser.cs | 21 ++- Ficdown.Parser/Player/StateManager.cs | 26 ++- 13 files changed, 223 insertions(+), 141 deletions(-) diff --git a/Ficdown.Console/Program.cs b/Ficdown.Console/Program.cs index 37185bd..cd61c21 100644 --- a/Ficdown.Console/Program.cs +++ b/Ficdown.Console/Program.cs @@ -3,7 +3,6 @@ using System; using System.Linq; using System.IO; - using Microsoft.SqlServer.Server; using Parser; using Parser.Render; using Parser.Model.Parser; @@ -16,8 +15,7 @@ { if(e.ExceptionObject is FicdownException) { - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(e.ExceptionObject.ToString()); + Console.WriteLine(e.ExceptionObject.ToString()); Environment.Exit(3); } }; @@ -85,98 +83,114 @@ ShowHelp(); return 0; } - if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile)) - { - ShowHelp(); - return 1; - } - if (!File.Exists(infile)) - { - Console.WriteLine(@"Source file {0} not found.", infile); - return 2; - } - if (string.IsNullOrWhiteSpace(output)) - if (format == "html") - output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html"); - else if (format == "epub") - output = "output.epub"; - if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output))) + var lintMode = format == "lint"; + + if(!lintMode) { - Console.WriteLine(@"Specified output {0} already exists.", output); - return 2; - } - if (!string.IsNullOrWhiteSpace(tempdir)) - { - if (!Directory.Exists(tempdir)) + if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile)) { - Console.WriteLine(@"Template directory {0} does not exist.", tempdir); + ShowHelp(); + return 1; + } + if (!File.Exists(infile)) + { + Console.WriteLine(@"Source file {0} not found.", infile); return 2; } - if (!File.Exists(Path.Combine(tempdir, "index.html")) || - !File.Exists(Path.Combine(tempdir, "scene.html")) || - !File.Exists(Path.Combine(tempdir, "styles.css"))) - { - Console.WriteLine( - @"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files."); - } - } + if (string.IsNullOrWhiteSpace(output)) + if (format == "html") + output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html"); + else if (format == "epub") + output = "output.epub"; + else if(format == "lint") + lintMode = true; - if (!string.IsNullOrWhiteSpace(images) && !Directory.Exists(images)) - { - Console.WriteLine(@"Images directory {0} does not exist.", images); - return 2; + if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output))) + { + Console.WriteLine(@"Specified output {0} already exists.", output); + return 2; + } + if (!string.IsNullOrWhiteSpace(tempdir)) + { + if (!Directory.Exists(tempdir)) + { + Console.WriteLine(@"Template directory {0} does not exist.", tempdir); + return 2; + } + if (!File.Exists(Path.Combine(tempdir, "index.html")) || + !File.Exists(Path.Combine(tempdir, "scene.html")) || + !File.Exists(Path.Combine(tempdir, "styles.css"))) + { + Console.WriteLine( + @"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files."); + } + } + + if (!string.IsNullOrWhiteSpace(images) && !Directory.Exists(images)) + { + Console.WriteLine(@"Images directory {0} does not exist.", images); + return 2; + } } var parser = new FicdownParser(); - var storyText = File.ReadAllText(infile); - Console.WriteLine(@"Parsing story..."); + string storyText; + if(!lintMode) + { + storyText = File.ReadAllText(infile); + Console.WriteLine(@"Parsing story..."); + } + else + { + storyText = Console.In.ReadToEnd(); + } var story = parser.ParseStory(storyText); story.Orphans.ToList().ForEach(o => { - var currentColor = Console.ForegroundColor; - Console.ForegroundColor = ConsoleColor.Yellow; - Console.Error.WriteLine("Warning (line {0}): {1} {2} is unreachable", o.LineNumber, o.Type, o.Name); - Console.ForegroundColor = currentColor; + Console.WriteLine("Warning L{0},1: \"{1}\": Unreachable {2}", o.LineNumber, o.Name, o.Type); }); - IRenderer rend; - switch (format) + if(!lintMode) { - case "html": - Directory.CreateDirectory(output); - rend = new HtmlRenderer(language); - break; - case "epub": - if (string.IsNullOrWhiteSpace(author)) - { - Console.WriteLine(@"Epub format requires the --author argument."); + IRenderer rend; + switch (format) + { + case "html": + Directory.CreateDirectory(output); + rend = new HtmlRenderer(language); + break; + case "epub": + if (string.IsNullOrWhiteSpace(author)) + { + Console.WriteLine(@"Epub format requires the --author argument."); + return 1; + } + rend = new EpubRenderer(author, bookid, language); + break; + default: + ShowHelp(); return 1; - } - rend = new EpubRenderer(author, bookid, language); - break; - default: - ShowHelp(); - return 1; + } + + if (!string.IsNullOrWhiteSpace(tempdir)) + { + rend.IndexTemplate = File.ReadAllText(Path.Combine(tempdir, "index.html")); + rend.SceneTemplate = File.ReadAllText(Path.Combine(tempdir, "scene.html")); + rend.StylesTemplate = File.ReadAllText(Path.Combine(tempdir, "styles.css")); + }; + + if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images; + + Console.WriteLine(@"Rendering story..."); + + rend.Render(story, output, debug); + + Console.WriteLine(@"Done."); } - - if (!string.IsNullOrWhiteSpace(tempdir)) - { - rend.IndexTemplate = File.ReadAllText(Path.Combine(tempdir, "index.html")); - rend.SceneTemplate = File.ReadAllText(Path.Combine(tempdir, "scene.html")); - rend.StylesTemplate = File.ReadAllText(Path.Combine(tempdir, "styles.css")); - }; - - if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images; - - Console.WriteLine(@"Rendering story..."); - - rend.Render(story, output, debug); - - Console.WriteLine(@"Done."); return 0; } @@ -185,7 +199,7 @@ { Console.WriteLine( @"Usage: ficdown.exe - --format (html|epub) + --format (html|epub|lint) --in ""/path/to/source.md"" [--out ""/path/to/output""] [--template ""/path/to/template/dir""] diff --git a/Ficdown.Parser.Tests/UtilityTests.cs b/Ficdown.Parser.Tests/UtilityTests.cs index 51d3b81..f5ac70a 100644 --- a/Ficdown.Parser.Tests/UtilityTests.cs +++ b/Ficdown.Parser.Tests/UtilityTests.cs @@ -15,22 +15,22 @@ public void FullAnchorMatches() { var anchorStr = @"[Link text](/target-scene)"; - var anchor = Utilities.ParseAnchor(anchorStr); + var anchor = Utilities.ParseAnchor(anchorStr, 0, 0); Assert.Equal(anchorStr, anchor.Original); anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")"; - anchor = Utilities.ParseAnchor(anchorStr); + anchor = Utilities.ParseAnchor(anchorStr, 0, 0); Assert.Equal(anchorStr, anchor.Original); anchorStr = @"[Link text](""Title text"")"; - anchor = Utilities.ParseAnchor(anchorStr); + anchor = Utilities.ParseAnchor(anchorStr, 0, 0); Assert.Equal(anchorStr, anchor.Original); } [Fact] public void AnchorWithTargetMatches() { - var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene)"); + var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("target-scene", anchor.Href.Target); } @@ -38,15 +38,15 @@ [Fact] public void AnchorsWithConditionsMatch() { - var anchor = Utilities.ParseAnchor(@"[Link text](?condition-state)"); + var anchor = Utilities.ParseAnchor(@"[Link text](?condition-state)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.True(anchor.Href.Conditions["condition-state"]); - anchor = Utilities.ParseAnchor(@"[Link text](?!condition-state)"); + anchor = Utilities.ParseAnchor(@"[Link text](?!condition-state)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.False(anchor.Href.Conditions["condition-state"]); - anchor = Utilities.ParseAnchor(@"[Link text](?condition-1&!condition-2)"); + anchor = Utilities.ParseAnchor(@"[Link text](?condition-1&!condition-2)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.True(anchor.Href.Conditions["condition-1"]); Assert.False(anchor.Href.Conditions["condition-2"]); @@ -55,15 +55,15 @@ [Fact] public void AnchorsWithTogglesMatch() { - var anchor = Utilities.ParseAnchor(@"[Link text](#toggle-state)"); + var anchor = Utilities.ParseAnchor(@"[Link text](#toggle-state)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("#toggle-state", anchor.Href.Original); - anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)"); + anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original); - anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)"); + anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original); } @@ -71,11 +71,11 @@ [Fact] public void AnchorsWithTitlesMatch() { - var anchor = Utilities.ParseAnchor(@"[Link text](""Title text"")"); + var anchor = Utilities.ParseAnchor(@"[Link text](""Title text"")", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("Title text", anchor.Title); - anchor = Utilities.ParseAnchor(@"[Talking to Kid](""Lobby"")"); + anchor = Utilities.ParseAnchor(@"[Talking to Kid](""Lobby"")", 0, 0); Assert.Equal("Talking to Kid", anchor.Text); Assert.Equal("Lobby", anchor.Title); } @@ -83,20 +83,20 @@ [Fact] public void ComplexAnchorsMatch() { - var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")"); + var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Href.Original); Assert.Equal("Title text", anchor.Title); - anchor = Utilities.ParseAnchor(@"[Link text](/target-scene#toggle-state)"); + anchor = Utilities.ParseAnchor(@"[Link text](/target-scene#toggle-state)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("/target-scene#toggle-state", anchor.Href.Original); - anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state)"); + anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("/target-scene?condition-state", anchor.Href.Original); - anchor = Utilities.ParseAnchor(@"[Link text](?condition-state#toggle-state)"); + anchor = Utilities.ParseAnchor(@"[Link text](?condition-state#toggle-state)", 0, 0); Assert.Equal("Link text", anchor.Text); Assert.Equal("?condition-state#toggle-state", anchor.Href.Original); } diff --git a/Ficdown.Parser/FicDownParser.cs b/Ficdown.Parser/FicDownParser.cs index 7fc7112..600d89a 100644 --- a/Ficdown.Parser/FicDownParser.cs +++ b/Ficdown.Parser/FicDownParser.cs @@ -37,6 +37,23 @@ namespace Ficdown.Parser var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None); var blocks = BlockHandler.ExtractBlocks(lines); var story = BlockHandler.ParseBlocks(blocks); + + // dupe scene sanity check + foreach(var key in story.Scenes.Keys) + { + foreach(var scene in story.Scenes[key]) + { + foreach(var otherScene in story.Scenes[key].Where(s => s != scene)) + { + if((scene.Conditions == null && otherScene.Conditions == null) + || (scene.Conditions != null && otherScene.Conditions != null + && scene.Conditions.Count == otherScene.Conditions.Count + && !scene.Conditions.Except(otherScene.Conditions).Any())) + throw new FicdownException(scene.Name, string.Format("Scene defined again on line {0}", otherScene.LineNumber), scene.LineNumber); + } + } + } + GameTraverser.Story = story; var resolved = StateResolver.Resolve(GameTraverser.Enumerate(), story); resolved.Orphans = GameTraverser.OrphanedScenes.Select(o => new Orphan diff --git a/Ficdown.Parser/Model/Parser/Anchor.cs b/Ficdown.Parser/Model/Parser/Anchor.cs index 65eec09..33770db 100644 --- a/Ficdown.Parser/Model/Parser/Anchor.cs +++ b/Ficdown.Parser/Model/Parser/Anchor.cs @@ -6,5 +6,7 @@ public string Text { get; set; } public Href Href { get; set; } public string Title { get; set; } + public int LineNumber { get; set; } + public int ColNumber { get; set; } } } diff --git a/Ficdown.Parser/Model/Parser/FicdownException.cs b/Ficdown.Parser/Model/Parser/FicdownException.cs index c81054f..3e07663 100644 --- a/Ficdown.Parser/Model/Parser/FicdownException.cs +++ b/Ficdown.Parser/Model/Parser/FicdownException.cs @@ -6,24 +6,25 @@ namespace Ficdown.Parser.Model.Parser { public string BlockName { get; private set; } public int? LineNumber { get; private set; } + public int? ColNumber { get; private set; } - public FicdownException(string blockName, int? lineNumber, string message) : base(message) + public FicdownException(string blockName, string message, int? lineNumber = null, int? colNumber = null) : base(message) { BlockName = blockName; LineNumber = lineNumber; + ColNumber = colNumber; } public FicdownException(string message) : base(message) { } public override string ToString() { - return !string.IsNullOrEmpty(BlockName) - ? string.Format("Error in block \"{0}\" (Line {1}): {2}", - BlockName, - LineNumber.HasValue - ? LineNumber.ToString() - : "unknown", Message) - : string.Format("Error: {0}", Message); + return string.Format("Error L{0},{1}: {2}", + LineNumber ?? 1, + ColNumber ?? 1, + !string.IsNullOrEmpty(BlockName) + ? string.Format("\"{0}\": {1}", BlockName, Message) + : Message); } } } diff --git a/Ficdown.Parser/Model/Story/Action.cs b/Ficdown.Parser/Model/Story/Action.cs index 30dc720..0553946 100644 --- a/Ficdown.Parser/Model/Story/Action.cs +++ b/Ficdown.Parser/Model/Story/Action.cs @@ -4,6 +4,7 @@ { public int Id { get; set; } public string Toggle { get; set; } + public string RawDescription { get; set; } public string Description { get; set; } public int LineNumber { get; set; } public bool Visited { get; set; } diff --git a/Ficdown.Parser/Model/Story/Scene.cs b/Ficdown.Parser/Model/Story/Scene.cs index f957c33..e5b4ac6 100644 --- a/Ficdown.Parser/Model/Story/Scene.cs +++ b/Ficdown.Parser/Model/Story/Scene.cs @@ -8,6 +8,7 @@ public string Name { get; set; } public string Key { get; set; } public string Description { get; set; } + public string RawDescription { get; set; } public IDictionary Conditions { get; set; } public int LineNumber { get; set; } public bool Visited { get; set; } diff --git a/Ficdown.Parser/Parser/BlockHandler.cs b/Ficdown.Parser/Parser/BlockHandler.cs index 06ef227..c1bda26 100644 --- a/Ficdown.Parser/Parser/BlockHandler.cs +++ b/Ficdown.Parser/Parser/BlockHandler.cs @@ -51,16 +51,16 @@ Anchor storyAnchor; try { - storyAnchor = Utilities.GetInstance(storyBlock.Name, storyBlock.LineNumber).ParseAnchor(storyBlock.Name); + storyAnchor = Utilities.GetInstance(storyBlock.Name, storyBlock.LineNumber).ParseAnchor(storyBlock.Name, storyBlock.LineNumber, 1); } catch(FicdownException ex) { - throw new FicdownException(ex.BlockName, ex.LineNumber, "Story block must be an anchor pointing to the first scene"); + throw new FicdownException(ex.BlockName, "Story name must be an anchor pointing to the first scene", ex.LineNumber); } if (storyAnchor.Href.Target == null || storyAnchor.Href.Conditions != null || storyAnchor.Href.Toggles != null) - throw new FicdownException(storyBlock.Name, storyBlock.LineNumber, "Story href should only have target"); + throw new FicdownException(storyBlock.Name, "Story href should only have a target", storyBlock.LineNumber); var story = new Story { @@ -88,11 +88,11 @@ var a = blocks.First(b => b.Type == BlockType.Action && blocks.Any(d => b != d && BlockToAction(b, 0).Toggle == BlockToAction(d, 0).Toggle)); var actionA = BlockToAction(a, a.LineNumber); var dupe = blocks.First(b => b.Type == BlockType.Action && b != a && BlockToAction(b, 0).Toggle == actionA.Toggle); - throw new FicdownException(actionA.Toggle, actionA.LineNumber, string.Format("Action is defined again on line {0}", dupe.LineNumber)); + throw new FicdownException(actionA.Toggle, string.Format("Action is defined again on line {0}", dupe.LineNumber), actionA.LineNumber); } if (!story.Scenes.ContainsKey(storyAnchor.Href.Target)) - throw new FicdownException(storyBlock.Name, storyBlock.LineNumber, string.Format("Story targets non-existent scene: {0}", storyAnchor.Href.Target)); + throw new FicdownException(storyBlock.Name, string.Format("Story links to undefined scene: {0}", storyAnchor.Href.Target), storyBlock.LineNumber); story.FirstScene = storyAnchor.Href.Target; return story; @@ -105,19 +105,20 @@ { Id = id, LineNumber = block.LineNumber, + RawDescription = string.Join("\n", block.Lines.Select(l => l.Text)), Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim() }; - try + if(RegexLib.Anchors.IsMatch(block.Name)) { - var sceneName = Utilities.GetInstance(block.Name, block.LineNumber).ParseAnchor(block.Name); + var sceneName = Utilities.GetInstance(block.Name, block.LineNumber).ParseAnchor(block.Name, block.LineNumber, 1); scene.Name = sceneName.Title != null ? sceneName.Title.Trim() : sceneName.Text.Trim(); scene.Key = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(sceneName.Text); if(sceneName.Href.Target != null || sceneName.Href.Toggles != null) - throw new FicdownException(block.Name, block.LineNumber, string.Format("Scene href should only have conditions: {0}", block.Name)); + throw new FicdownException(block.Name, "Scene href should only have conditions", block.LineNumber); scene.Conditions = sceneName.Href.Conditions; } - catch(FicdownException) + else { scene.Name = block.Name.Trim(); scene.Key = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(block.Name); @@ -132,6 +133,7 @@ { Id = id, Toggle = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(block.Name), + RawDescription = string.Join("\n", block.Lines.Select(l => l.Text)), Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim(), LineNumber = block.LineNumber }; diff --git a/Ficdown.Parser/Parser/ParserExtensions.cs b/Ficdown.Parser/Parser/ParserExtensions.cs index 7957dbc..dab9447 100644 --- a/Ficdown.Parser/Parser/ParserExtensions.cs +++ b/Ficdown.Parser/Parser/ParserExtensions.cs @@ -1,6 +1,5 @@ namespace Ficdown.Parser.Parser { - using System; using System.Linq; using System.Collections.Generic; diff --git a/Ficdown.Parser/Parser/StateResolver.cs b/Ficdown.Parser/Parser/StateResolver.cs index 3f8184d..3028041 100644 --- a/Ficdown.Parser/Parser/StateResolver.cs +++ b/Ficdown.Parser/Parser/StateResolver.cs @@ -46,7 +46,7 @@ if (anchor.Href.Conditions != null) { var satisfied = Utilities.GetInstance(blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions); - var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(text); + var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(anchor); var replace = alts[satisfied]; text = RegexLib.EscapeChar.Replace(replace, string.Empty); } @@ -67,7 +67,7 @@ if (page.State.ActionsToShow[i]) { var actionTuple = _story.Actions.Single(a => a.Value.Id == i + 1); - var actionAnchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.Description); + var actionAnchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.RawDescription); var anchorDict = GetStateDictionary(page); if ( actionAnchors.Any( @@ -86,7 +86,7 @@ } } - var anchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.Description); + var anchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.RawDescription); var stateDict = GetStateDictionary(page); var text = RegexLib.EmptyListItem.Replace( diff --git a/Ficdown.Parser/Parser/Utilities.cs b/Ficdown.Parser/Parser/Utilities.cs index 15520c2..78022a8 100644 --- a/Ficdown.Parser/Parser/Utilities.cs +++ b/Ficdown.Parser/Parser/Utilities.cs @@ -1,5 +1,4 @@ - -namespace Ficdown.Parser.Parser +namespace Ficdown.Parser.Parser { using System; using System.Collections.Generic; @@ -34,7 +33,7 @@ namespace Ficdown.Parser.Parser return Regex.Replace(Regex.Replace(raw.ToLower(), @"^\W+|\W+$", string.Empty), @"\W+", "-"); } - private Href ParseHref(string href) + private Href ParseHref(string href, int lineNumber, int colNumber) { var match = RegexLib.Href.Match(href); if (match.Success) @@ -57,27 +56,50 @@ namespace Ficdown.Parser.Parser : null }; } - throw new FicdownException(_blockName, _lineNumber, string.Format("Invalid href: {0}", href)); + throw new FicdownException(_blockName, string.Format("Invalid href: {0}", href), lineNumber, colNumber); } - public Anchor ParseAnchor(string anchorText) + public Anchor ParseAnchor(string anchorText, int lineNumber, int colNumber) { var match = RegexLib.Anchors.Match(anchorText); - if (!match.Success) throw new FicdownException(_blockName, _lineNumber, string.Format("Invalid anchor: {0}", anchorText)); - return MatchToAnchor(match); + if (!match.Success) throw new FicdownException(_blockName, string.Format("Invalid anchor: {0}", anchorText), lineNumber, colNumber); + return MatchToAnchor(match, lineNumber, colNumber); + } + + private void PosFromIndex(string text, int index, out int line, out int col) + { + line = 1; + col = 1; + for (int i = 0; i <= index - 1; i++) + { + col++; + if (text[i] == '\n') + { + line++; + col = 1; + } + } } public IList ParseAnchors(string text) { var matches = RegexLib.Anchors.Matches(text); - return matches.Cast().Select(MatchToAnchor).ToList(); + return matches.Cast().Select(m => + { + int line, col; + PosFromIndex(text, m.Index, out line, out col); + if(_lineNumber.HasValue) line += _lineNumber.Value; + return MatchToAnchor(m, line, col); + }).ToList(); } - private Anchor MatchToAnchor(Match match) + private Anchor MatchToAnchor(Match match, int lineNumber, int colNumber) { var astr = match.Groups["anchor"].Value; var txstr = match.Groups["text"].Value; - var ttstr = match.Groups["title"].Value; + var ttstr = match.Groups["title"].Success + ? match.Groups["title"].Value + : null; var hrefstr = match.Groups["href"].Value; if (hrefstr.StartsWith(@"""")) { @@ -89,14 +111,18 @@ namespace Ficdown.Parser.Parser Original = !string.IsNullOrEmpty(astr) ? astr : null, Text = !string.IsNullOrEmpty(txstr) ? txstr : null, Title = ttstr, - Href = ParseHref(hrefstr) + Href = ParseHref(hrefstr, lineNumber, colNumber), + LineNumber = lineNumber, + ColNumber = colNumber }; } - public IDictionary ParseConditionalText(string text) + public IDictionary ParseConditionalText(Anchor anchor) { - var match = RegexLib.ConditionalText.Match(text); - if (!match.Success) throw new FicdownException(_blockName, _lineNumber, string.Format(@"Invalid conditional text: {0}", text)); + var match = RegexLib.ConditionalText.Match(anchor.Text); + if (!match.Success) + throw new FicdownException(_blockName, string.Format(@"Invalid conditional text: {0}", anchor.Text), anchor.LineNumber, anchor.ColNumber); + return new Dictionary { {true, match.Groups["true"].Value}, diff --git a/Ficdown.Parser/Player/GameTraverser.cs b/Ficdown.Parser/Player/GameTraverser.cs index d4fdb36..e7d7286 100644 --- a/Ficdown.Parser/Player/GameTraverser.cs +++ b/Ficdown.Parser/Player/GameTraverser.cs @@ -125,11 +125,11 @@ var states = new HashSet(); - var anchors = Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.Description).ToList(); + var anchors = Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.RawDescription).ToList(); foreach (var action in GetActionsForPage(currentState.Page)) { action.Visited = true; - anchors.AddRange(Utilities.GetInstance(action.Toggle, action.LineNumber).ParseAnchors(action.Description)); + anchors.AddRange(Utilities.GetInstance(action.Toggle, action.LineNumber).ParseAnchors(action.RawDescription)); } var conditionals = anchors.SelectMany( @@ -143,8 +143,19 @@ // signal to previous scenes that this scene's used conditionals are important if(currentState.Page.Scene.Conditions != null) foreach (var conditional in currentState.Page.Scene.Conditions) - _manager.ToggleStateOn(affected, conditional.Key); - foreach (var conditional in conditionals) _manager.ToggleStateOn(affected, conditional); + { + var anchor = anchors.FirstOrDefault(a => + a.Href.Conditions != null + && a.Href.Conditions.Keys.Contains(conditional.Key)); + _manager.ToggleStateOn(affected, conditional.Key, currentState.Page.Scene.Name, anchor); + } + foreach (var conditional in conditionals) + { + var anchor = anchors.FirstOrDefault(a => + a.Href.Conditions != null + && a.Href.Conditions.Keys.Contains(conditional)); + _manager.ToggleStateOn(affected, conditional, currentState.Page.Scene.Name, anchor); + } // signal to previous scenes if this scene has first-seen text if (hasFirstSeen) _manager.ToggleSeenSceneOn(affected, currentState.Page.Scene.Id); @@ -155,7 +166,7 @@ // don't follow links that would be hidden if (anchor.Href.Conditions != null && string.IsNullOrEmpty( - Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor.Text)[ + Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor)[ Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page), anchor.Href.Conditions)])) continue; diff --git a/Ficdown.Parser/Player/StateManager.cs b/Ficdown.Parser/Player/StateManager.cs index 6a7fcf5..23805b2 100644 --- a/Ficdown.Parser/Player/StateManager.cs +++ b/Ficdown.Parser/Player/StateManager.cs @@ -28,7 +28,7 @@ var toggle in allScenes.SelectMany( sc => - Utilities.GetInstance(sc.Name, sc.LineNumber).ParseAnchors(sc.Description) + Utilities.GetInstance(sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription) .SelectMany( a => a.Href.Toggles != null @@ -43,6 +43,12 @@ { get { + var scene = _story.Scenes[_story.FirstScene].Where(s => s.Conditions == null); + if(scene == null) + throw new FicdownException(_story.Name, string.Format("Story links to undefined scene: {0}", _story.FirstScene)); + if(scene.Count() > 1) + throw new FicdownException(_story.Name, string.Format("Story links to scene that is defined more than once: {0}", _story.FirstScene)); + return new PageState { Id = Guid.Empty, @@ -61,7 +67,7 @@ ActionsToShow = new BitArray(_actionCount), ActionFirstToggles = null }, - Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null), + Scene = scene.Single(), StateMatrix = _stateMatrix }; } @@ -83,7 +89,7 @@ if(actionFirstToggles == null) actionFirstToggles = new List(); newState.State.ActionsToShow[_story.Actions[toggle].Id - 1] = true; if ( - Utilities.GetInstance(_story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].Description) + Utilities.GetInstance(_story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].RawDescription) .Any(a => a.Href.Conditions != null && a.Href.Conditions.ContainsKey(toggle))) actionFirstToggles.Add(!current.State.PlayerState[_stateMatrix[toggle]]); } @@ -93,12 +99,14 @@ newState.State.ActionFirstToggles = actionFirstToggles != null ? new BitArray(actionFirstToggles.ToArray()) : null; - newState.Scene = GetScene(current.Scene.Name, current.Scene.LineNumber, target, newState.State.PlayerState); + newState.Scene = GetScene(current.Scene.Name, anchor, target, newState.State.PlayerState); return newState; } - public void ToggleStateOn(State state, string toggle) + public void ToggleStateOn(State state, string toggle, string blockName, Anchor anchor) { + if(!_stateMatrix.ContainsKey(toggle)) + throw new FicdownException(blockName, string.Format("Conditional for undefined state: {0}", toggle), anchor != null ? anchor.LineNumber : 1, anchor != null ? anchor.ColNumber : 1); state.PlayerState[_stateMatrix[toggle]] = true; } @@ -138,10 +146,10 @@ return GetUniqueHash(compressed, page.Scene.Key); } - private Scene GetScene(string blockName, int lineNumber, string target, BitArray playerState) + private Scene GetScene(string blockName, Anchor anchor, string target, BitArray playerState) { if (!_story.Scenes.ContainsKey(target)) - throw new FicdownException(blockName, lineNumber, string.Format("Encountered link to non-existent scene: {0}", target)); + throw new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber); Scene newScene = null; foreach (var scene in _story.Scenes[target]) @@ -154,7 +162,7 @@ } } if (newScene == null) - throw new FicdownException(blockName, lineNumber, string.Format("Scene {0} reached with unmatched player state", target)); + throw new FicdownException(blockName, string.Format("Link to scene that is undefined for conditionals: {0}", target), anchor.LineNumber, anchor.ColNumber); return newScene; } @@ -163,7 +171,7 @@ if (scene.Conditions == null) return true; scene.Conditions.ToList().ForEach(c => { - if(!_stateMatrix.ContainsKey(c.Key)) throw new FicdownException(scene.Name, scene.LineNumber, string.Format("Reference to non-existent state: {0}", c.Key)); + if(!_stateMatrix.ContainsKey(c.Key)) throw new FicdownException(scene.Name, string.Format("Conditional for undefined state: {0}", c.Key), scene.LineNumber); }); return scene.Conditions.All(c => playerState[_stateMatrix[c.Key]] == c.Value); } From b75a672ce2098701502564c9bcb2bd18ca5414b3 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Tue, 25 Sep 2018 17:18:11 -0500 Subject: [PATCH 2/4] handling more errors gracefully --- Ficdown.Parser/Parser/BlockHandler.cs | 7 +++++-- Ficdown.Parser/Player/StateManager.cs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Ficdown.Parser/Parser/BlockHandler.cs b/Ficdown.Parser/Parser/BlockHandler.cs index c1bda26..f8e202d 100644 --- a/Ficdown.Parser/Parser/BlockHandler.cs +++ b/Ficdown.Parser/Parser/BlockHandler.cs @@ -45,8 +45,11 @@ public Story ParseBlocks(IEnumerable blocks) { // get the story - var storyBlock = blocks.SingleOrDefault(b => b.Type == BlockType.Story); - if(storyBlock == null) throw new FicdownException("No story block found"); + var storyBlocks = blocks.Where(b => b.Type == BlockType.Story); + if(storyBlocks.Count() == 0) throw new FicdownException("No story block found"); + if(storyBlocks.Count() > 1) throw new FicdownException("More than one story block found"); + + var storyBlock = storyBlocks.Single(); Anchor storyAnchor; try diff --git a/Ficdown.Parser/Player/StateManager.cs b/Ficdown.Parser/Player/StateManager.cs index 23805b2..0722fd3 100644 --- a/Ficdown.Parser/Player/StateManager.cs +++ b/Ficdown.Parser/Player/StateManager.cs @@ -44,7 +44,7 @@ get { var scene = _story.Scenes[_story.FirstScene].Where(s => s.Conditions == null); - if(scene == null) + if(scene.Count() == 0) throw new FicdownException(_story.Name, string.Format("Story links to undefined scene: {0}", _story.FirstScene)); if(scene.Count() > 1) throw new FicdownException(_story.Name, string.Format("Story links to scene that is defined more than once: {0}", _story.FirstScene)); From 85a06d77cd7450391030074476cbd538dd30fd55 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Thu, 27 Sep 2018 12:32:32 -0500 Subject: [PATCH 3/4] output multiple errors instead of just dying on the first one encountered --- Ficdown.Console/Program.cs | 8 ++-- Ficdown.Parser.Tests/BlockHandlerTests.cs | 39 ++++++++++------ Ficdown.Parser.Tests/UtilityTests.cs | 6 ++- Ficdown.Parser/FicDownParser.cs | 43 +++++++++++++++--- Ficdown.Parser/Parser/BlockHandler.cs | 29 ++++++------ Ficdown.Parser/Parser/IBlockHandler.cs | 1 + Ficdown.Parser/Parser/IStateResolver.cs | 1 + Ficdown.Parser/Parser/StateResolver.cs | 19 +++++--- Ficdown.Parser/Parser/Utilities.cs | 23 +++++++--- Ficdown.Parser/Player/GameTraverser.cs | 40 ++++++++++------- Ficdown.Parser/Player/IGameTraverser.cs | 2 + Ficdown.Parser/Player/StateManager.cs | 55 +++++++++++++---------- Ficdown.Parser/Render/HtmlRenderer.cs | 4 +- Ficdown.Parser/Render/IRenderer.cs | 3 +- 14 files changed, 176 insertions(+), 97 deletions(-) diff --git a/Ficdown.Console/Program.cs b/Ficdown.Console/Program.cs index cd61c21..d501264 100644 --- a/Ficdown.Console/Program.cs +++ b/Ficdown.Console/Program.cs @@ -149,12 +149,10 @@ var story = parser.ParseStory(storyText); - story.Orphans.ToList().ForEach(o => - { - Console.WriteLine("Warning L{0},1: \"{1}\": Unreachable {2}", o.LineNumber, o.Name, o.Type); - }); + parser.Warnings.Select(w => w.ToString()).Distinct().ToList().ForEach(s => Console.WriteLine(s)); + story.Orphans.ToList().ForEach(o => Console.WriteLine("Warning L{0},1: \"{1}\": Unreachable {2}", o.LineNumber, o.Name, o.Type)); - if(!lintMode) + if(!lintMode && parser.Warnings.Count() == 0) { IRenderer rend; switch (format) diff --git a/Ficdown.Parser.Tests/BlockHandlerTests.cs b/Ficdown.Parser.Tests/BlockHandlerTests.cs index 32dccfa..73275fc 100644 --- a/Ficdown.Parser.Tests/BlockHandlerTests.cs +++ b/Ficdown.Parser.Tests/BlockHandlerTests.cs @@ -4,13 +4,19 @@ using Parser; using Model.Parser; using Extensions; + using System.Collections.Generic; public class BlockHandlerTests { + private BlockHandler NewBlockHandler + { + get { return new BlockHandler { Warnings = new List() }; } + } + [Fact] public void NoStoryBlockThrowsException() { - var bh = new BlockHandler(); + var bh = NewBlockHandler; Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" ## this file has no story just a lonely scene".ToLines()))); @@ -19,7 +25,7 @@ just a lonely scene".ToLines()))); [Fact] public void StoryWithNoAnchorThrowsException() { - var bh = new BlockHandler(); + var bh = NewBlockHandler; Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" # my story doesn't link to a scene @@ -30,17 +36,23 @@ nothing links here".ToLines()))); [Fact] public void StoriesWithFancyAnchorsThrowExceptions() { - var bh = new BlockHandler(); - Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" + var bh = NewBlockHandler; + bh.ParseBlocks(bh.ExtractBlocks(@" # [my story](/a-scene?conditional) story with a conditional ## a scene -this is a scene".ToLines()))); - Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" +this is a scene".ToLines())); + Assert.NotEmpty(bh.Warnings); + + bh = NewBlockHandler; + bh.ParseBlocks(bh.ExtractBlocks(@" # [my story](/a-scene#toggle) story with a toggle ## a scene -this is a scene".ToLines()))); +this is a scene".ToLines())); + Assert.NotEmpty(bh.Warnings); + + bh = NewBlockHandler; Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" # [my story](/a-scene#?conditional#toggle) story with a conditional and a toggle @@ -51,7 +63,7 @@ this is a scene".ToLines()))); [Fact] public void StoryLinkingToNonExistentSceneThrowsException() { - var bh = new BlockHandler(); + var bh = NewBlockHandler; Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" # [a story](/non-existent) this story links to a first scene that doesn't exist @@ -62,7 +74,7 @@ this scene is so cold and lonely".ToLines()))); [Fact] public void StoryWithALegitAnchorParses() { - var bh = new BlockHandler(); + var bh = NewBlockHandler; bh.ParseBlocks(bh.ExtractBlocks(@" # [my story](/a-scene) story with a simple link @@ -73,8 +85,8 @@ this is a scene".ToLines())); [Fact] public void StoryWithDuplicateActionsThrowsException() { - var bh = new BlockHandler(); - Assert.Throws(() => bh.ParseBlocks(bh.ExtractBlocks(@" + var bh = NewBlockHandler; + bh.ParseBlocks(bh.ExtractBlocks(@" # [a story](/a-scene) this story is action-happy ## a scene @@ -84,13 +96,14 @@ this is an action ## another scene this is another scene ### an action -oops, this is the same action!".ToLines()))); +oops, this is the same action!".ToLines())); + Assert.NotEmpty(bh.Warnings); } [Fact] public void StoryWithScenesAndActionsParses() { - var bh = new BlockHandler(); + var bh = NewBlockHandler; var story = bh.ParseBlocks(bh.ExtractBlocks(@" # [my story](/a-scene) story with a simple link diff --git a/Ficdown.Parser.Tests/UtilityTests.cs b/Ficdown.Parser.Tests/UtilityTests.cs index f5ac70a..ce40719 100644 --- a/Ficdown.Parser.Tests/UtilityTests.cs +++ b/Ficdown.Parser.Tests/UtilityTests.cs @@ -1,14 +1,18 @@ namespace Ficdown.Parser.Tests { + using System.Collections.Generic; using System.Linq; + using Model.Parser; using Parser; using Xunit; public class UtilityTests { + private List Warnings = new List(); + private Utilities Utilities { - get { return Utilities.GetInstance("none", 0); } + get { return Utilities.GetInstance(Warnings, "none", 0); } } [Fact] diff --git a/Ficdown.Parser/FicDownParser.cs b/Ficdown.Parser/FicDownParser.cs index 600d89a..0bb2404 100644 --- a/Ficdown.Parser/FicDownParser.cs +++ b/Ficdown.Parser/FicDownParser.cs @@ -4,6 +4,7 @@ namespace Ficdown.Parser { using System; + using System.Collections.Generic; using System.Linq; using Model.Parser; using Parser; @@ -11,25 +12,53 @@ namespace Ficdown.Parser public class FicdownParser { + public List Warnings { get; private set; } + private IBlockHandler _blockHandler; internal IBlockHandler BlockHandler { - get { return _blockHandler ?? (_blockHandler = new BlockHandler()); } - set { _blockHandler = value; } + get + { + return _blockHandler ?? + (_blockHandler = new BlockHandler { Warnings = Warnings }); + } + set + { + _blockHandler = value; + _blockHandler.Warnings = Warnings; + } } private IGameTraverser _gameTraverser; internal IGameTraverser GameTraverser { - get { return _gameTraverser ?? (_gameTraverser = new GameTraverser()); } - set { _gameTraverser = value; } + get { return _gameTraverser ?? + (_gameTraverser = new GameTraverser { Warnings = Warnings }); } + set + { + _gameTraverser = value; + _gameTraverser.Warnings = Warnings; + } } private IStateResolver _stateResolver; internal IStateResolver StateResolver { - get { return _stateResolver ?? (_stateResolver = new StateResolver()); } - set { _stateResolver = value; } + get + { + return _stateResolver ?? + (_stateResolver = new StateResolver { Warnings = Warnings }); + } + set + { + _stateResolver = value; + _stateResolver.Warnings = Warnings; + } + } + + public FicdownParser() + { + Warnings = new List(); } public ResolvedStory ParseStory(string storyText) @@ -49,7 +78,7 @@ namespace Ficdown.Parser || (scene.Conditions != null && otherScene.Conditions != null && scene.Conditions.Count == otherScene.Conditions.Count && !scene.Conditions.Except(otherScene.Conditions).Any())) - throw new FicdownException(scene.Name, string.Format("Scene defined again on line {0}", otherScene.LineNumber), scene.LineNumber); + Warnings.Add(new FicdownException(scene.Name, string.Format("Scene defined again on line {0}", otherScene.LineNumber), scene.LineNumber)); } } } diff --git a/Ficdown.Parser/Parser/BlockHandler.cs b/Ficdown.Parser/Parser/BlockHandler.cs index f8e202d..4803497 100644 --- a/Ficdown.Parser/Parser/BlockHandler.cs +++ b/Ficdown.Parser/Parser/BlockHandler.cs @@ -10,6 +10,8 @@ internal class BlockHandler : IBlockHandler { + public List Warnings { get; set; } + public IEnumerable ExtractBlocks(IEnumerable lines) { var blocks = new List(); @@ -51,19 +53,16 @@ var storyBlock = storyBlocks.Single(); - Anchor storyAnchor; - try + var storyAnchor = Utilities.GetInstance(Warnings, storyBlock.Name, storyBlock.LineNumber).ParseAnchor(storyBlock.Name, storyBlock.LineNumber, 1); + + if(storyAnchor == null || storyAnchor.Href == null) { - storyAnchor = Utilities.GetInstance(storyBlock.Name, storyBlock.LineNumber).ParseAnchor(storyBlock.Name, storyBlock.LineNumber, 1); - } - catch(FicdownException ex) - { - throw new FicdownException(ex.BlockName, "Story name must be an anchor pointing to the first scene", ex.LineNumber); + throw new FicdownException(storyBlock.Name, "Story name must be an anchor pointing to the first scene", storyBlock.LineNumber); } if (storyAnchor.Href.Target == null || storyAnchor.Href.Conditions != null || storyAnchor.Href.Toggles != null) - throw new FicdownException(storyBlock.Name, "Story href should only have a target", storyBlock.LineNumber); + Warnings.Add(new FicdownException(storyBlock.Name, "Story href should only have a target", storyBlock.LineNumber)); var story = new Story { @@ -91,7 +90,7 @@ var a = blocks.First(b => b.Type == BlockType.Action && blocks.Any(d => b != d && BlockToAction(b, 0).Toggle == BlockToAction(d, 0).Toggle)); var actionA = BlockToAction(a, a.LineNumber); var dupe = blocks.First(b => b.Type == BlockType.Action && b != a && BlockToAction(b, 0).Toggle == actionA.Toggle); - throw new FicdownException(actionA.Toggle, string.Format("Action is defined again on line {0}", dupe.LineNumber), actionA.LineNumber); + Warnings.Add(new FicdownException(actionA.Toggle, string.Format("Action is defined again on line {0}", dupe.LineNumber), actionA.LineNumber)); } if (!story.Scenes.ContainsKey(storyAnchor.Href.Target)) @@ -112,19 +111,19 @@ Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim() }; - if(RegexLib.Anchors.IsMatch(block.Name)) + Anchor sceneName; + if(RegexLib.Anchors.IsMatch(block.Name) && (sceneName = Utilities.GetInstance(Warnings, block.Name, block.LineNumber).ParseAnchor(block.Name, block.LineNumber, 1)).Href != null) { - var sceneName = Utilities.GetInstance(block.Name, block.LineNumber).ParseAnchor(block.Name, block.LineNumber, 1); scene.Name = sceneName.Title != null ? sceneName.Title.Trim() : sceneName.Text.Trim(); - scene.Key = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(sceneName.Text); + scene.Key = Utilities.GetInstance(Warnings, block.Name, block.LineNumber).NormalizeString(sceneName.Text); if(sceneName.Href.Target != null || sceneName.Href.Toggles != null) - throw new FicdownException(block.Name, "Scene href should only have conditions", block.LineNumber); + Warnings.Add(new FicdownException(block.Name, "Scene href should only have conditions", block.LineNumber)); scene.Conditions = sceneName.Href.Conditions; } else { scene.Name = block.Name.Trim(); - scene.Key = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(block.Name); + scene.Key = Utilities.GetInstance(Warnings, block.Name, block.LineNumber).NormalizeString(block.Name); } return scene; @@ -135,7 +134,7 @@ return new Action { Id = id, - Toggle = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(block.Name), + Toggle = Utilities.GetInstance(Warnings, block.Name, block.LineNumber).NormalizeString(block.Name), RawDescription = string.Join("\n", block.Lines.Select(l => l.Text)), Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim(), LineNumber = block.LineNumber diff --git a/Ficdown.Parser/Parser/IBlockHandler.cs b/Ficdown.Parser/Parser/IBlockHandler.cs index 282805b..fcfcdff 100644 --- a/Ficdown.Parser/Parser/IBlockHandler.cs +++ b/Ficdown.Parser/Parser/IBlockHandler.cs @@ -6,6 +6,7 @@ internal interface IBlockHandler { + List Warnings { set; } IEnumerable ExtractBlocks(IEnumerable lines); Story ParseBlocks(IEnumerable blocks); } diff --git a/Ficdown.Parser/Parser/IStateResolver.cs b/Ficdown.Parser/Parser/IStateResolver.cs index 13c694e..eb7e5f9 100644 --- a/Ficdown.Parser/Parser/IStateResolver.cs +++ b/Ficdown.Parser/Parser/IStateResolver.cs @@ -7,6 +7,7 @@ internal interface IStateResolver { + List Warnings { set; } ResolvedStory Resolve(IEnumerable pages, Story story); } } diff --git a/Ficdown.Parser/Parser/StateResolver.cs b/Ficdown.Parser/Parser/StateResolver.cs index 3028041..959d302 100644 --- a/Ficdown.Parser/Parser/StateResolver.cs +++ b/Ficdown.Parser/Parser/StateResolver.cs @@ -15,6 +15,8 @@ private readonly HashSet _usedNames; private Story _story; + public List Warnings { private get; set; } + public StateResolver() { _pageNames = new Dictionary(); @@ -43,12 +45,15 @@ private string ResolveAnchor(string blockName, int lineNumber, Anchor anchor, IDictionary playerState, string targetHash) { var text = anchor.Text; - if (anchor.Href.Conditions != null) + if (anchor.Href != null && anchor.Href.Conditions != null) { - var satisfied = Utilities.GetInstance(blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions); - var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(anchor); - var replace = alts[satisfied]; - text = RegexLib.EscapeChar.Replace(replace, string.Empty); + var satisfied = Utilities.GetInstance(Warnings, blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions); + var alts = Utilities.GetInstance(Warnings, blockName, lineNumber).ParseConditionalText(anchor); + if(alts != null) + { + var replace = alts[satisfied]; + text = RegexLib.EscapeChar.Replace(replace, string.Empty); + } } return !string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(targetHash) ? string.Format("[{0}](/{1})", text, GetPageNameForHash(targetHash)) @@ -67,7 +72,7 @@ if (page.State.ActionsToShow[i]) { var actionTuple = _story.Actions.Single(a => a.Value.Id == i + 1); - var actionAnchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.RawDescription); + var actionAnchors = Utilities.GetInstance(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.RawDescription); var anchorDict = GetStateDictionary(page); if ( actionAnchors.Any( @@ -86,7 +91,7 @@ } } - var anchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.RawDescription); + var anchors = Utilities.GetInstance(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.RawDescription); var stateDict = GetStateDictionary(page); var text = RegexLib.EmptyListItem.Replace( diff --git a/Ficdown.Parser/Parser/Utilities.cs b/Ficdown.Parser/Parser/Utilities.cs index 78022a8..5bdba33 100644 --- a/Ficdown.Parser/Parser/Utilities.cs +++ b/Ficdown.Parser/Parser/Utilities.cs @@ -1,6 +1,5 @@ namespace Ficdown.Parser.Parser { - using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -8,19 +7,23 @@ internal class Utilities { - public static Utilities GetInstance(string blockName, int lineNumber) + private List _warnings { get; set; } + + public static Utilities GetInstance(List warnings, string blockName, int lineNumber) { return new Utilities { + _warnings = warnings, _blockName = blockName, _lineNumber = lineNumber }; } - public static Utilities GetInstance(string blockName) + public static Utilities GetInstance(List warnings, string blockName) { return new Utilities { + _warnings = warnings, _blockName = blockName }; } @@ -56,13 +59,18 @@ : null }; } - throw new FicdownException(_blockName, string.Format("Invalid href: {0}", href), lineNumber, colNumber); + _warnings.Add(new FicdownException(_blockName, string.Format("Invalid href: {0}", href), lineNumber, colNumber)); + return null; } public Anchor ParseAnchor(string anchorText, int lineNumber, int colNumber) { var match = RegexLib.Anchors.Match(anchorText); - if (!match.Success) throw new FicdownException(_blockName, string.Format("Invalid anchor: {0}", anchorText), lineNumber, colNumber); + if (!match.Success) + { + _warnings.Add(new FicdownException(_blockName, string.Format("Invalid anchor: {0}", anchorText), lineNumber, colNumber)); + return null; + } return MatchToAnchor(match, lineNumber, colNumber); } @@ -121,7 +129,10 @@ { var match = RegexLib.ConditionalText.Match(anchor.Text); if (!match.Success) - throw new FicdownException(_blockName, string.Format(@"Invalid conditional text: {0}", anchor.Text), anchor.LineNumber, anchor.ColNumber); + { + _warnings.Add(new FicdownException(_blockName, string.Format(@"Invalid conditional text: {0}", anchor.Text), anchor.LineNumber, anchor.ColNumber)); + return null; + } return new Dictionary { diff --git a/Ficdown.Parser/Player/GameTraverser.cs b/Ficdown.Parser/Player/GameTraverser.cs index e7d7286..a91ea0f 100644 --- a/Ficdown.Parser/Player/GameTraverser.cs +++ b/Ficdown.Parser/Player/GameTraverser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using Model.Parser; using Model.Player; using Model.Story; using Parser; @@ -17,6 +18,8 @@ private IDictionary _actionMatrix; private bool _wasRun = false; + public List Warnings { private get; set; } + private Story _story; public Story Story { @@ -25,7 +28,7 @@ { _story = value; _actionMatrix = _story.Actions.ToDictionary(a => a.Value.Id, a => a.Value); - _manager = new StateManager(_story); + _manager = new StateManager(_story, Warnings); _processingQueue = new Queue(); _processed = new Dictionary(); _compressed = new Dictionary(); @@ -125,15 +128,15 @@ var states = new HashSet(); - var anchors = Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.RawDescription).ToList(); + var anchors = Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.RawDescription).ToList(); foreach (var action in GetActionsForPage(currentState.Page)) { action.Visited = true; - anchors.AddRange(Utilities.GetInstance(action.Toggle, action.LineNumber).ParseAnchors(action.RawDescription)); + anchors.AddRange(Utilities.GetInstance(Warnings, action.Toggle, action.LineNumber).ParseAnchors(action.RawDescription)); } var conditionals = anchors.SelectMany( - a => a.Href.Conditions != null ? a.Href.Conditions.Select(c => c.Key) : new string[] {}) + a => a.Href != null && a.Href.Conditions != null ? a.Href.Conditions.Select(c => c.Key) : new string[] {}) .Distinct() .ToArray(); var hasFirstSeen = RegexLib.BlockQuotes.IsMatch(currentState.Page.Scene.Description); @@ -147,39 +150,42 @@ var anchor = anchors.FirstOrDefault(a => a.Href.Conditions != null && a.Href.Conditions.Keys.Contains(conditional.Key)); - _manager.ToggleStateOn(affected, conditional.Key, currentState.Page.Scene.Name, anchor); + _manager.ToggleStateOn(affected, conditional.Key, currentState.Page.Scene.Name, anchor != null ? anchor.LineNumber : currentState.Page.Scene.LineNumber, anchor != null ? anchor.ColNumber : 1); } foreach (var conditional in conditionals) { var anchor = anchors.FirstOrDefault(a => a.Href.Conditions != null && a.Href.Conditions.Keys.Contains(conditional)); - _manager.ToggleStateOn(affected, conditional, currentState.Page.Scene.Name, anchor); + _manager.ToggleStateOn(affected, conditional, currentState.Page.Scene.Name, anchor != null ? anchor.LineNumber : currentState.Page.Scene.LineNumber, anchor != null ? anchor.ColNumber : 1); } // signal to previous scenes if this scene has first-seen text if (hasFirstSeen) _manager.ToggleSeenSceneOn(affected, currentState.Page.Scene.Id); } - foreach (var anchor in anchors.Where(a => a.Href.Target != null || a.Href.Toggles != null)) + foreach (var anchor in anchors.Where(a => a.Href != null && (a.Href.Target != null || a.Href.Toggles != null))) { // don't follow links that would be hidden if (anchor.Href.Conditions != null && string.IsNullOrEmpty( - Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor)[ - Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page), + Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor)[ + Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page), anchor.Href.Conditions)])) continue; var newState = _manager.ResolveNewState(anchor, currentState.Page); - if (!currentState.Page.Links.ContainsKey(anchor.Original)) - currentState.Page.Links.Add(anchor.Original, newState.UniqueHash); - - if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash)) + if(newState.Scene != null) { - states.Add(newState.UniqueHash); - var newAffected = new List(currentState.AffectedStates); - newAffected.Add(newState.AffectedState); - _processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected}); + if (!currentState.Page.Links.ContainsKey(anchor.Original)) + currentState.Page.Links.Add(anchor.Original, newState.UniqueHash); + + if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash)) + { + states.Add(newState.UniqueHash); + var newAffected = new List(currentState.AffectedStates); + newAffected.Add(newState.AffectedState); + _processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected}); + } } } } diff --git a/Ficdown.Parser/Player/IGameTraverser.cs b/Ficdown.Parser/Player/IGameTraverser.cs index 23c995d..3ce38bd 100644 --- a/Ficdown.Parser/Player/IGameTraverser.cs +++ b/Ficdown.Parser/Player/IGameTraverser.cs @@ -1,11 +1,13 @@ namespace Ficdown.Parser.Player { using System.Collections.Generic; + using Model.Parser; using Model.Player; using Model.Story; internal interface IGameTraverser { + List Warnings { set; } Story Story { get; set; } IEnumerable Enumerate(); IEnumerable OrphanedScenes { get; } diff --git a/Ficdown.Parser/Player/StateManager.cs b/Ficdown.Parser/Player/StateManager.cs index 0722fd3..724b140 100644 --- a/Ficdown.Parser/Player/StateManager.cs +++ b/Ficdown.Parser/Player/StateManager.cs @@ -16,8 +16,11 @@ private readonly int _sceneCount; private readonly int _actionCount; - public StateManager(Story story) + private List _warnings { get; set; } + + public StateManager(Story story, List warnings) { + _warnings = warnings; _story = story; var allScenes = _story.Scenes.SelectMany(s => s.Value); _sceneCount = allScenes.Max(s => s.Id); @@ -28,10 +31,10 @@ var toggle in allScenes.SelectMany( sc => - Utilities.GetInstance(sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription) + Utilities.GetInstance(_warnings, sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription) .SelectMany( a => - a.Href.Toggles != null + a.Href != null && a.Href.Toggles != null ? a.Href.Toggles.Where(t => !_stateMatrix.ContainsKey(t)) : new string[] {}))) { @@ -47,7 +50,7 @@ if(scene.Count() == 0) throw new FicdownException(_story.Name, string.Format("Story links to undefined scene: {0}", _story.FirstScene)); if(scene.Count() > 1) - throw new FicdownException(_story.Name, string.Format("Story links to scene that is defined more than once: {0}", _story.FirstScene)); + _warnings.Add(new FicdownException(_story.Name, string.Format("Story links to scene that is defined more than once: {0}", _story.FirstScene))); return new PageState { @@ -67,7 +70,7 @@ ActionsToShow = new BitArray(_actionCount), ActionFirstToggles = null }, - Scene = scene.Single(), + Scene = scene.First(), StateMatrix = _stateMatrix }; } @@ -89,7 +92,7 @@ if(actionFirstToggles == null) actionFirstToggles = new List(); newState.State.ActionsToShow[_story.Actions[toggle].Id - 1] = true; if ( - Utilities.GetInstance(_story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].RawDescription) + Utilities.GetInstance(_warnings, _story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].RawDescription) .Any(a => a.Href.Conditions != null && a.Href.Conditions.ContainsKey(toggle))) actionFirstToggles.Add(!current.State.PlayerState[_stateMatrix[toggle]]); } @@ -103,11 +106,12 @@ return newState; } - public void ToggleStateOn(State state, string toggle, string blockName, Anchor anchor) + public void ToggleStateOn(State state, string toggle, string blockName, int lineNumber, int colNumber) { - if(!_stateMatrix.ContainsKey(toggle)) - throw new FicdownException(blockName, string.Format("Conditional for undefined state: {0}", toggle), anchor != null ? anchor.LineNumber : 1, anchor != null ? anchor.ColNumber : 1); - state.PlayerState[_stateMatrix[toggle]] = true; + if(_stateMatrix.ContainsKey(toggle)) + state.PlayerState[_stateMatrix[toggle]] = true; + else + _warnings.Add(new FicdownException(blockName, string.Format("Conditional for undefined state: {0}", toggle), lineNumber, colNumber)); } public void ToggleSeenSceneOn(State state, int sceneId) @@ -148,22 +152,24 @@ private Scene GetScene(string blockName, Anchor anchor, string target, BitArray playerState) { - if (!_story.Scenes.ContainsKey(target)) - throw new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber); - - Scene newScene = null; - foreach (var scene in _story.Scenes[target]) + if (_story.Scenes.ContainsKey(target)) { - if (ConditionsMatch(scene, playerState) && - (newScene == null || newScene.Conditions == null || - scene.Conditions.Count > newScene.Conditions.Count)) + Scene newScene = null; + foreach (var scene in _story.Scenes[target]) { - newScene = scene; + if (ConditionsMatch(scene, playerState) && + (newScene == null || newScene.Conditions == null || + scene.Conditions.Count > newScene.Conditions.Count)) + { + newScene = scene; + } } + if (newScene == null) + _warnings.Add(new FicdownException(blockName, string.Format("Link to scene that is undefined for conditionals: {0}", target), anchor.LineNumber, anchor.ColNumber)); + return newScene; } - if (newScene == null) - throw new FicdownException(blockName, string.Format("Link to scene that is undefined for conditionals: {0}", target), anchor.LineNumber, anchor.ColNumber); - return newScene; + _warnings.Add(new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber)); + return null; } private bool ConditionsMatch(Scene scene, BitArray playerState) @@ -171,9 +177,10 @@ if (scene.Conditions == null) return true; scene.Conditions.ToList().ForEach(c => { - if(!_stateMatrix.ContainsKey(c.Key)) throw new FicdownException(scene.Name, string.Format("Conditional for undefined state: {0}", c.Key), scene.LineNumber); + if(!_stateMatrix.ContainsKey(c.Key)) + _warnings.Add(new FicdownException(scene.Name, string.Format("Conditional for undefined state: {0}", c.Key), scene.LineNumber)); }); - return scene.Conditions.All(c => playerState[_stateMatrix[c.Key]] == c.Value); + return scene.Conditions.Where(c => _stateMatrix.ContainsKey(c.Key)).All(c => playerState[_stateMatrix[c.Key]] == c.Value); } private PageState ClonePage(PageState page) diff --git a/Ficdown.Parser/Render/HtmlRenderer.cs b/Ficdown.Parser/Render/HtmlRenderer.cs index b1e521a..15e3d06 100644 --- a/Ficdown.Parser/Render/HtmlRenderer.cs +++ b/Ficdown.Parser/Render/HtmlRenderer.cs @@ -13,6 +13,8 @@ protected readonly Markdown Markdown; + public List Warnings { private get; set; } + public string IndexTemplate { get; set; } public string SceneTemplate { get; set; } public string StylesTemplate { get; set; } @@ -55,7 +57,7 @@ File.WriteAllText(Path.Combine(outPath, "styles.css"), StylesTemplate ?? Template.Styles); var content = page.Content; - foreach (var anchor in Utilities.GetInstance(page.Name).ParseAnchors(page.Content)) + foreach (var anchor in Utilities.GetInstance(Warnings, page.Name).ParseAnchors(page.Content)) { var newAnchor = string.Format("[{0}]({1}.html)", anchor.Text, anchor.Href.Target); content = content.Replace(anchor.Original, newAnchor); diff --git a/Ficdown.Parser/Render/IRenderer.cs b/Ficdown.Parser/Render/IRenderer.cs index a19648e..541047f 100644 --- a/Ficdown.Parser/Render/IRenderer.cs +++ b/Ficdown.Parser/Render/IRenderer.cs @@ -1,10 +1,11 @@ namespace Ficdown.Parser.Render { - using System.Security.Cryptography.X509Certificates; + using System.Collections.Generic; using Model.Parser; public interface IRenderer { + List Warnings { set; } string IndexTemplate { get; set; } string SceneTemplate { get; set; } string StylesTemplate { get; set; } From 3206a015e684c59af14656c3e0d1f3bc377384d3 Mon Sep 17 00:00:00 2001 From: Rudis Muiznieks Date: Sun, 12 May 2019 09:44:10 -0500 Subject: [PATCH 4/4] note about lint mode in help text --- Ficdown.Console/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ficdown.Console/Program.cs b/Ficdown.Console/Program.cs index d501264..d8508ad 100644 --- a/Ficdown.Console/Program.cs +++ b/Ficdown.Console/Program.cs @@ -198,7 +198,7 @@ Console.WriteLine( @"Usage: ficdown.exe --format (html|epub|lint) - --in ""/path/to/source.md"" + --in ""/path/to/source.md"" (lint reads sdtin) [--out ""/path/to/output""] [--template ""/path/to/template/dir""] [--images ""/path/to/images/dir""]