Compare commits
14 Commits
Author | SHA1 | Date |
---|---|---|
Rudis Muiznieks | f09aab9f82 | |
Rudis Muiznieks | 03a612bfc0 | |
Rudis Muiznieks | 3a81abf1b1 | |
Rudis Muiznieks | 7970c6cd4b | |
Rudis Muiznieks | f7cab514cc | |
Rudis Muiznieks | 44ed5f165b | |
Rudis Muiznieks | bace78ff2b | |
Rudis Muiznieks | 5a1c3ecfae | |
Rudis Muiznieks | 3206a015e6 | |
Rudis Muiznieks | 85a06d77cd | |
Rudis Muiznieks | b75a672ce2 | |
Rudis Muiznieks | 4ff4693fa3 | |
Rudis Muiznieks | ed54a71fb3 | |
Rudis Muiznieks | 5ace751b4b |
|
@ -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,6 +83,14 @@
|
||||||
ShowHelp();
|
ShowHelp();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.Initialize(debug);
|
||||||
|
var logger = Logger.GetLogger<Program>();
|
||||||
|
|
||||||
|
var lintMode = format == "lint";
|
||||||
|
|
||||||
|
if(!lintMode)
|
||||||
|
{
|
||||||
if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile))
|
if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile))
|
||||||
{
|
{
|
||||||
ShowHelp();
|
ShowHelp();
|
||||||
|
@ -92,7 +98,7 @@
|
||||||
}
|
}
|
||||||
if (!File.Exists(infile))
|
if (!File.Exists(infile))
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Source file {0} not found.", infile);
|
logger.Error($"Source file {infile} not found.");
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (string.IsNullOrWhiteSpace(output))
|
if (string.IsNullOrWhiteSpace(output))
|
||||||
|
@ -100,49 +106,57 @@
|
||||||
output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html");
|
output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html");
|
||||||
else if (format == "epub")
|
else if (format == "epub")
|
||||||
output = "output.epub";
|
output = "output.epub";
|
||||||
|
else if(format == "lint")
|
||||||
|
lintMode = true;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output)))
|
if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output)))
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Specified output {0} already exists.", output);
|
logger.Error($"Specified output {output} already exists.");
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (!string.IsNullOrWhiteSpace(tempdir))
|
if (!string.IsNullOrWhiteSpace(tempdir))
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(tempdir))
|
if (!Directory.Exists(tempdir))
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Template directory {0} does not exist.", tempdir);
|
logger.Error($"Template directory {tempdir} does not exist.");
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
if (!File.Exists(Path.Combine(tempdir, "index.html")) ||
|
if (!File.Exists(Path.Combine(tempdir, "index.html")) ||
|
||||||
!File.Exists(Path.Combine(tempdir, "scene.html")) ||
|
!File.Exists(Path.Combine(tempdir, "scene.html")) ||
|
||||||
!File.Exists(Path.Combine(tempdir, "styles.css")))
|
!File.Exists(Path.Combine(tempdir, "styles.css")))
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
logger.Error(
|
||||||
@"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files.");
|
@"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(images) && !Directory.Exists(images))
|
if (!string.IsNullOrWhiteSpace(images) && !Directory.Exists(images))
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Images directory {0} does not exist.", images);
|
logger.Error($"Images directory {images} does not exist.");
|
||||||
return 2;
|
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);
|
||||||
|
logger.Log(@"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 => logger.Raw(s));
|
||||||
{
|
story.Orphans.ToList().ForEach(o => logger.Raw($"Warning L{o.LineNumber},1: \"{o.Name}\": Unreachable {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;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if(!lintMode && parser.Warnings.Count() == 0)
|
||||||
|
{
|
||||||
IRenderer rend;
|
IRenderer rend;
|
||||||
switch (format)
|
switch (format)
|
||||||
{
|
{
|
||||||
|
@ -153,7 +167,7 @@
|
||||||
case "epub":
|
case "epub":
|
||||||
if (string.IsNullOrWhiteSpace(author))
|
if (string.IsNullOrWhiteSpace(author))
|
||||||
{
|
{
|
||||||
Console.WriteLine(@"Epub format requires the --author argument.");
|
logger.Error(@"Epub format requires the --author argument.");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
rend = new EpubRenderer(author, bookid, language);
|
rend = new EpubRenderer(author, bookid, language);
|
||||||
|
@ -172,11 +186,12 @@
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images;
|
if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images;
|
||||||
|
|
||||||
Console.WriteLine(@"Rendering story...");
|
logger.Log(@"Rendering story...");
|
||||||
|
|
||||||
rend.Render(story, output, debug);
|
rend.Render(story, output, debug);
|
||||||
|
|
||||||
Console.WriteLine(@"Done.");
|
logger.Log(@"Done.");
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +199,7 @@
|
||||||
private static void ShowHelp()
|
private static void ShowHelp()
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
@"Usage: ficdown.exe
|
@"Usage: ficdown
|
||||||
--format (html|epub)
|
--format (html|epub)
|
||||||
--in ""/path/to/source.md""
|
--in ""/path/to/source.md""
|
||||||
[--out ""/path/to/output""]
|
[--out ""/path/to/output""]
|
||||||
|
@ -193,7 +208,9 @@
|
||||||
[--author ""Author Name""]
|
[--author ""Author Name""]
|
||||||
[--bookid ""ePub Book ID""]
|
[--bookid ""ePub Book ID""]
|
||||||
[--language ""language""]
|
[--language ""language""]
|
||||||
[--debug]");
|
[--debug]
|
||||||
|
or: ficdown --format lint
|
||||||
|
(reads input from stdin)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CanParseValidStoryFile()
|
public void CanParseValidStoryFile()
|
||||||
{
|
{
|
||||||
|
Logger.Initialize(true);
|
||||||
var parser = new FicdownParser();
|
var parser = new FicdownParser();
|
||||||
var storyText = File.ReadAllText(Path.Combine(Template.BaseDir, "TestStories", "CloakOfDarkness.md"));
|
var storyText = File.ReadAllText(Path.Combine(Template.BaseDir, "TestStories", "CloakOfDarkness.md"));
|
||||||
var story = parser.ParseStory(storyText);
|
var story = parser.ParseStory(storyText);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,32 +12,81 @@ namespace Ficdown.Parser
|
||||||
|
|
||||||
public class FicdownParser
|
public class FicdownParser
|
||||||
{
|
{
|
||||||
|
private static Logger _logger = Logger.GetLogger<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)
|
||||||
{
|
{
|
||||||
var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None);
|
var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None);
|
||||||
|
_logger.Debug($"Parsed {lines.Length} lines.");
|
||||||
var blocks = BlockHandler.ExtractBlocks(lines);
|
var blocks = BlockHandler.ExtractBlocks(lines);
|
||||||
|
_logger.Debug($"Extracted {blocks.Count()} blocks.");
|
||||||
var story = BlockHandler.ParseBlocks(blocks);
|
var story = BlockHandler.ParseBlocks(blocks);
|
||||||
|
_logger.Debug("Finished initial story breakdown.");
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Epub4Net" Version="1.2.0" />
|
<PackageReference Include="Epub4Net" Version="1.2.0" />
|
||||||
<PackageReference Include="Ionic.Zip" Version="1.9.1.8" />
|
<PackageReference Include="Ionic.Zip" Version="1.9.1.8" />
|
||||||
<PackageReference Include="MarkdownSharp" Version="2.0.5" />
|
<PackageReference Include="Markdig" Version="0.17.1" />
|
||||||
|
<PackageReference Include="System.Security.Permissions" />
|
||||||
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
namespace Ficdown.Parser
|
||||||
|
{
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
public class Logger
|
||||||
|
{
|
||||||
|
private static bool _initialized = false;
|
||||||
|
private static bool _debug = false;
|
||||||
|
private static Dictionary<Type, Logger> _cache;
|
||||||
|
|
||||||
|
public Type Type { get; private set; }
|
||||||
|
|
||||||
|
private Logger(Type type)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Initialize(bool debug)
|
||||||
|
{
|
||||||
|
_debug = debug;
|
||||||
|
_cache = new Dictionary<Type, Logger>();
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Logger GetLogger<T>()
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
lock(_cache)
|
||||||
|
{
|
||||||
|
if(!_cache.ContainsKey(type))
|
||||||
|
_cache.Add(type, new Logger(type));
|
||||||
|
}
|
||||||
|
return _cache[type];
|
||||||
|
}
|
||||||
|
|
||||||
|
private string Decorate(string message)
|
||||||
|
{
|
||||||
|
return $"{DateTime.Now.ToString("")} <{Type.Name}> {message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Raw(string message)
|
||||||
|
{
|
||||||
|
Console.WriteLine(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Log(string message)
|
||||||
|
{
|
||||||
|
Raw(Decorate(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string message)
|
||||||
|
{
|
||||||
|
if(!_debug) return;
|
||||||
|
Log($"DEBUG: {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(string message, Exception ex = null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(Decorate($"ERROR: {message}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
internal class PageState
|
internal class PageState
|
||||||
{
|
{
|
||||||
|
public StateManager Manager { get; set; }
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Scene Scene { get; set; }
|
public Scene Scene { get; set; }
|
||||||
public State State { get; set; }
|
public State State { get; set; }
|
||||||
|
@ -21,12 +23,12 @@
|
||||||
|
|
||||||
public string UniqueHash
|
public string UniqueHash
|
||||||
{
|
{
|
||||||
get { return _uniqueHash ?? (_uniqueHash = StateManager.GetUniqueHash(State, Scene.Key)); }
|
get { return _uniqueHash ?? (_uniqueHash = Manager.GetUniqueHash(State, Scene.Key)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CompressedHash
|
public string CompressedHash
|
||||||
{
|
{
|
||||||
get { return _compressedHash ?? (_compressedHash = StateManager.GetCompressedHash(this)); }
|
get { return _compressedHash ?? (_compressedHash = Manager.GetCompressedHash(this)); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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; }
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,14 @@
|
||||||
|
|
||||||
internal class StateResolver : IStateResolver
|
internal class StateResolver : IStateResolver
|
||||||
{
|
{
|
||||||
|
private static Logger _logger = Logger.GetLogger<StateResolver>();
|
||||||
private static readonly Random _random = new Random((int) DateTime.Now.Ticks);
|
private static readonly Random _random = new Random((int) DateTime.Now.Ticks);
|
||||||
private readonly IDictionary<string, string> _pageNames;
|
private readonly IDictionary<string, string> _pageNames;
|
||||||
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>();
|
||||||
|
@ -23,6 +26,7 @@
|
||||||
|
|
||||||
public ResolvedStory Resolve(IEnumerable<PageState> pages, Story story)
|
public ResolvedStory Resolve(IEnumerable<PageState> pages, Story story)
|
||||||
{
|
{
|
||||||
|
_logger.Debug("Resolving story paths...");
|
||||||
_story = story;
|
_story = story;
|
||||||
return new ResolvedStory
|
return new ResolvedStory
|
||||||
{
|
{
|
||||||
|
@ -43,13 +47,16 @@
|
||||||
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(Warnings, blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions);
|
||||||
|
var alts = Utilities.GetInstance(Warnings, blockName, lineNumber).ParseConditionalText(anchor);
|
||||||
|
if(alts != null)
|
||||||
{
|
{
|
||||||
var satisfied = Utilities.GetInstance(blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions);
|
|
||||||
var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(text);
|
|
||||||
var replace = alts[satisfied];
|
var replace = alts[satisfied];
|
||||||
text = RegexLib.EscapeChar.Replace(replace, string.Empty);
|
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))
|
||||||
: text;
|
: text;
|
||||||
|
@ -67,7 +74,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 +93,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(
|
||||||
|
|
|
@ -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,32 @@ 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()
|
||||||
{
|
{
|
||||||
return new Utilities
|
return new Utilities
|
||||||
{
|
{
|
||||||
|
_warnings = new List<FicdownException>(),
|
||||||
|
_blockName = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Utilities GetInstance(List<FicdownException> warnings, string blockName, int lineNumber)
|
||||||
|
{
|
||||||
|
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 +45,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 +68,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 +128,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},
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -10,6 +11,7 @@
|
||||||
|
|
||||||
internal class GameTraverser : IGameTraverser
|
internal class GameTraverser : IGameTraverser
|
||||||
{
|
{
|
||||||
|
private static Logger _logger = Logger.GetLogger<GameTraverser>();
|
||||||
private StateManager _manager;
|
private StateManager _manager;
|
||||||
private Queue<StateQueueItem> _processingQueue;
|
private Queue<StateQueueItem> _processingQueue;
|
||||||
private IDictionary<string, PageState> _processed;
|
private IDictionary<string, PageState> _processed;
|
||||||
|
@ -17,6 +19,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 +29,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>();
|
||||||
|
@ -56,6 +60,7 @@
|
||||||
_wasRun = true;
|
_wasRun = true;
|
||||||
|
|
||||||
// generate comprehensive enumeration
|
// generate comprehensive enumeration
|
||||||
|
_logger.Debug("Enumerating story scenes...");
|
||||||
|
|
||||||
var initial = _manager.InitialState;
|
var initial = _manager.InitialState;
|
||||||
_processingQueue.Enqueue(new StateQueueItem
|
_processingQueue.Enqueue(new StateQueueItem
|
||||||
|
@ -63,6 +68,7 @@
|
||||||
Page = initial,
|
Page = initial,
|
||||||
AffectedStates = new List<State> {initial.AffectedState}
|
AffectedStates = new List<State> {initial.AffectedState}
|
||||||
});
|
});
|
||||||
|
var interval = 0;
|
||||||
while (_processingQueue.Count > 0)
|
while (_processingQueue.Count > 0)
|
||||||
{
|
{
|
||||||
var state = _processingQueue.Dequeue();
|
var state = _processingQueue.Dequeue();
|
||||||
|
@ -71,9 +77,13 @@
|
||||||
_processed.Add(state.Page.UniqueHash, state.Page);
|
_processed.Add(state.Page.UniqueHash, state.Page);
|
||||||
ProcessState(state);
|
ProcessState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(++interval % 100 == 0 || _processingQueue.Count == 0)
|
||||||
|
_logger.Debug($"Processed {interval} scenes, {_processingQueue.Count} queued...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure every page gets affected data on every page that it links to
|
// make sure every page gets affected data on every page that it links to
|
||||||
|
_logger.Debug("Processing scene links...");
|
||||||
foreach (var pageTuple in _processed)
|
foreach (var pageTuple in _processed)
|
||||||
{
|
{
|
||||||
foreach (var linkTuple in pageTuple.Value.Links)
|
foreach (var linkTuple in pageTuple.Value.Links)
|
||||||
|
@ -92,6 +102,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// compress redundancies
|
// compress redundancies
|
||||||
|
_logger.Debug("Compressing redundant scenes...");
|
||||||
foreach (var row in _processed)
|
foreach (var row in _processed)
|
||||||
{
|
{
|
||||||
if (!_compressed.ContainsKey(row.Value.CompressedHash))
|
if (!_compressed.ContainsKey(row.Value.CompressedHash))
|
||||||
|
@ -125,15 +136,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,23 +154,36 @@
|
||||||
// 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(newState.Scene != null)
|
||||||
|
{
|
||||||
if (!currentState.Page.Links.ContainsKey(anchor.Original))
|
if (!currentState.Page.Links.ContainsKey(anchor.Original))
|
||||||
currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
|
currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
|
||||||
|
|
||||||
|
@ -174,3 +198,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -11,16 +11,33 @@
|
||||||
|
|
||||||
internal class StateManager
|
internal class StateManager
|
||||||
{
|
{
|
||||||
|
private static Logger _logger = Logger.GetLogger<StateManager>();
|
||||||
private readonly Story _story;
|
private readonly Story _story;
|
||||||
private readonly Dictionary<string, int> _stateMatrix;
|
private readonly Dictionary<string, int> _stateMatrix;
|
||||||
private readonly int _sceneCount;
|
private readonly int _sceneCount;
|
||||||
private readonly int _actionCount;
|
private readonly int _actionCount;
|
||||||
|
private BitArray _scenesSeenMask;
|
||||||
|
|
||||||
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);
|
||||||
|
_scenesSeenMask = new BitArray(_sceneCount);
|
||||||
|
|
||||||
|
// figure out which scenes can affect state
|
||||||
|
var masked = 0;
|
||||||
|
foreach(var scene in allScenes)
|
||||||
|
{
|
||||||
|
if(Utilities.GetInstance().ParseAnchors(scene.RawDescription).Any(a => a.Href.Toggles != null) || RegexLib.BlockQuotes.IsMatch(scene.RawDescription))
|
||||||
|
{
|
||||||
|
_scenesSeenMask[scene.Id - 1] = true;
|
||||||
|
masked++;
|
||||||
|
}
|
||||||
|
}
|
||||||
_actionCount = _story.Actions.Count > 0 ? _story.Actions.Max(a => a.Value.Id) : 0;
|
_actionCount = _story.Actions.Count > 0 ? _story.Actions.Max(a => a.Value.Id) : 0;
|
||||||
_stateMatrix = new Dictionary<string, int>();
|
_stateMatrix = new Dictionary<string, int>();
|
||||||
var state = 0;
|
var state = 0;
|
||||||
|
@ -28,23 +45,34 @@
|
||||||
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[] {})))
|
||||||
{
|
{
|
||||||
_stateMatrix.Add(toggle, state++);
|
_stateMatrix.Add(toggle, state++);
|
||||||
}
|
}
|
||||||
|
_logger.Debug($"{_sceneCount} scenes ({masked} can change state).");
|
||||||
|
_logger.Debug($"{_actionCount} actions.");
|
||||||
|
_logger.Debug($"{_stateMatrix.Count()} states.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageState InitialState
|
public PageState InitialState
|
||||||
{
|
{
|
||||||
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
|
||||||
{
|
{
|
||||||
|
Manager = this,
|
||||||
|
|
||||||
Id = Guid.Empty,
|
Id = Guid.Empty,
|
||||||
Links = new Dictionary<string, string>(),
|
Links = new Dictionary<string, string>(),
|
||||||
State = new State
|
State = new State
|
||||||
|
@ -61,7 +89,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 +111,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 +121,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)
|
||||||
{
|
{
|
||||||
|
if(_stateMatrix.ContainsKey(toggle))
|
||||||
state.PlayerState[_stateMatrix[toggle]] = true;
|
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)
|
||||||
|
@ -107,14 +138,14 @@
|
||||||
state.ScenesSeen[sceneId - 1] = true;
|
state.ScenesSeen[sceneId - 1] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetUniqueHash(State state, string sceneKey)
|
public string GetUniqueHash(State state, string sceneKey)
|
||||||
{
|
{
|
||||||
var combined =
|
var combined =
|
||||||
new bool[
|
new bool[
|
||||||
state.PlayerState.Count + state.ScenesSeen.Count + state.ActionsToShow.Count +
|
state.PlayerState.Count + state.ScenesSeen.Count + state.ActionsToShow.Count +
|
||||||
(state.ActionFirstToggles != null ? state.ActionFirstToggles.Count : 0)];
|
(state.ActionFirstToggles != null ? state.ActionFirstToggles.Count : 0)];
|
||||||
state.PlayerState.CopyTo(combined, 0);
|
state.PlayerState.CopyTo(combined, 0);
|
||||||
state.ScenesSeen.CopyTo(combined, state.PlayerState.Count);
|
state.ScenesSeen.And(_scenesSeenMask).CopyTo(combined, state.PlayerState.Count);
|
||||||
state.ActionsToShow.CopyTo(combined, state.PlayerState.Count + state.ScenesSeen.Count);
|
state.ActionsToShow.CopyTo(combined, state.PlayerState.Count + state.ScenesSeen.Count);
|
||||||
if (state.ActionFirstToggles != null)
|
if (state.ActionFirstToggles != null)
|
||||||
state.ActionFirstToggles.CopyTo(combined,
|
state.ActionFirstToggles.CopyTo(combined,
|
||||||
|
@ -122,11 +153,15 @@
|
||||||
var ba = new BitArray(combined);
|
var ba = new BitArray(combined);
|
||||||
var byteSize = (int)Math.Ceiling(combined.Length / 8.0);
|
var byteSize = (int)Math.Ceiling(combined.Length / 8.0);
|
||||||
var encoded = new byte[byteSize];
|
var encoded = new byte[byteSize];
|
||||||
|
for(var i = 0; i < byteSize; i++)
|
||||||
|
{
|
||||||
|
encoded[i] = 0;
|
||||||
|
}
|
||||||
ba.CopyTo(encoded, 0);
|
ba.CopyTo(encoded, 0);
|
||||||
return string.Format("{0}=={1}", sceneKey, Convert.ToBase64String(encoded));
|
return string.Format("{0}=={1}", sceneKey, Convert.ToBase64String(encoded));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetCompressedHash(PageState page)
|
public string GetCompressedHash(PageState page)
|
||||||
{
|
{
|
||||||
var compressed = new State
|
var compressed = new State
|
||||||
{
|
{
|
||||||
|
@ -138,40 +173,45 @@
|
||||||
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;
|
Scene newScene = null;
|
||||||
foreach (var scene in _story.Scenes[target])
|
foreach (var scene in _story.Scenes[target])
|
||||||
{
|
{
|
||||||
if (ConditionsMatch(scene, playerState) &&
|
if (ConditionsMatch(scene, playerState) &&
|
||||||
(newScene == null || newScene.Conditions == null ||
|
(newScene == null || newScene.Conditions == null ||
|
||||||
scene.Conditions.Count > newScene.Conditions.Count))
|
(scene.Conditions != null && scene.Conditions.Count > newScene.Conditions.Count)))
|
||||||
{
|
{
|
||||||
newScene = scene;
|
newScene = scene;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newScene == null)
|
if (newScene == null)
|
||||||
throw new FicdownException(blockName, lineNumber, string.Format("Scene {0} reached with unmatched player state", target));
|
_warnings.Add(new FicdownException(blockName, string.Format("Link to scene that is undefined for conditionals: {0}", target), anchor.LineNumber, anchor.ColNumber));
|
||||||
return newScene;
|
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)
|
private bool ConditionsMatch(Scene scene, BitArray playerState)
|
||||||
{
|
{
|
||||||
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)
|
||||||
{
|
{
|
||||||
return new PageState
|
return new PageState
|
||||||
{
|
{
|
||||||
|
Manager = this,
|
||||||
|
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Links = new Dictionary<string, string>(),
|
Links = new Dictionary<string, string>(),
|
||||||
State = new State
|
State = new State
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
|
|
||||||
public class EpubRenderer : HtmlRenderer
|
public class EpubRenderer : HtmlRenderer
|
||||||
{
|
{
|
||||||
|
private static readonly Logger _logger = Logger.GetLogger<EpubRenderer>();
|
||||||
private readonly string _author;
|
private readonly string _author;
|
||||||
private readonly string _bookId;
|
private readonly string _bookId;
|
||||||
private readonly string _language;
|
private readonly string _language;
|
||||||
|
@ -98,10 +99,12 @@
|
||||||
_author = author;
|
_author = author;
|
||||||
_bookId = bookId ?? Guid.NewGuid().ToString("D");
|
_bookId = bookId ?? Guid.NewGuid().ToString("D");
|
||||||
_language = language ?? "en";
|
_language = language ?? "en";
|
||||||
|
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Render(Model.Parser.ResolvedStory story, string outPath, bool debug = false)
|
public override void Render(Model.Parser.ResolvedStory story, string outPath, bool debug = false)
|
||||||
{
|
{
|
||||||
|
_logger.Debug("Generating epub...");
|
||||||
var temppath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
var temppath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
|
||||||
Directory.CreateDirectory(temppath);
|
Directory.CreateDirectory(temppath);
|
||||||
base.Render(story, temppath, debug);
|
base.Render(story, temppath, debug);
|
||||||
|
|
|
@ -3,15 +3,16 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MarkdownSharp;
|
using Markdig;
|
||||||
using Model.Parser;
|
using Model.Parser;
|
||||||
using Parser;
|
using Parser;
|
||||||
|
|
||||||
public class HtmlRenderer : IRenderer
|
public class HtmlRenderer : IRenderer
|
||||||
{
|
{
|
||||||
|
private static Logger _logger = Logger.GetLogger<HtmlRenderer>();
|
||||||
private readonly string _language;
|
private readonly string _language;
|
||||||
|
|
||||||
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; }
|
||||||
|
@ -23,7 +24,6 @@
|
||||||
public HtmlRenderer(string language)
|
public HtmlRenderer(string language)
|
||||||
{
|
{
|
||||||
_language = language;
|
_language = language;
|
||||||
Markdown = new Markdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Render(ResolvedStory story, string outPath, bool debug = false)
|
public virtual void Render(ResolvedStory story, string outPath, bool debug = false)
|
||||||
|
@ -40,11 +40,12 @@
|
||||||
|
|
||||||
protected void GenerateHtml(ResolvedStory story, string outPath, bool debug)
|
protected void GenerateHtml(ResolvedStory story, string outPath, bool debug)
|
||||||
{
|
{
|
||||||
|
_logger.Debug("Generating HTML...");
|
||||||
var index = FillTemplate(IndexTemplate ?? Template.Index, new Dictionary<string, string>
|
var index = FillTemplate(IndexTemplate ?? Template.Index, new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
{"Language", _language},
|
{"Language", _language},
|
||||||
{"Title", story.Name},
|
{"Title", story.Name},
|
||||||
{"Description", Markdown.Transform(story.Description)},
|
{"Description", Markdown.ToHtml(story.Description)},
|
||||||
{"FirstScene", string.Format("{0}.html", story.FirstPage)}
|
{"FirstScene", string.Format("{0}.html", story.FirstPage)}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,7 +56,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);
|
||||||
|
@ -70,7 +71,7 @@
|
||||||
{
|
{
|
||||||
{"Language", _language},
|
{"Language", _language},
|
||||||
{"Title", story.Name},
|
{"Title", story.Name},
|
||||||
{"Content", Markdown.Transform(content)}
|
{"Content", Markdown.ToHtml(content)}
|
||||||
});
|
});
|
||||||
|
|
||||||
File.WriteAllText(Path.Combine(outPath, string.Format("{0}.html", page.Name)), scene);
|
File.WriteAllText(Path.Combine(outPath, string.Format("{0}.html", page.Name)), scene);
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Rudis Muiznieks
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
1
Makefile
1
Makefile
|
@ -10,6 +10,7 @@ test:
|
||||||
dotnet test Ficdown.Parser.Tests
|
dotnet test Ficdown.Parser.Tests
|
||||||
|
|
||||||
publish: clean
|
publish: clean
|
||||||
|
rm -rf /tmp/ficdown*
|
||||||
dotnet publish --self-contained -c Release -r linux-x64 Ficdown.Console
|
dotnet publish --self-contained -c Release -r linux-x64 Ficdown.Console
|
||||||
tar -C Ficdown.Console/bin/Release/netcoreapp2.1/linux-x64/publish -cvzf /tmp/ficdown-linux64.tar.gz .
|
tar -C Ficdown.Console/bin/Release/netcoreapp2.1/linux-x64/publish -cvzf /tmp/ficdown-linux64.tar.gz .
|
||||||
dotnet publish --self-contained -c Release -r win-x64 Ficdown.Console
|
dotnet publish --self-contained -c Release -r win-x64 Ficdown.Console
|
||||||
|
|
16
README.md
16
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Ficdown
|
# Ficdown
|
||||||
|
|
||||||
Ficdown is a system for building interactive fiction using MarkDown syntax. See [Ficdown.com](http://www.ficdown.com) for more information.
|
Ficdown is a system for building interactive fiction using MarkDown syntax.
|
||||||
|
|
||||||
This project contains the core Ficdown library for parsing Ficdown stories, as well as a console application that can be used to generate HTML or epub ebook formats.
|
This project contains the core Ficdown library for parsing Ficdown stories, as well as a console application that can be used to generate HTML or epub ebook formats.
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ Ficdown is written using .NET Core and should run on Windows, Linux, and OSX wit
|
||||||
|
|
||||||
## Obtaining
|
## Obtaining
|
||||||
|
|
||||||
If you want to use Ficdown to convert your stories into ebooks, download the latest version from the [releases](https://github.com/rudism/Ficdown/releases) page and decompress it somewhere on your hard drive. Ficdown does not include an installer, the application and all of its dependencies are included directly in the zip archive.
|
If you want to use Ficdown to convert your stories into ebooks, download the latest version from the [releases](https://code.sitosis.com/rudism/ficdown/releases) page and decompress it somewhere on your hard drive. Ficdown does not include an installer, the application and all of its dependencies are included directly in the zip archive.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
@ -29,8 +29,8 @@ The pre-built releases are self-contained .NET Core deployments, so you should b
|
||||||
Running ficdown.exe without any arguments will produce the following help text:
|
Running ficdown.exe without any arguments will produce the following help text:
|
||||||
|
|
||||||
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 stdin)
|
||||||
[--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"]
|
||||||
|
@ -85,6 +85,10 @@ If you pass this option, all of the pages in your story will include output at t
|
||||||
|
|
||||||
To generate other formats than HTML or epub, you will have to use third party tools. [Calibre](http://www.calibre-ebook.com) is a popular ebook management suite that includes the ability to convert books from almost any format to any other format. Also, Amazon has an official tool called [KindleGen](http://www.amazon.com/gp/feature.html?docId=1000765211) that you can use to convert your epub to a format that can be read on Kindles.
|
To generate other formats than HTML or epub, you will have to use third party tools. [Calibre](http://www.calibre-ebook.com) is a popular ebook management suite that includes the ability to convert books from almost any format to any other format. Also, Amazon has an official tool called [KindleGen](http://www.amazon.com/gp/feature.html?docId=1000765211) that you can use to convert your epub to a format that can be read on Kindles.
|
||||||
|
|
||||||
### Interactive Website
|
## Additional Tools
|
||||||
|
|
||||||
Ficdown stories can be played interactively in a web browser without even requiring the command line utility here. See [Ficdown.js](https://github.com/rudism/Ficdown.js) for a Javascript Ficdown parser and interpreter that you can include on your own website to present your Ficdown stories.
|
- Ficdown stories can be played interactively in a web browser without even requiring the command line utility here. See [Ficdown.js](https://code.sitosis.com/rudism/ficdown.js) for a Javascript Ficdown parser and interpreter that you can include on your own website to present your Ficdown stories.
|
||||||
|
|
||||||
|
- [Ficdown-editor](https://byfernanz.github.io/ficdown-editor/) is a web-based GUI for writing Ficdown.
|
||||||
|
|
||||||
|
- [Prop](https://github.com/ByFernanz/prop) is a YAML-header style preprocessor for Ficdown
|
||||||
|
|
Loading…
Reference in New Issue