Compare commits

...

20 Commits
1.0.1 ... main

Author SHA1 Message Date
Rudis Muiznieks f09aab9f82 Update 'README.md' 2023-03-18 20:01:13 +00:00
Rudis Muiznieks 03a612bfc0
Update README.md 2020-05-11 08:20:05 -05:00
Rudis Muiznieks 3a81abf1b1 added additional tool links to README 2020-05-11 08:02:18 -05:00
Rudis Muiznieks 7970c6cd4b added license 2020-05-11 07:43:14 -05:00
Rudis Muiznieks f7cab514cc fixed bug with scene resolution when one scene has no conditionals 2020-05-10 12:00:32 -05:00
Rudis Muiznieks 44ed5f165b improved logging facilities, improved game traversal efficiency 2019-09-21 18:52:00 -05:00
Rudis Muiznieks bace78ff2b updated readme, tagged new version 2019-05-12 09:58:52 -05:00
Rudis Muiznieks 5a1c3ecfae
Merge pull request #8 from rudism/lint-mode
Lint mode
2019-05-12 09:50:02 -05:00
Rudis Muiznieks 3206a015e6 note about lint mode in help text 2019-05-12 09:44:10 -05:00
Rudis Muiznieks 85a06d77cd output multiple errors instead of just dying on the first one encountered 2019-05-12 09:30:23 -05:00
Rudis Muiznieks b75a672ce2 handling more errors gracefully 2019-05-12 09:29:09 -05:00
Rudis Muiznieks 4ff4693fa3 improved error handling and reporting, plus lint mode for checking files for errors 2019-05-12 09:29:09 -05:00
Rudis Muiznieks ed54a71fb3 removing old releases on publish 2019-01-16 16:50:50 -06:00
Rudis Muiznieks 5ace751b4b fixed missing encoding problem 2019-01-16 16:47:17 -06:00
Rudis Muiznieks 098812c5a8 switched from mono to .net core 2019-01-16 16:20:24 -06:00
Rudis Muiznieks b40a6abf89
fixed publish script 2018-05-22 09:47:47 -05:00
Rudis Muiznieks 8210276c8e
better handling of scenes with empty titles
closes #5
2017-12-28 17:42:33 -06:00
Rudis Muiznieks dbfd02ff96
fixes for epub validation re issue #4 2017-04-13 11:50:07 -05:00
Rudis Muiznieks 52c8768ac2
fixed current directory method in template class 2016-09-20 14:07:34 -05:00
Rudis Muiznieks 6f04123f3f
replaced resx with simpler static file lookup 2016-09-20 12:19:11 -05:00
48 changed files with 680 additions and 1154 deletions

View File

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

View File

