Merge pull request #8 from rudism/lint-mode

Lint mode
This commit is contained in:
Rudis Muiznieks 2019-05-12 09:50:02 -05:00 committed by GitHub
commit 5a1c3ecfae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 374 additions and 210 deletions

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Linq; using System.Linq;
using System.IO; using System.IO;
using Microsoft.SqlServer.Server;
using Parser; using Parser;
using Parser.Render; using Parser.Render;
using Parser.Model.Parser; using Parser.Model.Parser;
@ -16,8 +15,7 @@
{ {
if(e.ExceptionObject is FicdownException) if(e.ExceptionObject is FicdownException)
{ {
Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(e.ExceptionObject.ToString());
Console.Error.WriteLine(e.ExceptionObject.ToString());
Environment.Exit(3); Environment.Exit(3);
} }
}; };
@ -85,98 +83,112 @@
ShowHelp(); ShowHelp();
return 0; 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); if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile))
return 2;
}
if (!string.IsNullOrWhiteSpace(tempdir))
{
if (!Directory.Exists(tempdir))
{ {
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; return 2;
} }
if (!File.Exists(Path.Combine(tempdir, "index.html")) || if (string.IsNullOrWhiteSpace(output))
!File.Exists(Path.Combine(tempdir, "scene.html")) || if (format == "html")
!File.Exists(Path.Combine(tempdir, "styles.css"))) output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html");
{ else if (format == "epub")
Console.WriteLine( output = "output.epub";
@"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files."); else if(format == "lint")
} lintMode = true;
}
if (!string.IsNullOrWhiteSpace(images) && !Directory.Exists(images)) if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output)))
{ {
Console.WriteLine(@"Images directory {0} does not exist.", images); Console.WriteLine(@"Specified output {0} already exists.", output);
return 2; 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 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); var story = parser.ParseStory(storyText);
story.Orphans.ToList().ForEach(o => 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));
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;
});
IRenderer rend; if(!lintMode && parser.Warnings.Count() == 0)
switch (format)
{ {
case "html": IRenderer rend;
Directory.CreateDirectory(output); switch (format)
rend = new HtmlRenderer(language); {
break; case "html":
case "epub": Directory.CreateDirectory(output);
if (string.IsNullOrWhiteSpace(author)) rend = new HtmlRenderer(language);
{ break;
Console.WriteLine(@"Epub format requires the --author argument."); 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; return 1;
} }
rend = new EpubRenderer(author, bookid, language);
break; if (!string.IsNullOrWhiteSpace(tempdir))
default: {
ShowHelp(); rend.IndexTemplate = File.ReadAllText(Path.Combine(tempdir, "index.html"));
return 1; 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; return 0;
} }
@ -185,8 +197,8 @@
{ {
Console.WriteLine( Console.WriteLine(
@"Usage: ficdown.exe @"Usage: ficdown.exe
--format (html|epub) --format (html|epub|lint)
--in ""/path/to/source.md"" --in ""/path/to/source.md"" (lint reads sdtin)
[--out ""/path/to/output""] [--out ""/path/to/output""]
[--template ""/path/to/template/dir""] [--template ""/path/to/template/dir""]
[--images ""/path/to/images/dir""] [--images ""/path/to/images/dir""]

View File

@ -4,13 +4,19 @@
using Parser; using Parser;
using Model.Parser; using Model.Parser;
using Extensions; using Extensions;
using System.Collections.Generic;
public class BlockHandlerTests public class BlockHandlerTests
{ {
private BlockHandler NewBlockHandler
{
get { return new BlockHandler { Warnings = new List<FicdownException>() }; }
}
[Fact] [Fact]
public void NoStoryBlockThrowsException() public void NoStoryBlockThrowsException()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@"
## this file has no story ## this file has no story
just a lonely scene".ToLines()))); just a lonely scene".ToLines())));
@ -19,7 +25,7 @@ just a lonely scene".ToLines())));
[Fact] [Fact]
public void StoryWithNoAnchorThrowsException() public void StoryWithNoAnchorThrowsException()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@"
# my story # my story
doesn't link to a scene doesn't link to a scene
@ -30,17 +36,23 @@ nothing links here".ToLines())));
[Fact] [Fact]
public void StoriesWithFancyAnchorsThrowExceptions() public void StoriesWithFancyAnchorsThrowExceptions()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" bh.ParseBlocks(bh.ExtractBlocks(@"
# [my story](/a-scene?conditional) # [my story](/a-scene?conditional)
story with a conditional story with a conditional
## a scene ## a scene
this is a scene".ToLines()))); this is a scene".ToLines()));
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" Assert.NotEmpty(bh.Warnings);
bh = NewBlockHandler;
bh.ParseBlocks(bh.ExtractBlocks(@"
# [my story](/a-scene#toggle) # [my story](/a-scene#toggle)
story with a toggle story with a toggle
## a scene ## a scene
this is a scene".ToLines()))); this is a scene".ToLines()));
Assert.NotEmpty(bh.Warnings);
bh = NewBlockHandler;
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@"
# [my story](/a-scene#?conditional#toggle) # [my story](/a-scene#?conditional#toggle)
story with a conditional and a toggle story with a conditional and a toggle
@ -51,7 +63,7 @@ this is a scene".ToLines())));
[Fact] [Fact]
public void StoryLinkingToNonExistentSceneThrowsException() public void StoryLinkingToNonExistentSceneThrowsException()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@"
# [a story](/non-existent) # [a story](/non-existent)
this story links to a first scene that doesn't exist this story links to a first scene that doesn't exist
@ -62,7 +74,7 @@ this scene is so cold and lonely".ToLines())));
[Fact] [Fact]
public void StoryWithALegitAnchorParses() public void StoryWithALegitAnchorParses()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
bh.ParseBlocks(bh.ExtractBlocks(@" bh.ParseBlocks(bh.ExtractBlocks(@"
# [my story](/a-scene) # [my story](/a-scene)
story with a simple link story with a simple link
@ -73,8 +85,8 @@ this is a scene".ToLines()));
[Fact] [Fact]
public void StoryWithDuplicateActionsThrowsException() public void StoryWithDuplicateActionsThrowsException()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@" bh.ParseBlocks(bh.ExtractBlocks(@"
# [a story](/a-scene) # [a story](/a-scene)
this story is action-happy this story is action-happy
## a scene ## a scene
@ -84,13 +96,14 @@ this is an action
## another scene ## another scene
this is another scene this is another scene
### an action ### an action
oops, this is the same action!".ToLines()))); oops, this is the same action!".ToLines()));
Assert.NotEmpty(bh.Warnings);
} }
[Fact] [Fact]
public void StoryWithScenesAndActionsParses() public void StoryWithScenesAndActionsParses()
{ {
var bh = new BlockHandler(); var bh = NewBlockHandler;
var story = bh.ParseBlocks(bh.ExtractBlocks(@" var story = bh.ParseBlocks(bh.ExtractBlocks(@"
# [my story](/a-scene) # [my story](/a-scene)
story with a simple link story with a simple link

View File

@ -1,36 +1,40 @@
namespace Ficdown.Parser.Tests namespace Ficdown.Parser.Tests
{ {
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Model.Parser;
using Parser; using Parser;
using Xunit; using Xunit;
public class UtilityTests public class UtilityTests
{ {
private List<FicdownException> Warnings = new List<FicdownException>();
private Utilities Utilities private Utilities Utilities
{ {
get { return Utilities.GetInstance("none", 0); } get { return Utilities.GetInstance(Warnings, "none", 0); }
} }
[Fact] [Fact]
public void FullAnchorMatches() public void FullAnchorMatches()
{ {
var anchorStr = @"[Link text](/target-scene)"; var anchorStr = @"[Link text](/target-scene)";
var anchor = Utilities.ParseAnchor(anchorStr); var anchor = Utilities.ParseAnchor(anchorStr, 0, 0);
Assert.Equal(anchorStr, anchor.Original); Assert.Equal(anchorStr, anchor.Original);
anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")"; anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")";
anchor = Utilities.ParseAnchor(anchorStr); anchor = Utilities.ParseAnchor(anchorStr, 0, 0);
Assert.Equal(anchorStr, anchor.Original); Assert.Equal(anchorStr, anchor.Original);
anchorStr = @"[Link text](""Title text"")"; anchorStr = @"[Link text](""Title text"")";
anchor = Utilities.ParseAnchor(anchorStr); anchor = Utilities.ParseAnchor(anchorStr, 0, 0);
Assert.Equal(anchorStr, anchor.Original); Assert.Equal(anchorStr, anchor.Original);
} }
[Fact] [Fact]
public void AnchorWithTargetMatches() 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("Link text", anchor.Text);
Assert.Equal("target-scene", anchor.Href.Target); Assert.Equal("target-scene", anchor.Href.Target);
} }
@ -38,15 +42,15 @@
[Fact] [Fact]
public void AnchorsWithConditionsMatch() 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.Equal("Link text", anchor.Text);
Assert.True(anchor.Href.Conditions["condition-state"]); 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.Equal("Link text", anchor.Text);
Assert.False(anchor.Href.Conditions["condition-state"]); 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.Equal("Link text", anchor.Text);
Assert.True(anchor.Href.Conditions["condition-1"]); Assert.True(anchor.Href.Conditions["condition-1"]);
Assert.False(anchor.Href.Conditions["condition-2"]); Assert.False(anchor.Href.Conditions["condition-2"]);
@ -55,15 +59,15 @@
[Fact] [Fact]
public void AnchorsWithTogglesMatch() 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("Link text", anchor.Text);
Assert.Equal("#toggle-state", anchor.Href.Original); 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("Link text", anchor.Text);
Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original); 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("Link text", anchor.Text);
Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original); Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original);
} }
@ -71,11 +75,11 @@
[Fact] [Fact]
public void AnchorsWithTitlesMatch() 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("Link text", anchor.Text);
Assert.Equal("Title text", anchor.Title); 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("Talking to Kid", anchor.Text);
Assert.Equal("Lobby", anchor.Title); Assert.Equal("Lobby", anchor.Title);
} }
@ -83,20 +87,20 @@
[Fact] [Fact]
public void ComplexAnchorsMatch() 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("Link text", anchor.Text);
Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Href.Original); Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Href.Original);
Assert.Equal("Title text", anchor.Title); 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("Link text", anchor.Text);
Assert.Equal("/target-scene#toggle-state", anchor.Href.Original); 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("Link text", anchor.Text);
Assert.Equal("/target-scene?condition-state", anchor.Href.Original); 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("Link text", anchor.Text);
Assert.Equal("?condition-state#toggle-state", anchor.Href.Original); Assert.Equal("?condition-state#toggle-state", anchor.Href.Original);
} }

