Compare commits

..

No commits in common. "main" and "1.0.2" have entirely different histories.
main ... 1.0.2

44 changed files with 857 additions and 609 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>

View File

@ -1,12 +1,64 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{C9D8CF8A-3CBE-4E45-91A3-37F8256A81A7}</ProjectGuid>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework> <AppDesignerFolder>Properties</AppDesignerFolder>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle> <RootNamespace>Ficdown.Console</RootNamespace>
<AssemblyName>ficdown</AssemblyName> <AssemblyName>Ficdown.Console</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj" /> <Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj">
<Project>{780f652d-7541-4171-bb89-2d263d3961dc}</Project>
<Name>Ficdown.Parser</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project> </Project>

View File

@ -3,6 +3,7 @@
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;
@ -15,7 +16,8 @@
{ {
if(e.ExceptionObject is FicdownException) if(e.ExceptionObject is FicdownException)
{ {
Console.WriteLine(e.ExceptionObject.ToString()); Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(e.ExceptionObject.ToString());
Environment.Exit(3); Environment.Exit(3);
} }
}; };
@ -83,115 +85,98 @@
ShowHelp(); ShowHelp();
return 0; return 0;
} }
if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile))
Logger.Initialize(debug);
var logger = Logger.GetLogger<Program>();
var lintMode = format == "lint";
if(!lintMode)
{ {
if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile)) ShowHelp();
{ return 1;
ShowHelp(); }
return 1; if (!File.Exists(infile))
} {
if (!File.Exists(infile)) Console.WriteLine(@"Source file {0} not found.", infile);
{ return 2;
logger.Error($"Source file {infile} not found."); }
return 2; if (string.IsNullOrWhiteSpace(output))
} if (format == "html")
if (string.IsNullOrWhiteSpace(output)) output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html");
if (format == "html") else if (format == "epub")
output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html"); output = "output.epub";
else if (format == "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);
return 2;
}
if (!string.IsNullOrWhiteSpace(tempdir))
{
if (!Directory.Exists(tempdir))
{ {
logger.Error($"Specified output {output} already exists."); Console.WriteLine(@"Template directory {0} does not exist.", tempdir);
return 2; return 2;
} }
if (!string.IsNullOrWhiteSpace(tempdir)) if (!File.Exists(Path.Combine(tempdir, "index.html")) ||
!File.Exists(Path.Combine(tempdir, "scene.html")) ||
!File.Exists(Path.Combine(tempdir, "styles.css")))
{ {
if (!Directory.Exists(tempdir)) Console.WriteLine(
{ @"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files.");
logger.Error($"Template directory {tempdir} does not exist.");
return 2;
}
if (!File.Exists(Path.Combine(tempdir, "index.html")) ||
!File.Exists(Path.Combine(tempdir, "scene.html")) ||
!File.Exists(Path.Combine(tempdir, "styles.css")))
{
logger.Error(
@"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))
{ {
logger.Error($"Images directory {images} does not exist."); Console.WriteLine(@"Images directory {0} does not exist.", images);
return 2; return 2;
}
} }
var parser = new FicdownParser(); var parser = new FicdownParser();
var storyText = File.ReadAllText(infile);
string storyText; Console.WriteLine(@"Parsing story...");
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);
parser.Warnings.Select(w => w.ToString()).Distinct().ToList().ForEach(s => logger.Raw(s)); story.Orphans.ToList().ForEach(o =>
story.Orphans.ToList().ForEach(o => logger.Raw($"Warning L{o.LineNumber},1: \"{o.Name}\": Unreachable {o.Type}"));
if(!lintMode && parser.Warnings.Count() == 0)
{ {
IRenderer rend; var currentColor = Console.ForegroundColor;
switch (format) Console.ForegroundColor = ConsoleColor.Yellow;
{ Console.Error.WriteLine("Warning (line {0}): {1} {2} is unreachable", o.LineNumber, o.Type, o.Name);
case "html": Console.ForegroundColor = currentColor;
Directory.CreateDirectory(output); });
rend = new HtmlRenderer(language);
break; IRenderer rend;
case "epub": switch (format)
if (string.IsNullOrWhiteSpace(author)) {
{ case "html":
logger.Error(@"Epub format requires the --author argument."); Directory.CreateDirectory(output);
return 1; rend = new HtmlRenderer(language);
} break;
rend = new EpubRenderer(author, bookid, language); case "epub":
break; if (string.IsNullOrWhiteSpace(author))
default: {
ShowHelp(); Console.WriteLine(@"Epub format requires the --author argument.");
return 1; return 1;
} }
rend = new EpubRenderer(author, bookid, language);
if (!string.IsNullOrWhiteSpace(tempdir)) break;
{ default:
rend.IndexTemplate = File.ReadAllText(Path.Combine(tempdir, "index.html")); ShowHelp();
rend.SceneTemplate = File.ReadAllText(Path.Combine(tempdir, "scene.html")); return 1;
rend.StylesTemplate = File.ReadAllText(Path.Combine(tempdir, "styles.css"));
};
if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images;
logger.Log(@"Rendering story...");
rend.Render(story, output, debug);
logger.Log(@"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;
} }
@ -199,7 +184,7 @@
private static void ShowHelp() private static void ShowHelp()
{ {
Console.WriteLine( Console.WriteLine(
@"Usage: ficdown @"Usage: ficdown.exe
--format (html|epub) --format (html|epub)
--in ""/path/to/source.md"" --in ""/path/to/source.md""
[--out ""/path/to/output""] [--out ""/path/to/output""]
@ -208,9 +193,7 @@
[--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)");
} }
} }
} }

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Ficdown.Console")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Ficdown.Console")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("dafaccf5-0218-412d-b127-665c768f85ec")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -1,22 +1,19 @@
namespace Ficdown.Parser.Tests namespace Ficdown.Parser.Tests
{ {
using System;
using System.Linq;
using System.Collections.Generic;
using Xunit; using Xunit;
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 = NewBlockHandler; var bh = new BlockHandler();
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())));
@ -25,7 +22,7 @@ just a lonely scene".ToLines())));
[Fact] [Fact]
public void StoryWithNoAnchorThrowsException() public void StoryWithNoAnchorThrowsException()
{ {
var bh = NewBlockHandler; var bh = new BlockHandler();
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
@ -36,23 +33,17 @@ nothing links here".ToLines())));
[Fact] [Fact]
public void StoriesWithFancyAnchorsThrowExceptions() public void StoriesWithFancyAnchorsThrowExceptions()
{ {
var bh = NewBlockHandler; var bh = new BlockHandler();
bh.ParseBlocks(bh.ExtractBlocks(@" Assert.Throws<FicdownException>(() => 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.NotEmpty(bh.Warnings); Assert.Throws<FicdownException>(() => bh.ParseBlocks(bh.ExtractBlocks(@"
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
@ -63,7 +54,7 @@ this is a scene".ToLines())));
[Fact] [Fact]
public void StoryLinkingToNonExistentSceneThrowsException() public void StoryLinkingToNonExistentSceneThrowsException()
{ {
var bh = NewBlockHandler; var bh = new BlockHandler();
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
@ -74,7 +65,7 @@ this scene is so cold and lonely".ToLines())));
[Fact] [Fact]
public void StoryWithALegitAnchorParses() public void StoryWithALegitAnchorParses()
{ {
var bh = NewBlockHandler; var bh = new BlockHandler();
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
@ -85,8 +76,8 @@ this is a scene".ToLines()));
[Fact] [Fact]
public void StoryWithDuplicateActionsThrowsException() public void StoryWithDuplicateActionsThrowsException()
{ {
var bh = NewBlockHandler; var bh = new BlockHandler();
bh.ParseBlocks(bh.ExtractBlocks(@" Assert.Throws<FicdownException>(() => 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
@ -96,14 +87,13 @@ 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 = NewBlockHandler; var bh = new BlockHandler();
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,23 +1,87 @@
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{756192E2-BA47-4850-8096-289D44878A7E}</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework> <AppDesignerFolder>Properties</AppDesignerFolder>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle> <RootNamespace>Ficdown.Parser.Tests</RootNamespace>
<AssemblyName>Ficdown.Parser.Tests</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="TestStories\**\*" CopyToOutputDirectory="Always" /> <Reference Include="Moq">
<HintPath>..\packages\Moq.4.2.1402.2112\lib\net40\Moq.dll</HintPath>
</Reference>
<Reference Include="ServiceStack.Text">
<HintPath>..\packages\ServiceStack.Text.4.0.22\lib\net40\ServiceStack.Text.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="xunit">
<HintPath>..\packages\xunit.1.9.2\lib\net20\xunit.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj" /> <Compile Include="BlockHandlerTests.cs" />
<Compile Include="IntegrationTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestStories\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="UtilityTests.cs" />
<Compile Include="Extensions\TestExtensions.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> <None Include="packages.config" />
<PackageReference Include="Moq" Version="4.10.0" /> <None Include="TestStories\CloakOfDarkness.md" />
<PackageReference Include="xunit" Version="2.4.0" /> <None Include="TestStories\TheRobotKing.md" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup />
<ItemGroup>
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj">
<Project>{780f652d-7541-4171-bb89-2d263d3961dc}</Project>
<Name>Ficdown.Parser</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="TestStories\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project> </Project>

View File

@ -1,7 +1,10 @@
namespace Ficdown.Parser.Tests namespace Ficdown.Parser.Tests
{ {
using System;
using System.IO; using System.IO;
using System.Text;
using Render; using Render;
using TestStories;
using Xunit; using Xunit;
public class IntegrationTests public class IntegrationTests
@ -9,11 +12,10 @@
[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 = Encoding.UTF8.GetString(Resources.CloakOfDarkness);
var story = parser.ParseStory(storyText); var story = parser.ParseStory(storyText);
var path = Path.Combine(Template.BaseDir, "itest_output"); var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "itest_output");
if (!Directory.Exists(path)) Directory.CreateDirectory(path); if (!Directory.Exists(path)) Directory.CreateDirectory(path);
foreach (var file in Directory.GetFiles(path)) foreach (var file in Directory.GetFiles(path))
{ {

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Ficdown.Parser.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Ficdown.Parser.Tests")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("7380fc2c-7382-4fbb-be17-574662992d92")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,83 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.34014
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Ficdown.Parser.Tests.TestStories {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Ficdown.Parser.Tests.TestStories.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] CloakOfDarkness {
get {
object obj = ResourceManager.GetObject("CloakOfDarkness", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] TheRobotKing {
get {
object obj = ResourceManager.GetObject("TheRobotKing", resourceCulture);
return ((byte[])(obj));
}
}
}
}

View File

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="CloakOfDarkness" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>CloakOfDarkness.md;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="TheRobotKing" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>TheRobotKing.md;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>

View File

@ -1,40 +1,36 @@
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(Warnings, "none", 0); } get { return Utilities.GetInstance("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, 0, 0); var anchor = Utilities.ParseAnchor(anchorStr);
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, 0, 0); anchor = Utilities.ParseAnchor(anchorStr);
Assert.Equal(anchorStr, anchor.Original); Assert.Equal(anchorStr, anchor.Original);
anchorStr = @"[Link text](""Title text"")"; anchorStr = @"[Link text](""Title text"")";
anchor = Utilities.ParseAnchor(anchorStr, 0, 0); anchor = Utilities.ParseAnchor(anchorStr);
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)", 0, 0); var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene)");
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);
} }
@ -42,15 +38,15 @@
[Fact] [Fact]
public void AnchorsWithConditionsMatch() public void AnchorsWithConditionsMatch()
{ {
var anchor = Utilities.ParseAnchor(@"[Link text](?condition-state)", 0, 0); var anchor = Utilities.ParseAnchor(@"[Link text](?condition-state)");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](?!condition-state)");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](?condition-1&!condition-2)");
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"]);
@ -59,15 +55,15 @@
[Fact] [Fact]
public void AnchorsWithTogglesMatch() public void AnchorsWithTogglesMatch()
{ {
var anchor = Utilities.ParseAnchor(@"[Link text](#toggle-state)", 0, 0); var anchor = Utilities.ParseAnchor(@"[Link text](#toggle-state)");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)");
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);
} }
@ -75,11 +71,11 @@
[Fact] [Fact]
public void AnchorsWithTitlesMatch() public void AnchorsWithTitlesMatch()
{ {
var anchor = Utilities.ParseAnchor(@"[Link text](""Title text"")", 0, 0); var anchor = Utilities.ParseAnchor(@"[Link text](""Title text"")");
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"")", 0, 0); anchor = Utilities.ParseAnchor(@"[Talking to Kid](""Lobby"")");
Assert.Equal("Talking to Kid", anchor.Text); Assert.Equal("Talking to Kid", anchor.Text);
Assert.Equal("Lobby", anchor.Title); Assert.Equal("Lobby", anchor.Title);
} }
@ -87,20 +83,20 @@
[Fact] [Fact]
public void ComplexAnchorsMatch() public void ComplexAnchorsMatch()
{ {
var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")", 0, 0); var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](/target-scene#toggle-state)");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state)");
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)", 0, 0); anchor = Utilities.ParseAnchor(@"[Link text](?condition-state#toggle-state)");
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

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Moq" version="4.2.1402.2112" targetFramework="net45" />
<package id="ServiceStack.Text" version="4.0.22" targetFramework="net45" />
<package id="xunit" version="1.9.2" targetFramework="net45" />
</packages>

View File

@ -4,89 +4,40 @@
namespace Ficdown.Parser namespace Ficdown.Parser
{ {
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using Model.Parser; using Model.Parser;
using Parser; using Parser;
using Player; using Player;
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 get { return _blockHandler ?? (_blockHandler = new BlockHandler()); }
{ 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 ?? get { return _gameTraverser ?? (_gameTraverser = new GameTraverser()); }
(_gameTraverser = new GameTraverser { Warnings = Warnings }); } set { _gameTraverser = value; }
set
{
_gameTraverser = value;
_gameTraverser.Warnings = Warnings;
}
} }
private IStateResolver _stateResolver; private IStateResolver _stateResolver;
internal IStateResolver StateResolver internal IStateResolver StateResolver
{ {
get get { return _stateResolver ?? (_stateResolver = new StateResolver()); }
{ 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

View File

@ -1,19 +1,109 @@
<?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{780F652D-7541-4171-BB89-2D263D3961DC}</ProjectGuid>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework> <AppDesignerFolder>Properties</AppDesignerFolder>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle> <RootNamespace>Ficdown.Parser</RootNamespace>
<AssemblyName>Ficdown.Parser</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="Render\**\*.html" CopyToOutputDirectory="Always" /> <Reference Include="Epub4Net">
<None Include="Render\**\*.css" CopyToOutputDirectory="Always" /> <HintPath>..\packages\Epub4Net.1.2.0\lib\net40\Epub4Net.dll</HintPath>
</Reference>
<Reference Include="Ionic.Zip">
<HintPath>..\packages\DotNetZip.1.9.1.8\lib\net20\Ionic.Zip.dll</HintPath>
</Reference>
<Reference Include="MarkdownSharp">
<HintPath>..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Epub4Net" Version="1.2.0" /> <Compile Include="Model\Parser\ResolvedPage.cs" />
<PackageReference Include="Ionic.Zip" Version="1.9.1.8" /> <Compile Include="Model\Parser\ResolvedStory.cs" />
<PackageReference Include="Markdig" Version="0.17.1" /> <Compile Include="Model\Player\PageState.cs" />
<PackageReference Include="System.Security.Permissions" /> <Compile Include="Model\Player\State.cs" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" /> <Compile Include="Model\Player\StateQueueItem.cs" />
<Compile Include="Parser\BlockHandler.cs" />
<Compile Include="Parser\IBlockHandler.cs" />
<Compile Include="Parser\RegexLib.cs" />
<Compile Include="Parser\StateResolver.cs" />
<Compile Include="Parser\Utilities.cs" />
<Compile Include="FicDownParser.cs" />
<Compile Include="Model\Parser\Anchor.cs" />
<Compile Include="Model\Parser\Block.cs" />
<Compile Include="Model\Parser\BlockType.cs" />
<Compile Include="Parser\IStateResolver.cs" />
<Compile Include="Model\Parser\Href.cs" />
<Compile Include="Model\Player\PlayerState.cs" />
<Compile Include="Model\Story\Extensions\SceneExtensions.cs" />
<Compile Include="Player\GameTraverser.cs" />
<Compile Include="Player\IGameTraverser.cs" />
<Compile Include="Player\StateManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Model\Story\Action.cs" />
<Compile Include="Model\Story\Scene.cs" />
<Compile Include="Model\Story\Story.cs" />
<Compile Include="Render\EpubRenderer.cs" />
<Compile Include="Render\HtmlRenderer.cs" />
<Compile Include="Render\IRenderer.cs" />
<Compile Include="Render\MimeHelper.cs" />
<Compile Include="Model\Parser\Line.cs" />
<Compile Include="Model\Parser\FicdownException.cs" />
<Compile Include="Parser\ParserExtensions.cs" />
<Compile Include="Model\Parser\Orphan.cs" />
<Compile Include="Render\Template.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<Content Include="Render\Views\scene.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Render\Assets\styles.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Render\Views\index.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project> </Project>

View File

@ -1,63 +0,0 @@
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}"));
}
}
}

View File

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

View File

@ -7,8 +7,6 @@
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; }
@ -23,12 +21,12 @@
public string UniqueHash public string UniqueHash
{ {
get { return _uniqueHash ?? (_uniqueHash = Manager.GetUniqueHash(State, Scene.Key)); } get { return _uniqueHash ?? (_uniqueHash = StateManager.GetUniqueHash(State, Scene.Key)); }
} }
public string CompressedHash public string CompressedHash
{ {
get { return _compressedHash ?? (_compressedHash = Manager.GetCompressedHash(this)); } get { return _compressedHash ?? (_compressedHash = StateManager.GetCompressedHash(this)); }
} }
} }
} }

View File

@ -4,7 +4,6 @@
{ {
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,7 +8,6 @@
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,8 +10,6 @@
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>();
@ -47,22 +45,22 @@
public Story ParseBlocks(IEnumerable<Block> blocks) public Story ParseBlocks(IEnumerable<Block> blocks)
{ {
// get the story // get the story
var storyBlocks = blocks.Where(b => b.Type == BlockType.Story); var storyBlock = blocks.SingleOrDefault(b => b.Type == BlockType.Story);
if(storyBlocks.Count() == 0) throw new FicdownException("No story block found"); if(storyBlock == null) throw new FicdownException("No story block found");
if(storyBlocks.Count() > 1) throw new FicdownException("More than one story block found");
var storyBlock = storyBlocks.Single(); Anchor storyAnchor;
try
var storyAnchor = Utilities.GetInstance(Warnings, storyBlock.Name, storyBlock.LineNumber).ParseAnchor(storyBlock.Name, storyBlock.LineNumber, 1);
if(storyAnchor == null || storyAnchor.Href == null)
{ {
throw new FicdownException(storyBlock.Name, "Story name must be an anchor pointing to the first scene", storyBlock.LineNumber); storyAnchor = Utilities.GetInstance(storyBlock.Name, storyBlock.LineNumber).ParseAnchor(storyBlock.Name);
}
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)
Warnings.Add(new FicdownException(storyBlock.Name, "Story href should only have a target", storyBlock.LineNumber)); throw new FicdownException(storyBlock.Name, storyBlock.LineNumber, "Story href should only have target");
var story = new Story var story = new Story
{ {
@ -90,11 +88,11 @@
var a = blocks.First(b => b.Type == BlockType.Action && blocks.Any(d => b != d && BlockToAction(b, 0).Toggle == BlockToAction(d, 0).Toggle)); var 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);
Warnings.Add(new FicdownException(actionA.Toggle, string.Format("Action is defined again on line {0}", dupe.LineNumber), actionA.LineNumber)); throw new FicdownException(actionA.Toggle, actionA.LineNumber, string.Format("Action is defined again on line {0}", dupe.LineNumber));
} }
if (!story.Scenes.ContainsKey(storyAnchor.Href.Target)) if (!story.Scenes.ContainsKey(storyAnchor.Href.Target))
throw new FicdownException(storyBlock.Name, string.Format("Story links to undefined scene: {0}", storyAnchor.Href.Target), storyBlock.LineNumber); throw new FicdownException(storyBlock.Name, storyBlock.LineNumber, string.Format("Story targets non-existent scene: {0}", storyAnchor.Href.Target));
story.FirstScene = storyAnchor.Href.Target; story.FirstScene = storyAnchor.Href.Target;
return story; return story;
@ -107,23 +105,22 @@
{ {
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()
}; };
Anchor sceneName; try
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(Warnings, block.Name, block.LineNumber).NormalizeString(sceneName.Text); scene.Key = Utilities.GetInstance(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)
Warnings.Add(new FicdownException(block.Name, "Scene href should only have conditions", block.LineNumber)); throw new FicdownException(block.Name, block.LineNumber, string.Format("Scene href should only have conditions: {0}", block.Name));
scene.Conditions = sceneName.Href.Conditions; scene.Conditions = sceneName.Href.Conditions;
} }
else catch(FicdownException)
{ {
scene.Name = block.Name.Trim(); scene.Name = block.Name.Trim();
scene.Key = Utilities.GetInstance(Warnings, block.Name, block.LineNumber).NormalizeString(block.Name); scene.Key = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(block.Name);
} }
return scene; return scene;
@ -134,8 +131,7 @@
return new Action return new Action
{ {
Id = id, Id = id,
Toggle = Utilities.GetInstance(Warnings, block.Name, block.LineNumber).NormalizeString(block.Name), Toggle = Utilities.GetInstance(block.Name, block.LineNumber).NormalizeString(block.Name),
RawDescription = string.Join("\n", block.Lines.Select(l => l.Text)),
Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim(), Description = string.Join("\n", block.Lines.Select(l => l.Text)).Trim(),
LineNumber = block.LineNumber LineNumber = block.LineNumber
}; };

View File

@ -6,7 +6,6 @@
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,7 +7,6 @@
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,5 +1,6 @@
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

@ -10,14 +10,11 @@
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>();
@ -26,7 +23,6 @@
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
{ {
@ -47,15 +43,12 @@
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 != null && anchor.Href.Conditions != null) if (anchor.Href.Conditions != null)
{ {
var satisfied = Utilities.GetInstance(Warnings, blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions); var satisfied = Utilities.GetInstance(blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions);
var alts = Utilities.GetInstance(Warnings, blockName, lineNumber).ParseConditionalText(anchor); var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(text);
if(alts != null) var replace = alts[satisfied];
{ 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))
@ -65,8 +58,7 @@
private string ResolveDescription(PageState page) private string ResolveDescription(PageState page)
{ {
var resolved = new StringBuilder(); var resolved = new StringBuilder();
if(!string.IsNullOrEmpty(page.Scene.Name)) resolved.AppendFormat("## {0}\n\n", page.Scene.Name);
resolved.AppendFormat("## {0}\n\n", page.Scene.Name);
var firstToggleCounter = 0; var firstToggleCounter = 0;
for (var i = 0; i < page.State.ActionsToShow.Count; i++) for (var i = 0; i < page.State.ActionsToShow.Count; i++)
@ -74,7 +66,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(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.RawDescription); var actionAnchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.Description);
var anchorDict = GetStateDictionary(page); var anchorDict = GetStateDictionary(page);
if ( if (
actionAnchors.Any( actionAnchors.Any(
@ -93,7 +85,7 @@
} }
} }
var anchors = Utilities.GetInstance(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.RawDescription); var anchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.Description);
var stateDict = GetStateDictionary(page); var stateDict = GetStateDictionary(page);
var text = var text =
RegexLib.EmptyListItem.Replace( RegexLib.EmptyListItem.Replace(

View File

@ -1,5 +1,7 @@
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;
@ -7,32 +9,19 @@
internal class Utilities internal class Utilities
{ {
private List<FicdownException> _warnings { get; set; } public static Utilities GetInstance(string blockName, int lineNumber)
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(List<FicdownException> warnings, string blockName) public static Utilities GetInstance(string blockName)
{ {
return new Utilities return new Utilities
{ {
_warnings = warnings,
_blockName = blockName _blockName = blockName
}; };
} }
@ -45,7 +34,7 @@
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, int lineNumber, int colNumber) private Href ParseHref(string href)
{ {
var match = RegexLib.Href.Match(href); var match = RegexLib.Href.Match(href);
if (match.Success) if (match.Success)
@ -68,55 +57,27 @@
: null : null
}; };
} }
_warnings.Add(new FicdownException(_blockName, string.Format("Invalid href: {0}", href), lineNumber, colNumber)); throw new FicdownException(_blockName, _lineNumber, string.Format("Invalid href: {0}", href));
return null;
} }
public Anchor ParseAnchor(string anchorText, int lineNumber, int colNumber) public Anchor ParseAnchor(string anchorText)
{ {
var match = RegexLib.Anchors.Match(anchorText); var match = RegexLib.Anchors.Match(anchorText);
if (!match.Success) if (!match.Success) throw new FicdownException(_blockName, _lineNumber, string.Format("Invalid anchor: {0}", anchorText));
{ 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(m => return matches.Cast<Match>().Select(MatchToAnchor).ToList();
{
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, int lineNumber, int colNumber) private Anchor MatchToAnchor(Match match)
{ {
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"].Success var ttstr = match.Groups["title"].Value;
? match.Groups["title"].Value
: null;
var hrefstr = match.Groups["href"].Value; var hrefstr = match.Groups["href"].Value;
if (hrefstr.StartsWith(@"""")) if (hrefstr.StartsWith(@""""))
{ {
@ -127,22 +88,15 @@
{ {
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 = !string.IsNullOrEmpty(ttstr) ? ttstr : null,
Href = ParseHref(hrefstr, lineNumber, colNumber), Href = ParseHref(hrefstr)
LineNumber = lineNumber,
ColNumber = colNumber
}; };
} }
public IDictionary<bool, string> ParseConditionalText(Anchor anchor) public IDictionary<bool, string> ParseConditionalText(string text)
{ {
var match = RegexLib.ConditionalText.Match(anchor.Text); var match = RegexLib.ConditionalText.Match(text);
if (!match.Success) if (!match.Success) throw new FicdownException(_blockName, _lineNumber, string.Format(@"Invalid conditional text: {0}", text));
{
_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,7 +3,6 @@
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;
@ -11,7 +10,6 @@
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;
@ -19,8 +17,6 @@
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
{ {
@ -29,7 +25,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, Warnings); _manager = new StateManager(_story);
_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>();
@ -60,7 +56,6 @@
_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
@ -68,7 +63,6 @@
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();
@ -77,13 +71,9 @@
_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)
@ -102,7 +92,6 @@
} }
// 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))
@ -136,15 +125,15 @@
var states = new HashSet<string>(); var states = new HashSet<string>();
var anchors = Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.RawDescription).ToList(); var anchors = Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.Description).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(Warnings, action.Toggle, action.LineNumber).ParseAnchors(action.RawDescription)); anchors.AddRange(Utilities.GetInstance(action.Toggle, action.LineNumber).ParseAnchors(action.Description));
} }
var conditionals = var conditionals =
anchors.SelectMany( anchors.SelectMany(
a => a.Href != null && a.Href.Conditions != null ? a.Href.Conditions.Select(c => c.Key) : new string[] {}) a => 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);
@ -154,46 +143,32 @@
// 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);
var anchor = anchors.FirstOrDefault(a => foreach (var conditional in conditionals) _manager.ToggleStateOn(affected, conditional);
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 != null && (a.Href.Target != null || a.Href.Toggles != null))) foreach (var anchor in anchors.Where(a => 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(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor)[ Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor.Text)[
Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page), Utilities.GetInstance(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))
{ currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
if (!currentState.Page.Links.ContainsKey(anchor.Original))
currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash)) if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash))
{ {
states.Add(newState.UniqueHash); states.Add(newState.UniqueHash);
var newAffected = new List<State>(currentState.AffectedStates); var newAffected = new List<State>(currentState.AffectedStates);
newAffected.Add(newState.AffectedState); newAffected.Add(newState.AffectedState);
_processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected}); _processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected});
}
} }
} }
} }