@ -1,64 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<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')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{C9D8CF8A-3CBE-4E45-91A3-37F8256A81A7}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Ficdown.Console</RootNamespace>
<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>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
<AssemblyName>ficdown</AssemblyName>
</PropertyGroup>
<ItemGroup>
<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" />
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj" />
</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,7 +3,6 @@
using System;
using System.Linq;
using System.IO;
using Microsoft.SqlServer.Server;
using Parser;
using Parser.Render;
using Parser.Model.Parser;
@ -16,8 +15,7 @@
{
if(e.ExceptionObject is FicdownException)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(e.ExceptionObject.ToString());
Console.WriteLine(e.ExceptionObject.ToString());
Environment.Exit(3);
}
};
@ -27,6 +25,8 @@
string tempdir = null;
string format = null;
string author = null;
string bookid = null;
string language = "en";
string images = null;
var debug = false;
@ -59,6 +59,12 @@
case "--author":
author = args[i + 1];
break;
case "--bookid":
bookid = args[i + 1];
break;
case "--language":
language = args[i + 1];
break;
case "--images":
images = args[i + 1];
break;
@ -77,98 +83,115 @@
ShowHelp();
return 0;
}
if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile))
{
ShowHelp();
return 1;
}
if (!File.Exists(infile))
{
Console.WriteLine(@"Source file {0} not found.", infile);
return 2;
}
if (string.IsNullOrWhiteSpace(output))
if (format == "html")
output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html");
else if (format == "epub")
output = "output.epub";
if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output)))
Logger.Initialize(debug);
var logger = Logger.GetLogger<Program>();
var lintMode = format == "lint";
if(!lintMode)
{
Console.WriteLine(@"Specified output {0} already exists.", output);
return 2;
}
if (!string.IsNullOrWhiteSpace(tempdir))
{
if (!Directory.Exists(tempdir))
if (string.IsNullOrWhiteSpace(format) || string.IsNullOrWhiteSpace(infile))
{
Console.WriteLine(@"Template directory {0} does not exist.", tempdir);
ShowHelp();
return 1;
}
if (!File.Exists(infile))
{
logger.Error($"Source file {infile} not found.");
return 2;
}
if (!File.Exists(Path.Combine(tempdir, "index.html")) ||
!File.Exists(Path.Combine(tempdir, "scene.html")) ||
!File.Exists(Path.Combine(tempdir, "styles.css")))
{
Console.WriteLine(
@"Template directory must contain ""index.html"", ""scene.html"", and ""style.css"" files.");
}
}
if (string.IsNullOrWhiteSpace(output))
if (format == "html")
output = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"html");
else if (format == "epub")
output = "output.epub";
else if(format == "lint")
lintMode = true;
if (!string.IsNullOrWhiteSpace(images) && !Directory.Exists(images))
{
Console.WriteLine(@"Images directory {0} does not exist.", images);
return 2;
if (!string.IsNullOrWhiteSpace(output) && (Directory.Exists(output) || File.Exists(output)))
{
logger.Error($"Specified output {output} already exists.");
return 2;
}
if (!string.IsNullOrWhiteSpace(tempdir))
{
if (!Directory.Exists(tempdir))
{
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))
{
logger.Error($"Images directory {images} does not exist.");
return 2;
}
}
var parser = new FicdownParser();
var storyText = File.ReadAllText(infile);
Console.WriteLine(@"Parsing story...");
string storyText;
if(!lintMode)
{
storyText = File.ReadAllText(infile);
logger.Log(@"Parsing story...");
}
else
{
storyText = Console.In.ReadToEnd();
}
var story = parser.ParseStory(storyText);
story.Orphans.ToList().ForEach(o =>
{
var currentColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Error.WriteLine("Warning (line {0}): {1} {2} is unreachable", o.LineNumber, o.Type, o.Name);
Console.ForegroundColor = currentColor;
});
parser.Warnings.Select(w => w.ToString()).Distinct().ToList().ForEach(s => logger.Raw(s));
story.Orphans.ToList().ForEach(o => logger.Raw($"Warning L{o.LineNumber},1: \"{o.Name}\": Unreachable {o.Type}"));
IRenderer rend;
switch (format)
if(!lintMode && parser.Warnings.Count() == 0)
{
case "html":
Directory.CreateDirectory(output);
rend = new HtmlRenderer();
break;
case "epub":
if (string.IsNullOrWhiteSpace(author))
{
Console.WriteLine(@"Epub format requires the --author argument.");
IRenderer rend;
switch (format)
{
case "html":
Directory.CreateDirectory(output);
rend = new HtmlRenderer(language);
break;
case "epub":
if (string.IsNullOrWhiteSpace(author))
{
logger.Error(@"Epub format requires the --author argument.");
return 1;
}
rend = new EpubRenderer(author, bookid, language);
break;
default:
ShowHelp();
return 1;
}
rend = new EpubRenderer(author);
break;
default:
ShowHelp();
return 1;
}
if (!string.IsNullOrWhiteSpace(tempdir))
{
rend.IndexTemplate = File.ReadAllText(Path.Combine(tempdir, "index.html"));
rend.SceneTemplate = File.ReadAllText(Path.Combine(tempdir, "scene.html"));
rend.StylesTemplate = File.ReadAllText(Path.Combine(tempdir, "styles.css"));
};
if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images;
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;
}
@ -176,14 +199,18 @@
private static void ShowHelp()
{
Console.WriteLine(
@"Usage: ficdown.exe
@"Usage: ficdown
--format (html|epub)
--in ""/path/to/source.md""
[--out ""/path/to/output""]
[--template ""/path/to/template/dir""]
[--images ""/path/to/images/dir""]
[--author ""Author Name""]
[--debug]");
[--bookid ""ePub Book ID""]
[--language ""language""]
[--debug]
or: ficdown --format lint
(reads input from stdin)");
}
}
}

View File

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

View File

@ -1,87 +1,23 @@
<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>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{756192E2-BA47-4850-8096-289D44878A7E}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<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>
<ItemGroup>
<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>
<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>
<None Include="packages.config" />
<None Include="TestStories\CloakOfDarkness.md" />
<None Include="TestStories\TheRobotKing.md" />
</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>
-->
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<None Include="TestStories\**\*" CopyToOutputDirectory="Always" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@ -1,10 +1,7 @@
namespace Ficdown.Parser.Tests
{
using System;
using System.IO;
using System.Text;
using Render;
using TestStories;
using Xunit;
public class IntegrationTests
@ -12,16 +9,17 @@
[Fact]
public void CanParseValidStoryFile()
{
Logger.Initialize(true);
var parser = new FicdownParser();
var storyText = Encoding.UTF8.GetString(Resources.CloakOfDarkness);
var storyText = File.ReadAllText(Path.Combine(Template.BaseDir, "TestStories", "CloakOfDarkness.md"));
var story = parser.ParseStory(storyText);
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "itest_output");
var path = Path.Combine(Template.BaseDir, "itest_output");
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
foreach (var file in Directory.GetFiles(path))
{
File.Delete(file);
}
var rend = new HtmlRenderer();
var rend = new HtmlRenderer("en");
rend.Render(story, path, true);
}
}

View File

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

@ -1,83 +0,0 @@
//------------------------------------------------------------------------------
// <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

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

View File