View File

@ -4,6 +4,7 @@
namespace Ficdown.Parser namespace Ficdown.Parser
{ {
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Model.Parser; using Model.Parser;
using Parser; using Parser;
@ -11,25 +12,53 @@ namespace Ficdown.Parser
public class FicdownParser public class FicdownParser
{ {
public List<FicdownException> Warnings { get; private set; }
private IBlockHandler _blockHandler; private IBlockHandler _blockHandler;
internal IBlockHandler BlockHandler internal IBlockHandler BlockHandler
{ {
get { return _blockHandler ?? (_blockHandler = new BlockHandler()); } get
set { _blockHandler = value; } {
return _blockHandler ??
(_blockHandler = new BlockHandler { Warnings = Warnings });
}
set
{
_blockHandler = value;
_blockHandler.Warnings = Warnings;
}
} }
private IGameTraverser _gameTraverser; private IGameTraverser _gameTraverser;
internal IGameTraverser GameTraverser internal IGameTraverser GameTraverser
{ {
get { return _gameTraverser ?? (_gameTraverser = new GameTraverser()); } get { return _gameTraverser ??
set { _gameTraverser = value; } (_gameTraverser = new GameTraverser { Warnings = Warnings }); }
set
{
_gameTraverser = value;
_gameTraverser.Warnings = Warnings;
}
} }
private IStateResolver _stateResolver; private IStateResolver _stateResolver;
internal IStateResolver StateResolver internal IStateResolver StateResolver
{ {
get { return _stateResolver ?? (_stateResolver = new StateResolver()); } get
set { _stateResolver = value; } {
return _stateResolver ??
(_stateResolver = new StateResolver { Warnings = Warnings });
}
set
{
_stateResolver = value;
_stateResolver.Warnings = Warnings;
}
}
public FicdownParser()
{
Warnings = new List<FicdownException>();
} }
public ResolvedStory ParseStory(string storyText) public ResolvedStory ParseStory(string storyText)
@ -37,6 +66,23 @@ namespace Ficdown.Parser
var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None); var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None);
var blocks = BlockHandler.ExtractBlocks(lines); var blocks = BlockHandler.ExtractBlocks(lines);
var story = BlockHandler.ParseBlocks(blocks); 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()))
Warnings.Add(new FicdownException(scene.Name, string.Format("Scene defined again on line {0}", otherScene.LineNumber), scene.LineNumber));
}
}
}
GameTraverser.Story = story; GameTraverser.Story = story;
var resolved = StateResolver.Resolve(GameTraverser.Enumerate(), story); var resolved = StateResolver.Resolve(GameTraverser.Enumerate(), story);
resolved.Orphans = GameTraverser.OrphanedScenes.Select(o => new Orphan resolved.Orphans = GameTraverser.OrphanedScenes.Select(o => new Orphan

View File

@ -6,5 +6,7 @@
public string Text { get; set; } public string Text { get; set; }
public Href Href { get; set; } public Href Href { get; set; }
public string Title { get; set; } public string Title { get; set; }
public int LineNumber { get; set; }
public int ColNumber { get; set; }
} }
} }