View File

@ -1,13 +1,11 @@
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

@ -11,33 +11,16 @@
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;
private List<FicdownException> _warnings { get; set; } public StateManager(Story story)
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;
@ -45,34 +28,23 @@
var toggle in var toggle in
allScenes.SelectMany( allScenes.SelectMany(
sc => sc =>
Utilities.GetInstance(_warnings, sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription) Utilities.GetInstance(sc.Name, sc.LineNumber).ParseAnchors(sc.Description)
.SelectMany( .SelectMany(
a => a =>
a.Href != null && a.Href.Toggles != 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
@ -89,7 +61,7 @@
ActionsToShow = new BitArray(_actionCount), ActionsToShow = new BitArray(_actionCount),
ActionFirstToggles = null ActionFirstToggles = null
}, },
Scene = scene.First(), Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null),
StateMatrix = _stateMatrix StateMatrix = _stateMatrix
}; };
} }
@ -111,7 +83,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(_warnings, _story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].RawDescription) Utilities.GetInstance(_story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].Description)
.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]]);
} }
@ -121,16 +93,13 @@
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, anchor, target, newState.State.PlayerState); newState.Scene = GetScene(current.Scene.Name, current.Scene.LineNumber, target, newState.State.PlayerState);
return newState; return newState;
} }
public void ToggleStateOn(State state, string toggle, string blockName, int lineNumber, int colNumber) public void ToggleStateOn(State state, string toggle)
{ {
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)
@ -138,14 +107,14 @@
state.ScenesSeen[sceneId - 1] = true; state.ScenesSeen[sceneId - 1] = true;
} }
public string GetUniqueHash(State state, string sceneKey) public static 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.And(_scenesSeenMask).CopyTo(combined, state.PlayerState.Count); state.ScenesSeen.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,
@ -153,15 +122,11 @@
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 string GetCompressedHash(PageState page) public static string GetCompressedHash(PageState page)
{ {
var compressed = new State var compressed = new State
{ {
@ -173,26 +138,24 @@
return GetUniqueHash(compressed, page.Scene.Key); return GetUniqueHash(compressed, page.Scene.Key);
} }
private Scene GetScene(string blockName, Anchor anchor, string target, BitArray playerState) private Scene GetScene(string blockName, int lineNumber, 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])
{ {
Scene newScene = null; if (ConditionsMatch(scene, playerState) &&
foreach (var scene in _story.Scenes[target]) (newScene == null || newScene.Conditions == null ||
scene.Conditions.Count > newScene.Conditions.Count))
{ {
if (ConditionsMatch(scene, playerState) && newScene = scene;
(newScene == null || newScene.Conditions == null ||
(scene.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;
} }
_warnings.Add(new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber)); if (newScene == null)
return null; throw new FicdownException(blockName, lineNumber, string.Format("Scene {0} reached with unmatched player state", target));
return newScene;
} }
private bool ConditionsMatch(Scene scene, BitArray playerState) private bool ConditionsMatch(Scene scene, BitArray playerState)
@ -200,18 +163,15 @@
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)) if(!_stateMatrix.ContainsKey(c.Key)) throw new FicdownException(scene.Name, scene.LineNumber, string.Format("Reference to non-existent state: {0}", c.Key));
_warnings.Add(new FicdownException(scene.Name, string.Format("Conditional for undefined state: {0}", c.Key), scene.LineNumber));
}); });
return scene.Conditions.Where(c => _stateMatrix.ContainsKey(c.Key)).All(c => playerState[_stateMatrix[c.Key]] == c.Value); return scene.Conditions.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

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("FicDown.Parser")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("FicDown.Parser")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("f59ab52e-a106-4ed4-b004-71f417a67edf")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -89,7 +89,6 @@
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;
@ -99,12 +98,10 @@
_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);