@ -1,6 +0,0 @@
<?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,40 +4,89 @@
namespace Ficdown.Parser
{
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using Model.Parser;
using Parser;
using Player;
public class FicdownParser
{
private static Logger _logger = Logger.GetLogger<FicdownParser>();
public List<FicdownException> Warnings { get; private set; }
private IBlockHandler _blockHandler;
internal IBlockHandler BlockHandler
{
get { return _blockHandler ?? (_blockHandler = new BlockHandler()); }
set { _blockHandler = value; }
get
{
return _blockHandler ??
(_blockHandler = new BlockHandler { Warnings = Warnings });
}
set
{
_blockHandler = value;
_blockHandler.Warnings = Warnings;
}
}
private IGameTraverser _gameTraverser;
internal IGameTraverser GameTraverser
{
get { return _gameTraverser ?? (_gameTraverser = new GameTraverser()); }
set { _gameTraverser = value; }
get { return _gameTraverser ??
(_gameTraverser = new GameTraverser { Warnings = Warnings }); }
set
{
_gameTraverser = value;
_gameTraverser.Warnings = Warnings;
}
}
private IStateResolver _stateResolver;
internal IStateResolver StateResolver
{
get { return _stateResolver ?? (_stateResolver = new StateResolver()); }
set { _stateResolver = value; }
get
{
return _stateResolver ??
(_stateResolver = new StateResolver { Warnings = Warnings });
}
set
{
_stateResolver = value;
_stateResolver.Warnings = Warnings;
}
}
public FicdownParser()
{
Warnings = new List<FicdownException>();
}
public ResolvedStory ParseStory(string storyText)
{
var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None);
_logger.Debug($"Parsed {lines.Length} lines.");
var blocks = BlockHandler.ExtractBlocks(lines);
_logger.Debug($"Extracted {blocks.Count()} 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;
var resolved = StateResolver.Resolve(GameTraverser.Enumerate(), story);
resolved.Orphans = GameTraverser.OrphanedScenes.Select(o => new Orphan

View File

@ -1,113 +1,19 @@
<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>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{780F652D-7541-4171-BB89-2D263D3961DC}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<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>
<ItemGroup>
<Reference Include="Epub4Net">
<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>
<Compile Include="Model\Parser\ResolvedPage.cs" />
<Compile Include="Model\Parser\ResolvedStory.cs" />
<Compile Include="Model\Player\PageState.cs" />
<Compile Include="Model\Player\State.cs" />
<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="Render\Template.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Template.resx</DependentUpon>
</Compile>
<Compile Include="Model\Parser\Line.cs" />
<Compile Include="Model\Parser\FicdownException.cs" />
<Compile Include="Parser\ParserExtensions.cs" />
<Compile Include="Model\Parser\Orphan.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<Content Include="Render\Views\scene.html" />
</ItemGroup>
<ItemGroup>
<Content Include="Render\Assets\styles.css" />
</ItemGroup>
<ItemGroup>
<Content Include="Render\Views\index.html" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Render\Template.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Template.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>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
</PropertyGroup>
<ItemGroup>
<None Include="Render\**\*.html" CopyToOutputDirectory="Always" />
<None Include="Render\**\*.css" CopyToOutputDirectory="Always" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Epub4Net" Version="1.2.0" />
<PackageReference Include="Ionic.Zip" Version="1.9.1.8" />
<PackageReference Include="Markdig" Version="0.17.1" />
<PackageReference Include="System.Security.Permissions" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" />
</ItemGroup>
</Project>

63
Ficdown.Parser/Logger.cs Normal file
View File

@ -0,0 +1,63 @@
namespace Ficdown.Parser
{
using System;
using System.Collections.Generic;
public class Logger
{
private static bool _initialized = false;
private static bool _debug = false;
private static Dictionary<Type, Logger> _cache;
public Type Type { get; private set; }
private Logger(Type type)
{
Type = type;
}
public static void Initialize(bool debug)
{
_debug = debug;
_cache = new Dictionary<Type, Logger>();
_initialized = true;
}
public static Logger GetLogger<T>()
{
var type = typeof(T);
lock(_cache)
{
if(!_cache.ContainsKey(type))
_cache.Add(type, new Logger(type));
}
return _cache[type];
}
private string Decorate(string message)
{
return $"{DateTime.Now.ToString("")} <{Type.Name}> {message}";
}
public void Raw(string message)
{
Console.WriteLine(message);
}
public void Log(string message)
{
Raw(Decorate(message));
}
public void Debug(string message)
{
if(!_debug) return;
Log($"DEBUG: {message}");
}
public void Error(string message, Exception ex = null)
{
Console.Error.WriteLine(Decorate($"ERROR: {message}"));
}
}
}

View File

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

View File

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

View File

@ -7,6 +7,8 @@
internal class PageState
{
public StateManager Manager { get; set; }
public Guid Id { get; set; }
public Scene Scene { get; set; }
public State State { get; set; }
@ -21,12 +23,12 @@
public string UniqueHash
{
get { return _uniqueHash ?? (_uniqueHash = StateManager.GetUniqueHash(State, Scene.Key)); }
get { return _uniqueHash ?? (_uniqueHash = Manager.GetUniqueHash(State, Scene.Key)); }
}
public string CompressedHash
{
get { return _compressedHash ?? (_compressedHash = StateManager.GetCompressedHash(this)); }
get { return _compressedHash ?? (_compressedHash = Manager.GetCompressedHash(this)); }
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,14 @@
internal class StateResolver : IStateResolver
{
private static Logger _logger = Logger.GetLogger<StateResolver>();
private static readonly Random _random = new Random((int) DateTime.Now.Ticks);
private readonly IDictionary<string, string> _pageNames;
private readonly HashSet<string> _usedNames;
private Story _story;
public List<FicdownException> Warnings { private get; set; }
public StateResolver()
{
_pageNames = new Dictionary<string, string>();
@ -23,6 +26,7 @@
public ResolvedStory Resolve(IEnumerable<PageState> pages, Story story)
{
_logger.Debug("Resolving story paths...");
_story = story;
return new ResolvedStory
{
@ -43,12 +47,15 @@
private string ResolveAnchor(string blockName, int lineNumber, Anchor anchor, IDictionary<string, bool> playerState, string targetHash)
{
var text = anchor.Text;
if (anchor.Href.Conditions != null)
if (anchor.Href != null && anchor.Href.Conditions != null)
{
var satisfied = Utilities.GetInstance(blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions);
var alts = Utilities.GetInstance(blockName, lineNumber).ParseConditionalText(text);
var replace = alts[satisfied];
text = RegexLib.EscapeChar.Replace(replace, string.Empty);
var satisfied = Utilities.GetInstance(Warnings, blockName, lineNumber).ConditionsMet(playerState, anchor.Href.Conditions);
var alts = Utilities.GetInstance(Warnings, blockName, lineNumber).ParseConditionalText(anchor);
if(alts != null)
{
var replace = alts[satisfied];
text = RegexLib.EscapeChar.Replace(replace, string.Empty);
}
}
return !string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(targetHash)
? string.Format("[{0}](/{1})", text, GetPageNameForHash(targetHash))
@ -58,7 +65,8 @@
private string ResolveDescription(PageState page)
{
var resolved = new StringBuilder();
resolved.AppendFormat("## {0}\n\n", page.Scene.Name);
if(!string.IsNullOrEmpty(page.Scene.Name))
resolved.AppendFormat("## {0}\n\n", page.Scene.Name);
var firstToggleCounter = 0;
for (var i = 0; i < page.State.ActionsToShow.Count; i++)
@ -66,7 +74,7 @@
if (page.State.ActionsToShow[i])
{
var actionTuple = _story.Actions.Single(a => a.Value.Id == i + 1);
var actionAnchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.Description);
var actionAnchors = Utilities.GetInstance(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(actionTuple.Value.RawDescription);
var anchorDict = GetStateDictionary(page);
if (
actionAnchors.Any(
@ -85,7 +93,7 @@
}
}
var anchors = Utilities.GetInstance(page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.Description);
var anchors = Utilities.GetInstance(Warnings, page.Scene.Name, page.Scene.LineNumber).ParseAnchors(page.Scene.RawDescription);
var stateDict = GetStateDictionary(page);
var text =
RegexLib.EmptyListItem.Replace(

View File

@ -1,7 +1,5 @@

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

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Model.Parser;
using Model.Player;
using Model.Story;
using Parser;
@ -10,6 +11,7 @@
internal class GameTraverser : IGameTraverser
{
private static Logger _logger = Logger.GetLogger<GameTraverser>();
private StateManager _manager;
private Queue<StateQueueItem> _processingQueue;
private IDictionary<string, PageState> _processed;
@ -17,6 +19,8 @@
private IDictionary<int, Action> _actionMatrix;
private bool _wasRun = false;
public List<FicdownException> Warnings { private get; set; }
private Story _story;
public Story Story
{
@ -25,7 +29,7 @@
{
_story = value;
_actionMatrix = _story.Actions.ToDictionary(a => a.Value.Id, a => a.Value);
_manager = new StateManager(_story);
_manager = new StateManager(_story, Warnings);
_processingQueue = new Queue<StateQueueItem>();
_processed = new Dictionary<string, PageState>();
_compressed = new Dictionary<string, PageState>();
@ -56,6 +60,7 @@
_wasRun = true;
// generate comprehensive enumeration
_logger.Debug("Enumerating story scenes...");
var initial = _manager.InitialState;
_processingQueue.Enqueue(new StateQueueItem
@ -63,6 +68,7 @@
Page = initial,
AffectedStates = new List<State> {initial.AffectedState}
});
var interval = 0;
while (_processingQueue.Count > 0)
{
var state = _processingQueue.Dequeue();
@ -71,9 +77,13 @@
_processed.Add(state.Page.UniqueHash, state.Page);
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
_logger.Debug("Processing scene links...");
foreach (var pageTuple in _processed)
{
foreach (var linkTuple in pageTuple.Value.Links)
@ -92,6 +102,7 @@
}
// compress redundancies
_logger.Debug("Compressing redundant scenes...");
foreach (var row in _processed)
{
if (!_compressed.ContainsKey(row.Value.CompressedHash))
@ -125,15 +136,15 @@
var states = new HashSet<string>();
var anchors = Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.Description).ToList();
var anchors = Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseAnchors(currentState.Page.Scene.RawDescription).ToList();
foreach (var action in GetActionsForPage(currentState.Page))
{
action.Visited = true;
anchors.AddRange(Utilities.GetInstance(action.Toggle, action.LineNumber).ParseAnchors(action.Description));
anchors.AddRange(Utilities.GetInstance(Warnings, action.Toggle, action.LineNumber).ParseAnchors(action.RawDescription));
}
var conditionals =
anchors.SelectMany(
a => a.Href.Conditions != null ? a.Href.Conditions.Select(c => c.Key) : new string[] {})
a => a.Href != null && a.Href.Conditions != null ? a.Href.Conditions.Select(c => c.Key) : new string[] {})
.Distinct()
.ToArray();
var hasFirstSeen = RegexLib.BlockQuotes.IsMatch(currentState.Page.Scene.Description);
@ -143,32 +154,46 @@
// signal to previous scenes that this scene's used conditionals are important
if(currentState.Page.Scene.Conditions != null)
foreach (var conditional in currentState.Page.Scene.Conditions)
_manager.ToggleStateOn(affected, conditional.Key);
foreach (var conditional in conditionals) _manager.ToggleStateOn(affected, conditional);
{
var anchor = anchors.FirstOrDefault(a =>
a.Href.Conditions != null
&& a.Href.Conditions.Keys.Contains(conditional.Key));
_manager.ToggleStateOn(affected, conditional.Key, currentState.Page.Scene.Name, anchor != 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
if (hasFirstSeen) _manager.ToggleSeenSceneOn(affected, currentState.Page.Scene.Id);
}
foreach (var anchor in anchors.Where(a => a.Href.Target != null || a.Href.Toggles != null))
foreach (var anchor in anchors.Where(a => a.Href != null && (a.Href.Target != null || a.Href.Toggles != null)))
{
// don't follow links that would be hidden
if (anchor.Href.Conditions != null &&
string.IsNullOrEmpty(
Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor.Text)[
Utilities.GetInstance(currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page),
Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ParseConditionalText(anchor)[
Utilities.GetInstance(Warnings, currentState.Page.Scene.Name, currentState.Page.Scene.LineNumber).ConditionsMet(StateResolver.GetStateDictionary(currentState.Page),
anchor.Href.Conditions)])) continue;
var newState = _manager.ResolveNewState(anchor, currentState.Page);
if (!currentState.Page.Links.ContainsKey(anchor.Original))
currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash))
if(newState.Scene != null)
{
states.Add(newState.UniqueHash);
var newAffected = new List<State>(currentState.AffectedStates);
newAffected.Add(newState.AffectedState);
_processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected});
if (!currentState.Page.Links.ContainsKey(anchor.Original))
currentState.Page.Links.Add(anchor.Original, newState.UniqueHash);
if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash))
{
states.Add(newState.UniqueHash);
var newAffected = new List<State>(currentState.AffectedStates);
newAffected.Add(newState.AffectedState);
_processingQueue.Enqueue(new StateQueueItem {Page = newState, AffectedStates = newAffected});
}
}
}
}

View File

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

View File

@ -11,16 +11,33 @@
internal class StateManager
{
private static Logger _logger = Logger.GetLogger<StateManager>();
private readonly Story _story;
private readonly Dictionary<string, int> _stateMatrix;
private readonly int _sceneCount;
private readonly int _actionCount;
private BitArray _scenesSeenMask;
public StateManager(Story story)
private List<FicdownException> _warnings { get; set; }
public StateManager(Story story, List<FicdownException> warnings)
{
_warnings = warnings;
_story = story;
var allScenes = _story.Scenes.SelectMany(s => s.Value);
_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;
_stateMatrix = new Dictionary<string, int>();
var state = 0;
@ -28,23 +45,34 @@
var toggle in
allScenes.SelectMany(
sc =>
Utilities.GetInstance(sc.Name, sc.LineNumber).ParseAnchors(sc.Description)
Utilities.GetInstance(_warnings, sc.Name, sc.LineNumber).ParseAnchors(sc.RawDescription)
.SelectMany(
a =>
a.Href.Toggles != null
a.Href != null && a.Href.Toggles != null
? a.Href.Toggles.Where(t => !_stateMatrix.ContainsKey(t))
: new string[] {})))
{
_stateMatrix.Add(toggle, state++);
}
_logger.Debug($"{_sceneCount} scenes ({masked} can change state).");
_logger.Debug($"{_actionCount} actions.");
_logger.Debug($"{_stateMatrix.Count()} states.");
}
public PageState InitialState
{
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
{
Manager = this,
Id = Guid.Empty,
Links = new Dictionary<string, string>(),
State = new State
@ -61,7 +89,7 @@
ActionsToShow = new BitArray(_actionCount),
ActionFirstToggles = null
},
Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null),
Scene = scene.First(),
StateMatrix = _stateMatrix
};
}
@ -83,7 +111,7 @@
if(actionFirstToggles == null) actionFirstToggles = new List<bool>();
newState.State.ActionsToShow[_story.Actions[toggle].Id - 1] = true;
if (
Utilities.GetInstance(_story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].Description)
Utilities.GetInstance(_warnings, _story.Actions[toggle].Toggle, _story.Actions[toggle].LineNumber).ParseAnchors(_story.Actions[toggle].RawDescription)
.Any(a => a.Href.Conditions != null && a.Href.Conditions.ContainsKey(toggle)))
actionFirstToggles.Add(!current.State.PlayerState[_stateMatrix[toggle]]);
}
@ -93,13 +121,16 @@
newState.State.ActionFirstToggles = actionFirstToggles != null
? new BitArray(actionFirstToggles.ToArray())
: null;
newState.Scene = GetScene(current.Scene.Name, current.Scene.LineNumber, target, newState.State.PlayerState);
newState.Scene = GetScene(current.Scene.Name, anchor, target, newState.State.PlayerState);
return newState;
}
public void ToggleStateOn(State state, string toggle)
public void ToggleStateOn(State state, string toggle, string blockName, int lineNumber, int colNumber)
{
state.PlayerState[_stateMatrix[toggle]] = true;
if(_stateMatrix.ContainsKey(toggle))
state.PlayerState[_stateMatrix[toggle]] = true;
else
_warnings.Add(new FicdownException(blockName, string.Format("Conditional for undefined state: {0}", toggle), lineNumber, colNumber));
}
public void ToggleSeenSceneOn(State state, int sceneId)
@ -107,14 +138,14 @@
state.ScenesSeen[sceneId - 1] = true;
}
public static string GetUniqueHash(State state, string sceneKey)
public string GetUniqueHash(State state, string sceneKey)
{
var combined =
new bool[
state.PlayerState.Count + state.ScenesSeen.Count + state.ActionsToShow.Count +
(state.ActionFirstToggles != null ? state.ActionFirstToggles.Count : 0)];
state.PlayerState.CopyTo(combined, 0);
state.ScenesSeen.CopyTo(combined, state.PlayerState.Count);
state.ScenesSeen.And(_scenesSeenMask).CopyTo(combined, state.PlayerState.Count);
state.ActionsToShow.CopyTo(combined, state.PlayerState.Count + state.ScenesSeen.Count);
if (state.ActionFirstToggles != null)
state.ActionFirstToggles.CopyTo(combined,
@ -122,11 +153,15 @@
var ba = new BitArray(combined);
var byteSize = (int)Math.Ceiling(combined.Length / 8.0);
var encoded = new byte[byteSize];
for(var i = 0; i < byteSize; i++)
{
encoded[i] = 0;
}
ba.CopyTo(encoded, 0);
return string.Format("{0}=={1}", sceneKey, Convert.ToBase64String(encoded));
}
public static string GetCompressedHash(PageState page)
public string GetCompressedHash(PageState page)
{
var compressed = new State
{
@ -138,24 +173,26 @@
return GetUniqueHash(compressed, page.Scene.Key);
}
private Scene GetScene(string blockName, int lineNumber, string target, BitArray playerState)
private Scene GetScene(string blockName, Anchor anchor, string target, BitArray playerState)
{
if (!_story.Scenes.ContainsKey(target))
throw new FicdownException(blockName, lineNumber, string.Format("Encountered link to non-existent scene: {0}", target));
Scene newScene = null;
foreach (var scene in _story.Scenes[target])
if (_story.Scenes.ContainsKey(target))
{
if (ConditionsMatch(scene, playerState) &&
(newScene == null || newScene.Conditions == null ||
scene.Conditions.Count > newScene.Conditions.Count))
Scene newScene = null;
foreach (var scene in _story.Scenes[target])
{
newScene = scene;
if (ConditionsMatch(scene, playerState) &&
(newScene == null || newScene.Conditions == null ||
(scene.Conditions != null && scene.Conditions.Count > newScene.Conditions.Count)))
{
newScene = scene;
}
}
if (newScene == null)
_warnings.Add(new FicdownException(blockName, string.Format("Link to scene that is undefined for conditionals: {0}", target), anchor.LineNumber, anchor.ColNumber));
return newScene;
}
if (newScene == null)
throw new FicdownException(blockName, lineNumber, string.Format("Scene {0} reached with unmatched player state", target));
return newScene;
_warnings.Add(new FicdownException(blockName, string.Format("Link to undefined scene: {0}", target), anchor.LineNumber, anchor.ColNumber));
return null;
}
private bool ConditionsMatch(Scene scene, BitArray playerState)
@ -163,15 +200,18 @@
if (scene.Conditions == null) return true;
scene.Conditions.ToList().ForEach(c =>
{
if(!_stateMatrix.ContainsKey(c.Key)) throw new FicdownException(scene.Name, scene.LineNumber, string.Format("Reference to non-existent state: {0}", c.Key));
if(!_stateMatrix.ContainsKey(c.Key))
_warnings.Add(new FicdownException(scene.Name, string.Format("Conditional for undefined state: {0}", c.Key), scene.LineNumber));
});
return scene.Conditions.All(c => playerState[_stateMatrix[c.Key]] == c.Value);
return scene.Conditions.Where(c => _stateMatrix.ContainsKey(c.Key)).All(c => playerState[_stateMatrix[c.Key]] == c.Value);
}
private PageState ClonePage(PageState page)
{
return new PageState
{
Manager = this,
Id = Guid.NewGuid(),
Links = new Dictionary<string, string>(),
State = new State

View File

@ -1,36 +0,0 @@
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,15 +89,22 @@
public class EpubRenderer : HtmlRenderer
{
private static readonly Logger _logger = Logger.GetLogger<EpubRenderer>();
private readonly string _author;
private readonly string _bookId;
private readonly string _language;
public EpubRenderer(string author) : base()
public EpubRenderer(string author, string bookId, string language) : base(language)
{
_author = author;
_bookId = bookId ?? Guid.NewGuid().ToString("D");
_language = language ?? "en";
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
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());
Directory.CreateDirectory(temppath);
base.Render(story, temppath, debug);
@ -113,6 +120,8 @@
select new Chapter(Path.Combine(temppath, fname), fname, fname.Replace(".html", string.Empty)));
var epub = new Epub(Story.Name, _author, chapters);
epub.BookId = _bookId;
epub.Language = _language;
epub.AddResourceFile(new ResourceFile("styles.css", Path.Combine(temppath, "styles.css"), "text/css"));
if (!string.IsNullOrWhiteSpace(ImageDir))

View File

@ -3,13 +3,16 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MarkdownSharp;
using Markdig;
using Model.Parser;
using Parser;
public class HtmlRenderer : IRenderer
{
protected readonly Markdown Markdown;
private static Logger _logger = Logger.GetLogger<HtmlRenderer>();
private readonly string _language;
public List<FicdownException> Warnings { private get; set; }
public string IndexTemplate { get; set; }
public string SceneTemplate { get; set; }
@ -18,9 +21,9 @@
protected ResolvedStory Story { get; set; }
public HtmlRenderer()
public HtmlRenderer(string language)
{
Markdown = new Markdown();
_language = language;
}
public virtual void Render(ResolvedStory story, string outPath, bool debug = false)
@ -37,10 +40,12 @@
protected void GenerateHtml(ResolvedStory story, string outPath, bool debug)
{
_logger.Debug("Generating HTML...");
var index = FillTemplate(IndexTemplate ?? Template.Index, new Dictionary<string, string>
{
{"Language", _language},
{"Title", story.Name},
{"Description", Markdown.Transform(story.Description)},
{"Description", Markdown.ToHtml(story.Description)},
{"FirstScene", string.Format("{0}.html", story.FirstPage)}
});
@ -51,7 +56,7 @@
File.WriteAllText(Path.Combine(outPath, "styles.css"), StylesTemplate ?? Template.Styles);
var content = page.Content;
foreach (var anchor in Utilities.GetInstance(page.Name).ParseAnchors(page.Content))
foreach (var anchor in Utilities.GetInstance(Warnings, page.Name).ParseAnchors(page.Content))
{
var newAnchor = string.Format("[{0}]({1}.html)", anchor.Text, anchor.Href.Target);
content = content.Replace(anchor.Original, newAnchor);
@ -64,8 +69,9 @@
var scene = FillTemplate(SceneTemplate ?? Template.Scene, new Dictionary<string, string>
{
{"Language", _language},
{"Title", story.Name},
{"Content", Markdown.Transform(content)}
{"Content", Markdown.ToHtml(content)}
});
File.WriteAllText(Path.Combine(outPath, string.Format("{0}.html", page.Name)), scene);

View File

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

View File

@ -1,144 +0,0 @@
//------------------------------------------------------------------------------
// <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.Render {
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 Template {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Template() {
}
/// <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.Render.Template", typeof(Template).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 string similar to &lt;!DOCTYPE html&gt;
///
///&lt;html lang=&quot;en&quot; xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;
///&lt;head&gt;
/// &lt;meta charset=&quot;utf-8&quot; /&gt;
/// &lt;title&gt;@Title&lt;/title&gt;
/// &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;styles.css&quot;/&gt;
///&lt;/head&gt;
/// &lt;body&gt;
/// &lt;h1 class=&quot;title&quot;&gt;@Title&lt;/h1&gt;
/// @Description
/// &lt;p&gt;&lt;a href=&quot;@FirstScene&quot;&gt;Begin reading...&lt;/a&gt;&lt;/p&gt;
/// &lt;/body&gt;
///&lt;/html&gt;.
/// </summary>
internal static string Index {
get {
return ResourceManager.GetString("Index", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;!DOCTYPE html&gt;
///
///&lt;html lang=&quot;en&quot; xmlns=&quot;http://www.w3.org/1999/xhtml&quot;&gt;
///&lt;head&gt;
/// &lt;meta charset=&quot;utf-8&quot; /&gt;
/// &lt;title&gt;@Title&lt;/title&gt;
/// &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;styles.css&quot; /&gt;
///&lt;/head&gt;
///&lt;body&gt;
/// @Content
///&lt;/body&gt;
///&lt;/html&gt;.
/// </summary>
internal static string Scene {
get {
return ResourceManager.GetString("Scene", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to /* Adapted from http://wiki.mobileread.com/wiki/CSS_template */
///
///@page {
/// margin-top: 30px;
/// margin-bottom: 20px;
///}
///
///body {
/// margin-right: 30px;
/// margin-left: 30px;
/// padding: 0;
///}
///
///img {
/// max-width: 100%;
/// oeb-column-number: 1;
/// display: inline-block;
///}
///
///a {
/// font-style: italic;
/// color: #000;
/// text-decoration: none;
///}
///
///h1.title {
/// font-family: Verdana, Geneva, sans-serif;
/// font-size: x-large;
/// text-align: center;
/// font-weight: bold;
/// [rest of string was truncated]&quot;;.
/// </summary>
internal static string Styles {
get {
return ResourceManager.GetString("Styles", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,32 @@
namespace Ficdown.Parser.Render
{
using System;
using System.IO;
using System.Reflection;
public static class Template
{
public static string BaseDir => Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).AbsolutePath);
public static string Index
{
get { return GetFileContents("Views/index.html"); }
}
public static string Scene
{
get { return GetFileContents("Views/scene.html"); }
}
public static string Styles
{
get { return GetFileContents("Assets/styles.css"); }
}
private static string GetFileContents(string fname)
{
var path = Path.Combine(Template.BaseDir, "Render", fname);
return File.ReadAllText(path);
}
}
}

View File

@ -1,130 +0,0 @@
<?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="Index" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Views\index.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="Scene" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Views\scene.html;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="Styles" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Assets\styles.css;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
</root>

View File

@ -1,8 +1,9 @@
<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<html lang="@Language" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>@Title</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
@ -11,4 +12,4 @@
@Description
<p><a href="@FirstScene">Begin reading...</a></p>
</body>
</html>
</html>

View File

@ -1,12 +1,13 @@
<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<html lang="@Language" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>@Title</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body>
@Content
</body>
</html>
</html>

View File

@ -1,6 +0,0 @@
<?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
# Visual Studio 2013
VisualStudioVersion = 12.0.31101.0
MinimumVisualStudioVersion = 10.0.40219.1
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficdown.Parser", "Ficdown.Parser\Ficdown.Parser.csproj", "{780F652D-7541-4171-BB89-2D263D3961DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ficdown.Parser.Tests", "Ficdown.Parser.Tests\Ficdown.Parser.Tests.csproj", "{756192E2-BA47-4850-8096-289D44878A7E}"

21
LICENSE.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Rudis Muiznieks
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
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,18 +1,16 @@
# Ficdown
Ficdown is a system for building interactive fiction using MarkDown syntax. See [Ficdown.com](http://www.ficdown.com) for more information.
Ficdown is a system for building interactive fiction using MarkDown syntax.
This project contains the core Ficdown library for parsing Ficdown stories, as well as a console application that can be used to generate HTML or epub ebook formats.
## Dependencies
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).
Ficdown is written using .NET Core and should run on Windows, Linux, and OSX without needing any additional system dependencies installed.
## Obtaining
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.
If you want to use Ficdown to convert your stories into ebooks, download the latest version from the [releases](https://code.sitosis.com/rudism/ficdown/releases) page and decompress it somewhere on your hard drive. Ficdown does not include an installer, the application and all of its dependencies are included directly in the zip archive.
## Usage
@ -24,22 +22,22 @@ Once in your command prompt, you can run ficdown by typing `ficdown.exe` and the
### Linux/Mac OS X
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
The pre-built releases are self-contained .NET Core deployments, so you should be able to just run the `ficdown` executable after decompressing it.
### Options
Running ficdown.exe without any arguments will produce the following help text:
Usage: ficdown.exe
--format (html|epub)
--in "/path/to/source.md"
--format (html|epub|lint)
--in "/path/to/source.md" (lint reads stdin)
[--out "/path/to/output"]
[--template "/path/to/template/dir"]
[--images "/path/to/images/dir"]
[--author "Author Name"]
[--debug]
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`).
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`).
#### --format
@ -87,6 +85,10 @@ If you pass this option, all of the pages in your story will include output at t
To generate other formats than HTML or epub, you will have to use third party tools. [Calibre](http://www.calibre-ebook.com) is a popular ebook management suite that includes the ability to convert books from almost any format to any other format. Also, Amazon has an official tool called [KindleGen](http://www.amazon.com/gp/feature.html?docId=1000765211) that you can use to convert your epub to a format that can be read on Kindles.
### Interactive Website
## Additional Tools
Ficdown stories can be played interactively in a web browser without even requiring the command line utility here. See [Ficdown.js](https://github.com/rudism/Ficdown.js) for a Javascript Ficdown parser and interpreter that you can include on your own website to present your Ficdown stories.
- Ficdown stories can be played interactively in a web browser without even requiring the command line utility here. See [Ficdown.js](https://code.sitosis.com/rudism/ficdown.js) for a Javascript Ficdown parser and interpreter that you can include on your own website to present your Ficdown stories.
- [Ficdown-editor](https://byfernanz.github.io/ficdown-editor/) is a web-based GUI for writing Ficdown.
- [Prop](https://github.com/ByFernanz/prop) is a YAML-header style preprocessor for Ficdown

View File

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

View File

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

View File

@ -1,10 +0,0 @@
#!/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

View File

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

View File

@ -1,7 +0,0 @@
#!/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