View File

@ -6,24 +6,25 @@ namespace Ficdown.Parser.Model.Parser
{ {
public string BlockName { get; private set; } public string BlockName { get; private set; }
public int? LineNumber { 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; BlockName = blockName;
LineNumber = lineNumber; LineNumber = lineNumber;
ColNumber = colNumber;
} }
public FicdownException(string message) : base(message) { } public FicdownException(string message) : base(message) { }
public override string ToString() public override string ToString()
{ {
return !string.IsNullOrEmpty(BlockName) return string.Format("Error L{0},{1}: {2}",
? string.Format("Error in block \"{0}\" (Line {1}): {2}", LineNumber ?? 1,
BlockName, ColNumber ?? 1,
LineNumber.HasValue !string.IsNullOrEmpty(BlockName)
? LineNumber.ToString() ? string.Format("\"{0}\": {1}", BlockName, Message)
: "unknown", Message) : Message);
: string.Format("Error: {0}", Message);
} }
} }
} }

View File

@ -4,6 +4,7 @@
{ {
public int Id { get; set; } public int Id { get; set; }
public string Toggle { get; set; } public string Toggle { get; set; }
public string RawDescription { get; set; }
public string Description { get; set; } public string Description { get; set; }
public int LineNumber { get; set; } public int LineNumber { get; set; }
public bool Visited { get; set; } public bool Visited { get; set; }

View File

@ -8,6 +8,7 @@
public string Name { get; set; } public string Name { get; set; }
public string Key { get; set; } public string Key { get; set; }
public string Description { get; set; } public string Description { get; set; }
public string RawDescription { get; set; }
public IDictionary<string, bool> Conditions { get; set; } public IDictionary<string, bool> Conditions { get; set; }
public int LineNumber { get; set; } public int LineNumber { get; set; }
public bool Visited { get; set; } public bool Visited { get; set; }

View File

@ -10,6 +10,8 @@
internal class BlockHandler : IBlockHandler internal class BlockHandler : IBlockHandler
{ {
public List<FicdownException> Warnings { get; set; }
public IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines) public IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines)
{ {
var blocks = new List<Block>(); var blocks = new List<Block>();
@ -45,22 +47,22 @@
public Story ParseBlocks(IEnumerable<Block> blocks) public Story ParseBlocks(IEnumerable<Block> blocks)
{ {
// get the story // get the story
var storyBlock = blocks.SingleOrDefault(b => b.Type == BlockType.Story); var storyBlocks = blocks.Where(b => b.Type == BlockType.Story);
if(storyBlock == null) throw new FicdownException("No story block found"); if(storyBlocks.Count() == 0) throw new FicdownException("No story block found");
if(storyBlocks.Count() > 1) throw new FicdownException("More than one story block found");
Anchor storyAnchor; var storyBlock = storyBlocks.Single();
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); throw new FicdownException(storyBlock.Name, "Story name must be an anchor pointing to the first scene", storyBlock.LineNumber);
}
catch(FicdownException ex)
{
throw new FicdownException(ex.BlockName, ex.LineNumber, "Story block must be an anchor pointing to the first scene");
} }
if (storyAnchor.Href.Target == null || storyAnchor.Href.Conditions != null || if (storyAnchor.Href.Target == null || storyAnchor.Href.Conditions != null ||
storyAnchor.Href.Toggles != null) storyAnchor.Href.Toggles != null)
throw new FicdownException(storyBlock.Name, storyBlock.LineNumber, "Story href should only have target"); Warnings.Add(new FicdownException(storyBlock.Name, "Story href should only have a target", storyBlock.LineNumber));
var story = new Story var story = new Story
{ {
@ -88,11 +90,11 @@
var a = blocks.First(b => b.Type == BlockType.Action && blocks.Any(d => b != d && BlockToAction(b, 0).Toggle == BlockToAction(d, 0).Toggle)); 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 actionA = BlockToAction(a, a.LineNumber);
var dupe = blocks.First(b => b.Type == BlockType.Action && b != a && BlockToAction(b, 0).Toggle == actionA.Toggle); 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)); 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)) 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; story.FirstScene = storyAnchor.Href.Target;
return story; return story;
@ -105,22 +107,23 @@
{ {
Id = id, Id = id,
LineNumber = block.LineNumber, LineNumber = block.LineNumber,
RawDescription = string.Join("\n", block.Lines.Select(l => l.Text)),
Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim() Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim()
}; };
try 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);
scene.Name = sceneName.Title != null ? sceneName.Title.Trim() : sceneName.Text.Trim(); 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) 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)); Warnings.Add(new FicdownException(block.Name, "Scene href should only have conditions", block.LineNumber));
scene.Conditions = sceneName.Href.Conditions; scene.Conditions = sceneName.Href.Conditions;
} }
catch(FicdownException) else
{ {
scene.Name = block.Name.Trim(); 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; return scene;
@ -131,7 +134,8 @@
return new Action return new Action
{ {
Id = id, 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(), Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim(),
LineNumber = block.LineNumber LineNumber = block.LineNumber
}; };

View File

@ -6,6 +6,7 @@
internal interface IBlockHandler internal interface IBlockHandler
{ {
List<FicdownException> Warnings { set; }
IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines); IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines);
Story ParseBlocks(IEnumerable<Block> blocks); Story ParseBlocks(IEnumerable<Block> blocks);
} }

