Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
Rudis Muiznieks | f09aab9f82 | |
Rudis Muiznieks | 03a612bfc0 | |
Rudis Muiznieks | 3a81abf1b1 | |
Rudis Muiznieks | 7970c6cd4b | |
Rudis Muiznieks | f7cab514cc | |
Rudis Muiznieks | 44ed5f165b | |
Rudis Muiznieks | bace78ff2b | |
Rudis Muiznieks | 5a1c3ecfae | |
Rudis Muiznieks | 3206a015e6 | |
Rudis Muiznieks | 85a06d77cd | |
Rudis Muiznieks | b75a672ce2 | |
Rudis Muiznieks | 4ff4693fa3 | |
Rudis Muiznieks | ed54a71fb3 | |
Rudis Muiznieks | 5ace751b4b | |
Rudis Muiznieks | 098812c5a8 | |
Rudis Muiznieks | b40a6abf89 | |
Rudis Muiznieks | 8210276c8e |
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
|
||||
</startup>
|
||||
</configuration>
|
|
@ -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>
|
|
@ -3,7 +3,6 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Microsoft.SqlServer.Server;
|
||||
using Parser;
|
||||
using Parser.Render;
|
||||
using Parser.Model.Parser;
|
||||
|
@ -16,8 +15,7 @@
|
|||
{
|
||||
if(e.ExceptionObject is FicdownException)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.Error.WriteLine(e.ExceptionObject.ToString());
|
||||
Console.WriteLine(e.ExceptionObject.ToString());
|
||||
Environment.Exit(3);
|
||||
}
|
||||
};
|
||||
|
@ -85,98 +83,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(language);
|
||||
break;
|
||||
case "epub":
|
||||
if (string.IsNullOrWhiteSpace(author))
|
||||
{
|
||||
Console.WriteLine(@"Epub format requires the --author argument.");
|
||||
IRenderer rend;
|
||||
switch (format)
|
||||
{
|
||||
case "html":
|
||||
Directory.CreateDirectory(output);
|
||||
rend = new HtmlRenderer(language);
|
||||
break;
|
||||
case "epub":
|
||||
if (string.IsNullOrWhiteSpace(author))
|
||||
{
|
||||
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, bookid, language);
|
||||
break;
|
||||
default:
|
||||
ShowHelp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tempdir))
|
||||
{
|
||||
rend.IndexTemplate = File.ReadAllText(Path.Combine(tempdir, "index.html"));
|
||||
rend.SceneTemplate = File.ReadAllText(Path.Combine(tempdir, "scene.html"));
|
||||
rend.StylesTemplate = File.ReadAllText(Path.Combine(tempdir, "styles.css"));
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(images)) rend.ImageDir = images;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -184,7 +199,7 @@
|
|||
private static void ShowHelp()
|
||||
{
|
||||
Console.WriteLine(
|
||||
@"Usage: ficdown.exe
|
||||
@"Usage: ficdown
|
||||
--format (html|epub)
|
||||
--in ""/path/to/source.md""
|
||||
[--out ""/path/to/output""]
|
||||
|
@ -193,7 +208,9 @@
|
|||
[--author ""Author Name""]
|
||||
[--bookid ""ePub Book ID""]
|
||||
[--language ""language""]
|
||||
[--debug]");
|
||||
[--debug]
|
||||
or: ficdown --format lint
|
||||
(reads input from stdin)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")]
|
|
@ -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
|
||||
|
|
|
@ -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')" />
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<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>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
</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>
|
||||
<None Include="TestStories\**\*" CopyToOutputDirectory="Always" />
|
||||
</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" />
|
||||
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<None Include="TestStories\CloakOfDarkness.md" />
|
||||
<None Include="TestStories\TheRobotKing.md" />
|
||||
<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>
|
||||
<ItemGroup />
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ficdown.Parser\Ficdown.Parser.csproj">
|
||||
<Project>{780f652d-7541-4171-bb89-2d263d3961dc}</Project>
|
||||
<Name>Ficdown.Parser</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="TestStories\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
|
|
|
@ -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,10 +9,11 @@
|
|||
[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))
|
||||
{
|
||||
|
|
|
@ -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")]
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -1,109 +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')" />
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<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>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
</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" />
|
||||
<None Include="Render\**\*.html" CopyToOutputDirectory="Always" />
|
||||
<None Include="Render\**\*.css" CopyToOutputDirectory="Always" />
|
||||
</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="Model\Parser\Line.cs" />
|
||||
<Compile Include="Model\Parser\FicdownException.cs" />
|
||||
<Compile Include="Parser\ParserExtensions.cs" />
|
||||
<Compile Include="Model\Parser\Orphan.cs" />
|
||||
<Compile Include="Render\Template.cs" />
|
||||
<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>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
<Content Include="Render\Views\scene.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Render\Assets\styles.css">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Render\Views\index.html">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
namespace Ficdown.Parser
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class Logger
|
||||
{
|
||||
private static bool _initialized = false;
|
||||
private static bool _debug = false;
|
||||
private static Dictionary<Type, Logger> _cache;
|
||||
|
||||
public Type Type { get; private set; }
|
||||
|
||||
private Logger(Type type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public static void Initialize(bool debug)
|
||||
{
|
||||
_debug = debug;
|
||||
_cache = new Dictionary<Type, Logger>();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public static Logger GetLogger<T>()
|
||||
{
|
||||
var type = typeof(T);
|
||||
lock(_cache)
|
||||
{
|
||||
if(!_cache.ContainsKey(type))
|
||||
_cache.Add(type, new Logger(type));
|
||||
}
|
||||
return _cache[type];
|
||||
}
|
||||
|
||||
private string Decorate(string message)
|
||||
{
|
||||
return $"{DateTime.Now.ToString("")} <{Type.Name}> {message}";
|
||||
}
|
||||
|
||||
public void Raw(string message)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
|
||||
public void Log(string message)
|
||||
{
|
||||
Raw(Decorate(message));
|
||||
}
|
||||
|
||||
public void Debug(string message)
|
||||
{
|
||||
if(!_debug) return;
|
||||
Log($"DEBUG: {message}");
|
||||
}
|
||||
|
||||
public void Error(string message, Exception ex = null)
|
||||
{
|
||||
Console.Error.WriteLine(Decorate($"ERROR: {message}"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,5 +6,7 @@
|
|||
public string Text { get; set; }
|
||||
public Href Href { get; set; }
|
||||
public string Title { get; set; }
|
||||
public int LineNumber { get; set; }
|
||||
public int ColNumber { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,24 +6,25 @@ namespace Ficdown.Parser.Model.Parser
|
|||
{
|
||||
public string BlockName { get; private set; }
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)); }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
internal interface IBlockHandler
|
||||
{
|
||||
List<FicdownException> Warnings { set; }
|
||||
IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines);
|
||||
Story ParseBlocks(IEnumerable<Block> blocks);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
internal interface IStateResolver
|
||||
{
|
||||
List<FicdownException> Warnings { set; }
|
||||
ResolvedStory Resolve(IEnumerable<PageState> pages, Story story);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
namespace Ficdown.Parser.Parser
|
||||
{
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]
|
|
@ -89,6 +89,7 @@
|
|||
|
||||
public class EpubRenderer : HtmlRenderer
|
||||
{
|
||||
private static readonly Logger _logger = Logger.GetLogger<EpubRenderer>();
|
||||
private readonly string _author;
|
||||
private readonly string _bookId;
|
||||
private readonly string _language;
|
||||
|
@ -98,10 +99,12 @@
|
|||
_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);
|
||||
|
|
|
@ -3,15 +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
|
||||
{
|
||||
private static Logger _logger = Logger.GetLogger<HtmlRenderer>();
|
||||
private readonly string _language;
|
||||
|
||||
protected readonly Markdown Markdown;
|
||||
public List<FicdownException> Warnings { private get; set; }
|
||||
|
||||
public string IndexTemplate { get; set; }
|
||||
public string SceneTemplate { get; set; }
|
||||
|
@ -23,7 +24,6 @@
|
|||
public HtmlRenderer(string language)
|
||||
{
|
||||
_language = language;
|
||||
Markdown = new Markdown();
|
||||
}
|
||||
|
||||
public virtual void Render(ResolvedStory story, string outPath, bool debug = false)
|
||||
|
@ -40,11 +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)}
|
||||
});
|
||||
|
||||
|
@ -55,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);
|
||||
|
@ -70,7 +71,7 @@
|
|||
{
|
||||
{"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);
|
||||
|
|
|
@ -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; }
|
||||
|
|
|
@ -2,9 +2,12 @@ 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"); }
|
||||
|
@ -22,7 +25,7 @@ namespace Ficdown.Parser.Render
|
|||
|
||||
private static string GetFileContents(string fname)
|
||||
{
|
||||
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, string.Format("Render/{0}", fname));
|
||||
var path = Path.Combine(Template.BaseDir, "Render", fname);
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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}"
|
||||
|
|
|
@ -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.
|
|
@ -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 .
|
24
README.md
24
README.md
|
@ -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
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
DIR=`dirname $0`
|
||||
xbuild $DIR/../Ficdown.sln
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
DIR=`dirname $0`
|
||||
rm -rf $DIR/../*/bin $DIR/../*/obj
|
||||
xbuild $DIR/../Ficdown.sln
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
DIR=`dirname $0`
|
||||
mono $DIR/../Ficdown.Console/bin/Debug/Ficdown.Console.exe "$@"
|
|
@ -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
|
Loading…
Reference in New Issue