View File

@ -3,16 +3,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Markdig; using MarkdownSharp;
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;
public List<FicdownException> Warnings { private get; set; } protected readonly Markdown Markdown;
public string IndexTemplate { get; set; } public string IndexTemplate { get; set; }
public string SceneTemplate { get; set; } public string SceneTemplate { get; set; }
@ -24,6 +23,7 @@
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,12 +40,11 @@
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.ToHtml(story.Description)}, {"Description", Markdown.Transform(story.Description)},
{"FirstScene", string.Format("{0}.html", story.FirstPage)} {"FirstScene", string.Format("{0}.html", story.FirstPage)}
}); });
@ -56,7 +55,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(Warnings, page.Name).ParseAnchors(page.Content)) foreach (var anchor in Utilities.GetInstance(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);
@ -71,7 +70,7 @@
{ {
{"Language", _language}, {"Language", _language},
{"Title", story.Name}, {"Title", story.Name},
{"Content", Markdown.ToHtml(content)} {"Content", Markdown.Transform(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);

View File

@ -1,11 +1,10 @@
namespace Ficdown.Parser.Render namespace Ficdown.Parser.Render
{ {
using System.Collections.Generic; using System.Security.Cryptography.X509Certificates;
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; }

View File

@ -2,12 +2,9 @@ namespace Ficdown.Parser.Render
{ {
using System; using System;
using System.IO; using System.IO;
using System.Reflection;
public static class Template public static class Template
{ {
public static string BaseDir => Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).AbsolutePath);
public static string Index public static string Index
{ {
get { return GetFileContents("Views/index.html"); } get { return GetFileContents("Views/index.html"); }
@ -25,7 +22,7 @@ namespace Ficdown.Parser.Render
private static string GetFileContents(string fname) private static string GetFileContents(string fname)
{ {
var path = Path.Combine(Template.BaseDir, "Render", fname); var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Format("Render/{0}", fname));
return File.ReadAllText(path); return File.ReadAllText(path);
} }
} }

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="DotNetZip" version="1.9.1.8" targetFramework="net45" />
<package id="Epub4Net" version="1.2.0" targetFramework="net45" />
<package id="MarkdownSharp" version="1.13.0.0" targetFramework="net45" />
</packages>

View File

@ -1,8 +1,8 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 2013
VisualStudioVersion = 15.0.26124.0 VisualStudioVersion = 12.0.31101.0
MinimumVisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficdown.Parser", "Ficdown.Parser\Ficdown.Parser.csproj", "{780F652D-7541-4171-BB89-2D263D3961DC}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficdown.Parser", "Ficdown.Parser\Ficdown.Parser.csproj", "{780F652D-7541-4171-BB89-2D263D3961DC}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficdown.Parser.Tests", "Ficdown.Parser.Tests\Ficdown.Parser.Tests.csproj", "{756192E2-BA47-4850-8096-289D44878A7E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficdown.Parser.Tests", "Ficdown.Parser.Tests\Ficdown.Parser.Tests.csproj", "{756192E2-BA47-4850-8096-289D44878A7E}"

View File

@ -1,21 +0,0 @@
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.

View File

@ -1,19 +0,0 @@
build:
dotnet build
clean:
rm -rf */bin */obj
rebuild: clean build
test:
dotnet test Ficdown.Parser.Tests
publish: clean
rm -rf /tmp/ficdown*
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 .
dotnet publish --self-contained -c Release -r win-x64 Ficdown.Console
7z a -tzip /tmp/ficdown-win64.zip -w ./Ficdown.Console/bin/Release/netcoreapp2.1/win-x64/publish/*
dotnet publish --self-contained -c Release -r osx-x64 Ficdown.Console
tar -C Ficdown.Console/bin/Release/netcoreapp2.1/osx-x64/publish -cvzf /tmp/ficdown-osx64.tar.gz .

View File

@ -1,16 +1,18 @@
# Ficdown # Ficdown
Ficdown is a system for building interactive fiction using MarkDown syntax. Ficdown is a system for building interactive fiction using MarkDown syntax. See [Ficdown.com](http://www.ficdown.com) for more information.
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.
## Dependencies ## Dependencies
Ficdown is written using .NET Core and should run on Windows, Linux, and OSX without needing any additional system dependencies installed. Ficdown is written using .NET and should run on recent versions of Windows without requiring any additional downloads. Older versions may need to have .NET 4.5 installed via Windows Software Update.
It has been written and tested on Linux using [Mono](http://www.mono-project.com).
## Obtaining ## Obtaining
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. If you want to use Ficdown to convert your stories into ebooks, download *ficdown.zip* from the latest release on the [releases](https://github.com/rudism/Ficdown/releases) page and decompress it somewhere on your hard drive. Ficdown does not include an installer, the ficdown.exe utility is included directly in the zip archive.
## Usage ## Usage
@ -22,22 +24,22 @@ Once in your command prompt, you can run ficdown by typing `ficdown.exe` and the
### Linux/Mac OS X ### Linux/Mac OS X
The pre-built releases are self-contained .NET Core deployments, so you should be able to just run the `ficdown` executable after decompressing it. You must have Mono installed to use ficdown.exe. Assuming it is installed an on your path, from your command line in the ficdown folder you would just need to type `mono ficdown.exe` and include your command line options to pass to ficdown
### Options ### Options
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|lint) --format (html|epub)
--in "/path/to/source.md" (lint reads stdin) --in "/path/to/source.md"
[--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"]
[--author "Author Name"] [--author "Author Name"]
[--debug] [--debug]
Arguments surrounded by square brackets are optional, everything else is required. It should be noted that while the help text shows Linux-style paths, these will not work on Windows. On Windows you should pass regular paths as you normally would (for example `--in "C:\Users\Me\Documents\MyStory.md`). Options surrounded by square brackets are optional, everything else is required. It should be noted that while the help text shows Linux-style paths, these will not work on Windows. On Windows you should pass regular paths as you normally would (for example `--in "C:\Users\Me\Documents\MyStory.md`).
#### --format #### --format
@ -85,10 +87,6 @@ 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.
## Additional Tools ### Interactive Website
- 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 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-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

4
scripts/build.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
DIR=`dirname $0`
xbuild $DIR/../Ficdown.sln

5
scripts/rebuild.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
DIR=`dirname $0`
rm -rf $DIR/../*/bin $DIR/../*/obj
xbuild $DIR/../Ficdown.sln

10
scripts/release.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/sh
DIR=`dirname $0`
rm -rf $DIR/../*/bin $DIR/../*/obj
xbuild /p:Configuration=Release $DIR/../Ficdown.sln
cp -R $DIR/../Ficdown.Console/bin/Release /tmp/ficdown
rm -f /tmp/ficdown/*.mdb
mv /tmp/ficdown/Ficdown.Console.exe /tmp/ficdown/ficdown.exe
zip -j /tmp/ficdown.zip /tmp/ficdown/*
rm -rf /tmp/ficdown

4
scripts/run.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
DIR=`dirname $0`
mono $DIR/../Ficdown.Console/bin/Debug/Ficdown.Console.exe "$@"

7
scripts/test.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/sh
DIR=`dirname $0`
if [ ! -d "$DIR/xunit.runner.console.2.0.0" ]; then
nuget install xunit.runner.console -Version 2.0.0 -OutputDirectory $DIR
fi
mono --debug $DIR/xunit.runner.console.2.0.0/tools/xunit.console.exe $DIR/../Ficdown.Parser.Tests/bin/Debug/Ficdown.Parser.Tests.dll