View File

@ -7,6 +7,7 @@
internal interface IStateResolver internal interface IStateResolver
{ {
List<FicdownException> Warnings { set; }
ResolvedStory Resolve(IEnumerable<PageState> pages, Story story); ResolvedStory Resolve(IEnumerable<PageState> pages, Story story);
} }
} }

View File

@ -1,6 +1,5 @@
namespace Ficdown.Parser.Parser namespace Ficdown.Parser.Parser
{ {
using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -15,6 +15,8 @@
private readonly HashSet<string> _usedNames; private readonly HashSet<string> _usedNames;
private Story _story; private Story _story;
public List<FicdownException> Warnings { private get; set; }
public StateResolver() public StateResolver()
{ {
_pageNames = new Dictionary<string, string>(); _pageNames = new Dictionary<string, string>();
@ -43,12 +45,15 @@
private string ResolveAnchor(string blockName, int lineNumber, Anchor anchor, IDictionary<string, bool> playerState, string targetHash) private string ResolveAnchor(string blockName, int lineNumber, Anchor anchor, IDictionary<string, bool> playerState, string targetHash)
{ {
var text = anchor.Text; 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 satisfied = Utilities.GetInstance(Warnings, blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions);
var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(text); var alts = Utilities.GetInstance(Warnings, blockName, lineNumber).ParseConditionalText(anchor);
var replace = alts[satisfied]; if(alts != null)
text = RegexLib.EscapeChar.Replace(replace, string.Empty); {
var replace = alts[satisfied];
text = RegexLib.EscapeChar.Replace(replace, string.Empty);
}
} }
return !string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(targetHash) return !string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(targetHash)
? string.Format("[{0}](/{1})", text, GetPageNameForHash(targetHash)) ? string.Format("[{0}](/{1})", text, GetPageNameForHash(targetHash))
@ -67,7 +72,7 @@
if (page.State.ActionsToShow[i]) if (page.State.ActionsToShow[i])
{ {
var actionTuple = _story.Actions.Single(a => a.Value.Id == i + 1); 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(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.RawDescription);
var anchorDict = GetStateDictionary(page); var anchorDict = GetStateDictionary(page);
if ( if (
actionAnchors.Any( actionAnchors.Any(
@ -86,7 +91,7 @@
} }
} }
var anchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.Description); var anchors = Utilities.GetInstance(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.RawDescription);
var stateDict = GetStateDictionary(page); var stateDict = GetStateDictionary(page);
var text = var text =
RegexLib.EmptyListItem.Replace( RegexLib.EmptyListItem.Replace(

View File

@ -1,7 +1,5 @@
 namespace Ficdown.Parser.Parser
namespace Ficdown.Parser.Parser
{ {
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -9,19 +7,23 @@ namespace Ficdown.Parser.Parser
internal class Utilities internal class Utilities
{ {
public static Utilities GetInstance(string blockName, int lineNumber) private List<FicdownException> _warnings { get; set; }
public static Utilities GetInstance(List<FicdownException> warnings, string blockName, int lineNumber)
{ {
return new Utilities return new Utilities
{ {
_warnings = warnings,
_blockName = blockName, _blockName = blockName,
_lineNumber = lineNumber _lineNumber = lineNumber
}; };
} }
public static Utilities GetInstance(string blockName) public static Utilities GetInstance(List<FicdownException> warnings, string blockName)
{ {
return new Utilities return new Utilities
{ {
_warnings = warnings,
_blockName = blockName _blockName = blockName
}; };
} }
@ -34,7 +36,7 @@ namespace Ficdown.Parser.Parser
return Regex.Replace(Regex.Replace(raw.ToLower(), @"^\W+|\W+$", string.Empty), @"\W+", "-"); 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); var match = RegexLib.Href.Match(href);
if (match.Success) if (match.Success)
@ -57,27 +59,55 @@ namespace Ficdown.Parser.Parser
: null : null
}; };
} }
throw new FicdownException(_blockName, _lineNumber, string.Format("Invalid href: {0}", href)); _warnings.Add(new FicdownException(_blockName, string.Format("Invalid href: {0}", href), lineNumber, colNumber));
return null;
} }
public Anchor ParseAnchor(string anchorText) public Anchor ParseAnchor(string anchorText, int lineNumber, int colNumber)
{ {
var match = RegexLib.Anchors.Match(anchorText); var match = RegexLib.Anchors.Match(anchorText);
if (!match.Success) throw new FicdownException(_blockName, _lineNumber, string.Format("Invalid anchor: {0}", anchorText)); if (!match.Success)
return MatchToAnchor(match); {
_warnings.Add(new FicdownException(_blockName, string.Format("Invalid anchor: {0}", anchorText), lineNumber, colNumber));
return null;
}
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<Anchor> ParseAnchors(string text) public IList<Anchor> ParseAnchors(string text)
{ {
var matches = RegexLib.Anchors.Matches(text); var matches = RegexLib.Anchors.Matches(text);
return matches.Cast<Match>().Select(MatchToAnchor).ToList(); return matches.Cast<Match>().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 astr = match.Groups["anchor"].Value;
var txstr = match.Groups["text"].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; var hrefstr = match.Groups["href"].Value;
if (hrefstr.StartsWith(@"""")) if (hrefstr.StartsWith(@""""))
{ {
@ -89,14 +119,21 @@ namespace Ficdown.Parser.Parser
Original = !string.IsNullOrEmpty(astr) ? astr : null, Original = !string.IsNullOrEmpty(astr) ? astr : null,
Text = !string.IsNullOrEmpty(txstr) ? txstr : null, Text = !string.IsNullOrEmpty(txstr) ? txstr : null,
Title = ttstr, Title = ttstr,
Href = ParseHref(hrefstr) Href = ParseHref(hrefstr, lineNumber, colNumber),
LineNumber = lineNumber,
ColNumber = colNumber
}; };
} }
public IDictionary<bool, string> ParseConditionalText(string text) public IDictionary<bool, string> ParseConditionalText(Anchor anchor)
{ {
var match = RegexLib.ConditionalText.Match(text); var match = RegexLib.ConditionalText.Match(anchor.Text);
if (!match.Success) throw new FicdownException(_blockName, _lineNumber, string.Format(@"Invalid conditional text: {0}", text)); if (!match.Success)
{
_warnings.Add(new FicdownException(_blockName, string.Format(@"Invalid conditional text: {0}", anchor.Text), anchor.LineNumber, anchor.ColNumber));
return null;
}
return new Dictionary<bool, string> return new Dictionary<bool, string>
{ {
{true, match.Groups["true"].Value}, {true, match.Groups["true"].Value},

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Model.Parser;
using Model.Player; using Model.Player;
using Model.Story; using Model.Story;
using Parser; using Parser;
@ -17,6 +18,8 @@
private IDictionary<int, Action> _actionMatrix; private IDictionary<int, Action> _actionMatrix;
private bool _wasRun = false; private bool _wasRun = false;
public List<FicdownException> Warnings { private get; set; }
private Story _story; private Story _story;
public Story Story public Story Story
{ {
@ -25,7 +28,7 @@
{ {
_story = value; _story = value;
_actionMatrix = _story.Actions.ToDictionary(a => a.Value.Id, a => a.Value); _actionMatrix = _story.Actions.ToDictionary(a => a.Value.Id, a => a.Value);
_manager = new StateManager(_story); _manager = new StateManager(_story, Warnings);
_processingQueue = new Queue<StateQueueItem>(); _processingQueue = new Queue<StateQueueItem>();
_processed = new Dictionary<string, PageState>(); _processed = new Dictionary<string, PageState>();
_compressed = new Dictionary<string, PageState>(); _compressed = new Dictionary<string, PageState>();
@ -125,15 +128,15 @@
var states = new HashSet<string>(); var states = new HashSet<string>();
var anchors = Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.Description).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)) foreach (var action in GetActionsForPage(currentState.Page))
{ {
action.Visited = true; action.Visited = true;
anchors.AddRange(Utilities.GetInstance(action.Toggle, action.LineNumber).ParseAnchors(action.Description)); anchors.AddRange(Utilities.GetInstance(Warnings, action.Toggle, action.LineNumber).ParseAnchors(action.RawDescription));
} }
var conditionals = var conditionals =
anchors.SelectMany( 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() .Distinct()
.ToArray(); .ToArray();
var hasFirstSeen = RegexLib.BlockQuotes.IsMatch(currentState.Page.Scene.Description); var hasFirstSeen = RegexLib.BlockQuotes.IsMatch(currentState.Page.Scene.Description);
@ -143,32 +146,46 @@
// signal to previous scenes that this scene's used conditionals are important // signal to previous scenes that this scene's used conditionals are important
if(currentState.Page.Scene.Conditions != null) if(currentState.Page.Scene.Conditions != null)
foreach (var conditional in currentState.Page.Scene.Conditions) 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 != 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 != null ? anchor.LineNumber : currentState.Page.Scene.LineNumber, anchor != null ? anchor.ColNumber : 1);
}
// signal to previous scenes if this scene has first-seen text // signal to previous scenes if this scene has first-seen text
if (hasFirstSeen) _manager.ToggleSeenSceneOn(affected, currentState.Page.Scene.Id); 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 // don't follow links that would be hidden
if (anchor.Href.Conditions != null && if (anchor.Href.Conditions != null &&
string.IsNullOrEmpty( string.IsNullOrEmpty(
Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor.Text)[ Utilities.GetInstance(Warnings, 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).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page),
anchor.Href.Conditions)])) continue; anchor.Href.Conditions)])) continue;
var newState = _manager.ResolveNewState(anchor, currentState.Page); var newState = _manager.ResolveNewState(anchor, currentState.Page);
if (!currentState.Page.Links.ContainsKey(anchor.Original)) if(newState.Scene != null)
currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash))
{ {
states.Add(newState.UniqueHash); if (!currentState.Page.Links.ContainsKey(anchor.Original))
var newAffected = new List<State>(currentState.AffectedStates); currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
newAffected.Add(newState.AffectedState);
_processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected}); if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash))
{
states.Add(newState.UniqueHash);
var newAffected = new List<State>(currentState.AffectedStates);
newAffected.Add(newState.AffectedState);
_processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected});
}
} }
} }
} }

View File

@ -1,11 +1,13 @@
namespace Ficdown.Parser.Player namespace Ficdown.Parser.Player
{ {
using System.Collections.Generic; using System.Collections.Generic;
using Model.Parser;
using Model.Player; using Model.Player;
using Model.Story; using Model.Story;
internal interface IGameTraverser internal interface IGameTraverser
{ {
List<FicdownException> Warnings { set; }
Story Story { get; set; } Story Story { get; set; }
IEnumerable<PageState> Enumerate(); IEnumerable<PageState> Enumerate();
IEnumerable<Scene> OrphanedScenes { get; } IEnumerable<Scene> OrphanedScenes { get; }

View File

@ -16,8 +16,11 @@
private readonly int _sceneCount; private readonly int _sceneCount;
private readonly int _actionCount; private readonly int _actionCount;
public StateManager(Story story) private List<FicdownException> _warnings { get; set; }
public StateManager(Story story, List<FicdownException> warnings)
{ {
_warnings = warnings;
_story = story; _story = story;
var allScenes = _story.Scenes.SelectMany(s => s.Value); var allScenes = _story.Scenes.SelectMany(s => s.Value);
_sceneCount = allScenes.Max(s => s.Id); _sceneCount = allScenes.Max(s => s.Id);
@ -28,10 +31,10 @@
var toggle in var toggle in
allScenes.SelectMany( allScenes.SelectMany(
sc => sc =>
Utilities.GetInstance(sc.Name, sc.LineNumber).ParseAnchors(sc.Description) Utilities.GetInstance(_warnings, sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription)
.SelectMany( .SelectMany(
a => a =>
a.Href.Toggles != null a.Href != null && a.Href.Toggles != null
? a.Href.Toggles.Where(t => !_stateMatrix.ContainsKey(t)) ? a.Href.Toggles.Where(t => !_stateMatrix.ContainsKey(t))
: new string[] {}))) : new string[] {})))
{ {
@ -43,6 +46,12 @@
{ {
get get
{ {
var scene = _story.Scenes[_story.FirstScene].Where(s => s.Conditions == null);
if(scene.Count() == 0)
throw new FicdownException(_story.Name, string.Format("Story links to undefined scene: {0}", _story.FirstScene));
if(scene.Count() > 1)
_warnings.Add(new FicdownException(_story.Name, string.Format("Story links to scene that is defined more than once: {0}", _story.FirstScene)));
return new PageState return new PageState
{ {
Id = Guid.Empty, Id = Guid.Empty,
@ -61,7 +70,7 @@
ActionsToShow = new BitArray(_actionCount), ActionsToShow = new BitArray(_actionCount),
ActionFirstToggles = null ActionFirstToggles = null
}, },
Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null), Scene = scene.First(),
StateMatrix = _stateMatrix StateMatrix = _stateMatrix
}; };
} }
@ -83,7 +92,7 @@
if(actionFirstToggles == null) actionFirstToggles = new List<bool>(); if(actionFirstToggles == null) actionFirstToggles = new List<bool>();
newState.State.ActionsToShow[_story.Actions[toggle].Id - 1] = true; newState.State.ActionsToShow[_story.Actions[toggle].Id - 1] = true;
if ( if (
Utilities.GetInstance(_story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].Description) 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))) .Any(a => a.Href.Conditions != null && a.Href.Conditions.ContainsKey(toggle)))
actionFirstToggles.Add(!current.State.PlayerState[_stateMatrix[toggle]]); actionFirstToggles.Add(!current.State.PlayerState[_stateMatrix[toggle]]);
} }
@ -93,13 +102,16 @@
newState.State.ActionFirstToggles = actionFirstToggles != null newState.State.ActionFirstToggles = actionFirstToggles != null
? new BitArray(actionFirstToggles.ToArray()) ? new BitArray(actionFirstToggles.ToArray())
: null; : 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; return newState;
} }
public void ToggleStateOn(State state, string toggle) public void ToggleStateOn(State state, string toggle, string blockName, int lineNumber, int colNumber)
{ {
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) public void ToggleSeenSceneOn(State state, int sceneId)
@ -138,24 +150,26 @@
return GetUniqueHash(compressed, page.Scene.Key); 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)) if (_story.Scenes.ContainsKey(target))
throw new FicdownException(blockName, lineNumber, string.Format("Encountered link to non-existent scene: {0}", target));
Scene newScene = null;
foreach (var scene in _story.Scenes[target])
{ {
if (ConditionsMatch(scene, playerState) && Scene newScene = null;
(newScene == null || newScene.Conditions == null || foreach (var scene in _story.Scenes[target])
scene.Conditions.Count > newScene.Conditions.Count))
{ {
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) _warnings.Add(new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber));
throw new FicdownException(blockName, lineNumber, string.Format("Scene {0} reached with unmatched player state", target)); return null;
return newScene;
} }
private bool ConditionsMatch(Scene scene, BitArray playerState) private bool ConditionsMatch(Scene scene, BitArray playerState)
@ -163,9 +177,10 @@
if (scene.Conditions == null) return true; if (scene.Conditions == null) return true;
scene.Conditions.ToList().ForEach(c => 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))
_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) private PageState ClonePage(PageState page)

View File

@ -13,6 +13,8 @@
protected readonly Markdown Markdown; protected readonly Markdown Markdown;
public List<FicdownException> Warnings { private get; set; }
public string IndexTemplate { get; set; } public string IndexTemplate { get; set; }
public string SceneTemplate { get; set; } public string SceneTemplate { get; set; }
public string StylesTemplate { get; set; } public string StylesTemplate { get; set; }
@ -55,7 +57,7 @@
File.WriteAllText(Path.Combine(outPath, "styles.css"), StylesTemplate ?? Template.Styles); File.WriteAllText(Path.Combine(outPath, "styles.css"), StylesTemplate ?? Template.Styles);
var content = page.Content; 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); var newAnchor = string.Format("[{0}]({1}.html)", anchor.Text, anchor.Href.Target);
content = content.Replace(anchor.Original, newAnchor); content = content.Replace(anchor.Original, newAnchor);

View File

@ -1,10 +1,11 @@
namespace Ficdown.Parser.Render namespace Ficdown.Parser.Render
{ {
using System.Security.Cryptography.X509Certificates; using System.Collections.Generic;
using Model.Parser; using Model.Parser;
public interface IRenderer public interface IRenderer
{ {
List<FicdownException> Warnings { set; }
string IndexTemplate { get; set; } string IndexTemplate { get; set; }
string SceneTemplate { get; set; } string SceneTemplate { get; set; }
string StylesTemplate { get; set; } string StylesTemplate { get; set; }