Compare commits

...

27 commits
v0.9.0 ... main

Author SHA1 Message Date
D. Moonfire 5a993e85cb fix(html): added validatiors to Autofac module
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 21:49:41 -06:00
D. Moonfire aac4b4373d feat(html): added identify operations
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 21:14:36 -06:00
D. Moonfire 22ddae11f8 fix(temporal): added CurrentInstant and CurrentDateTime to TimeService
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 17:48:39 -06:00
D. Moonfire b32ca7582c fix(schedules): protect against null schedules 2023-01-21 12:13:09 -06:00
D. Moonfire 07eb12414a refactor(temporal)!: renamed Timekeeper to TimeService
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-21 01:52:52 -06:00
D. Moonfire 82e1bc3c28 feat(schedules)!: reworked schedule names and added a new style
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-20 23:28:41 -06:00
D. Moonfire 070cf2bfb8 fix(scheduler): added better error messages and regular expression for numerical paths
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 23:51:37 -06:00
D. Moonfire 185980b5c4 fix(schedules): corrected serialization of pathRegex
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 22:32:54 -06:00
D. Moonfire 2e93ebdb7c ci: add manual to the release process
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 15:52:39 -06:00
D. Moonfire bca501d4e5 ci: updating to not create 1.0.0 packages
Some checks failed
ci/woodpecker/manual/woodpecker Pipeline failed
2023-01-18 15:49:51 -06:00
D. Moonfire 189273692c feat(schedules): switched how periods were parsed to allow for "2 weeks"
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-18 14:29:11 -06:00
D. Moonfire 2892ec3445 feat!: added cancellation token support to pipelines and operations
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-17 19:24:09 -06:00
D. Moonfire 08aafb144c feat(schedules): changed ApplySchedules.Schedules to a getter GetSchedules 2023-01-17 18:42:31 -06:00
D. Moonfire fc1ab22a0e build: no prettier left
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/tag/woodpecker Pipeline was successful
2023-01-16 22:13:58 -06:00
D. Moonfire d5b975c179 feat(schedules): implemented the basic schedule
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
2023-01-16 22:10:24 -06:00
D. Moonfire c73805ae93 chore: fixed a reference to Gallium 2023-01-16 22:09:06 -06:00
D. Moonfire edda9a2773 refactor(temporal)!: cleaned up organization 2023-01-16 13:45:59 -06:00
D. Moonfire e02c56e77e feat: implemented SingletonComponent to wrap most of the Is* components 2023-01-16 12:38:29 -06:00
D. Moonfire a5694d0cee refactor!: moving generator attributes into Nitride.Generators namespace 2023-01-15 14:08:58 -06:00
D. Moonfire 0a36d70fb0 feat: implemented a JSON project as a variant of YAML 2023-01-15 13:57:43 -06:00
D. Moonfire c5c9b8bf9c refactor!: renamed GetText to GetTextContentString 2023-01-15 12:43:42 -06:00
D. Moonfire a8c6d0e582 feat: made PipelineBase.AddDependency be a params 2023-01-15 12:20:41 -06:00
D. Moonfire 084aa7e812 build: removed prettier and formatting in lefthook 2023-01-15 12:20:13 -06:00
D. Moonfire 7d388b09c2 chore: lowered verbosity on some pipeline operations
closes #2
2023-01-14 18:30:22 -06:00
D. Moonfire 9e93eb6ce6 refactor!: fixed missed namespaces
- reformatted code and cleaned up references
2023-01-14 18:19:42 -06:00
D. Moonfire 6a397f5284 build: removed old git versions
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline failed
ci/woodpecker/push/woodpecker Pipeline failed
2022-12-24 00:47:57 -06:00
D. Moonfire 0011dc715b refactor: cleaning up file 2022-12-24 00:41:34 -06:00
190 changed files with 3425 additions and 778 deletions

3
.envrc
View file

@ -1,2 +1,3 @@
export PATH=$PWD/scripts:$PATH
use flake || use nix
export PATH=$PWD/scripts:$PATH
export DOTNET_ROOT=$(dirname $(dirname $(which dotnet)))

View file

@ -10,20 +10,14 @@ pipeline:
commands:
- nix develop --command scripts/build.sh
when:
event: [push, pull_request, tag]
tag: v*
event: [push, pull_request, manual]
test:
image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
commands:
- nix develop --command scripts/test.sh
when:
event: [push, pull_request]
#paths:
# - ./**/*test-result.xml
# - ./coverage/Cobertura.xml
# - ./coverage/Summary.*
# - ./**/*.nupkg
event: [push, pull_request, manual]
release-main:
image: registry.gitlab.com/dmoonfire/nix-flake-docker:latest
@ -32,5 +26,5 @@ pipeline:
secrets:
- gitea_token
when:
event: push
event: [push, manual]
branch: main

View file

@ -47,6 +47,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Temporal.Te
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Markdown.Tests", "tests\MfGames.Nitride.Markdown.Tests\MfGames.Nitride.Markdown.Tests.csproj", "{2AAE2B69-A93D-4045-B7E6-A32ED08D0D65}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Json", "src\MfGames.Nitride.Json\MfGames.Nitride.Json.csproj", "{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Json.Tests", "tests\MfGames.Nitride.Json.Tests\MfGames.Nitride.Json.Tests.csproj", "{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Temporal.Schedules", "src\MfGames.Nitride.Temporal.Schedules\MfGames.Nitride.Temporal.Schedules.csproj", "{6AC8F985-B11B-44F4-A000-DFEAFEF59754}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Nitride.Temporal.Schedules.Tests", "tests\MfGames.Nitride.Temporal.Schedules.Tests\MfGames.Nitride.Temporal.Schedules.Tests.csproj", "{CA009524-E64A-4380-874E-C9D19D868572}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -288,6 +296,54 @@ Global
{2AAE2B69-A93D-4045-B7E6-A32ED08D0D65}.Release|x64.Build.0 = Release|Any CPU
{2AAE2B69-A93D-4045-B7E6-A32ED08D0D65}.Release|x86.ActiveCfg = Release|Any CPU
{2AAE2B69-A93D-4045-B7E6-A32ED08D0D65}.Release|x86.Build.0 = Release|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Debug|x64.ActiveCfg = Debug|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Debug|x64.Build.0 = Debug|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Debug|x86.ActiveCfg = Debug|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Debug|x86.Build.0 = Debug|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Release|Any CPU.Build.0 = Release|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Release|x64.ActiveCfg = Release|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Release|x64.Build.0 = Release|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Release|x86.ActiveCfg = Release|Any CPU
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113}.Release|x86.Build.0 = Release|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Debug|x64.ActiveCfg = Debug|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Debug|x64.Build.0 = Debug|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Debug|x86.ActiveCfg = Debug|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Debug|x86.Build.0 = Debug|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|Any CPU.Build.0 = Release|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x64.ActiveCfg = Release|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x64.Build.0 = Release|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x86.ActiveCfg = Release|Any CPU
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26}.Release|x86.Build.0 = Release|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x64.ActiveCfg = Debug|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x64.Build.0 = Debug|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x86.ActiveCfg = Debug|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Debug|x86.Build.0 = Debug|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|Any CPU.Build.0 = Release|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x64.ActiveCfg = Release|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x64.Build.0 = Release|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x86.ActiveCfg = Release|Any CPU
{6AC8F985-B11B-44F4-A000-DFEAFEF59754}.Release|x86.Build.0 = Release|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x64.ActiveCfg = Debug|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x64.Build.0 = Debug|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x86.ActiveCfg = Debug|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Debug|x86.Build.0 = Debug|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Release|Any CPU.Build.0 = Release|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x64.ActiveCfg = Release|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x64.Build.0 = Release|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x86.ActiveCfg = Release|Any CPU
{CA009524-E64A-4380-874E-C9D19D868572}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D480943C-764D-4A8A-B546-642ED10586BB} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
@ -309,5 +365,9 @@ Global
{C49E07D0-CD32-4332-90FA-07494195CAC4} = {251D9C68-34EB-439D-B167-688BCC47DA17}
{0B74A4DE-4F92-44EE-8273-E5A15EAB4266} = {251D9C68-34EB-439D-B167-688BCC47DA17}
{2AAE2B69-A93D-4045-B7E6-A32ED08D0D65} = {251D9C68-34EB-439D-B167-688BCC47DA17}
{9A0D2BEE-859A-4E74-8CA7-5E0FB7C2B113} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{7CCC3A82-D5FE-4D54-9751-5E7985DE1F26} = {251D9C68-34EB-439D-B167-688BCC47DA17}
{6AC8F985-B11B-44F4-A000-DFEAFEF59754} = {570184FD-ECE4-4EC8-86E1-C1265E17D647}
{CA009524-E64A-4380-874E-C9D19D868572} = {251D9C68-34EB-439D-B167-688BCC47DA17}
EndGlobalSection
EndGlobal

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="mfgames.com" value="https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json" protocolVersion="3" />
<add key="mfgames.com" value="https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">

View file

@ -7,20 +7,20 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\..\tests\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj" />
<ProjectReference Include="..\..\src\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\..\src\MfGames.Nitride\MfGames.Nitride.csproj"/>
<ProjectReference Include="..\..\tests\MfGames.Nitride.Tests\MfGames.Nitride.Tests.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.5.0" />
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="CliWrap" Version="3.5.0"/>
<PackageReference Include="JunitXml.TestLogger" Version="3.0.114"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MfGames.IO" Version="1.2.7" />
<PackageReference Include="MfGames.IO" Version="1.2.7"/>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>

View file

@ -1,9 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride;
using MfGames.Nitride.IO.Contents;
using MfGames.Nitride.IO.Directories;
@ -45,7 +44,9 @@ public class CopyFilesPipeline : PipelineBase
}
/// <inheritdoc />
public override IAsyncEnumerable<Entity> RunAsync(IEnumerable<Entity> _)
public override IAsyncEnumerable<Entity> RunAsync(
IEnumerable<Entity> _,
CancellationToken cancellationToken = default)
{
// We don't care about the incoming entities which means we can
// ignore them and use the entities from the ReadFiles operation
@ -75,8 +76,9 @@ public class CopyFilesPipeline : PipelineBase
// read. Coming out of this, we will have one entity that fulfills:
//
// entity.Get<UPath> == "/output/a.txt"
entities = entities.Run(this.removePathPrefix)
.Run(this.addPathPrefix);
entities = entities
.Run(this.removePathPrefix, cancellationToken)
.Run(this.addPathPrefix, cancellationToken);
// Then we write out the files to the output. First we make sure we
// clear out the output. This operation performs an action when it
@ -97,8 +99,9 @@ public class CopyFilesPipeline : PipelineBase
// The third way is to use an extension on entities which lets us
// chain calls, ala Gulp's pipelines. The below code does this along
// with writing the files to the output.
entities = entities.Run(this.clearDirectory)
.Run(this.writeFiles);
entities = entities
.Run(this.clearDirectory, cancellationToken)
.Run(this.writeFiles, cancellationToken);
// If we are chaining this pipeline into another, we return the
// entities. Otherwise, we can just return an empty list. The

View file

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Autofac;
using MfGames.IO.Extensions;
using MfGames.Nitride;
using MfGames.Nitride.IO;
@ -27,7 +26,8 @@ public static class CopyFilesProgram
// system. At the moment, we set the "root" directory which will
// contains all the paths, both input and output.
DirectoryInfo rootDir =
typeof(CopyFilesProgram).GetDirectory()!.FindGitRoot()!.GetDirectory("examples/CopyFiles");
typeof(CopyFilesProgram).GetDirectory()!.FindGitRoot()!
.GetDirectory("examples/CopyFiles");
builder.WithRootDirectory(rootDir);

View file

@ -1,10 +1,11 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using CliWrap;
using MfGames.IO.Extensions;
using MfGames.Nitride.Tests;
using Xunit;
@ -26,8 +27,10 @@ public class CopyFilesTest : NitrideTestBase
public async Task Run()
{
// Figure out the paths for this test.
DirectoryInfo rootDir =
typeof(CopyFilesProgram).GetDirectory()!.FindGitRoot()!.GetDirectory("examples/CopyFiles");
DirectoryInfo rootDir = typeof(CopyFilesProgram)
.GetDirectory()
!.FindGitRoot()!
.GetDirectory("examples/CopyFiles");
DirectoryInfo outputDir = rootDir.GetDirectory("output");
FileInfo projectFile = rootDir.GetFile("CopyFiles.csproj");
@ -41,14 +44,31 @@ public class CopyFilesTest : NitrideTestBase
}
// Execute the generator. This will throw if there is an exception.
await Cli.Wrap("dotnet")
.WithArguments(
x => x.Add("run")
.Add("--project")
.Add(projectFile.FullName)
.Add("--")
.Add("build"))
.ExecuteAsync();
StringBuilder output = new();
try
{
await Cli
.Wrap("dotnet")
.WithWorkingDirectory(projectFile.DirectoryName!)
.WithArguments(
argumentsBuilder => argumentsBuilder
.Add("run")
.Add("--no-build")
.Add("--")
.Add("build"))
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(output))
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(output))
.ExecuteAsync();
}
catch (Exception exception)
{
this.Logger.Fatal(
exception,
"There was an exception running the command:\n\n{Log:l}",
output);
throw;
}
// Make sure we have our output.
FileInfo aFile = outputDir.GetFile("a.txt");

View file

@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
@ -17,11 +17,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1662019588,
"narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=",
"lastModified": 1673631141,
"narHash": "sha256-AprpYQ5JvLS4wQG/ghm2UriZ9QZXvAwh1HlgA/6ZEVQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2da64a81275b68fdad38af669afeda43d401e94b",
"rev": "befc83905c965adfd33e5cae49acb0351f6e0404",
"type": "github"
},
"original": {

View file

@ -15,7 +15,6 @@
pkgs.dotnet-sdk
pkgs.lefthook
pkgs.convco
pkgs.nodePackages.prettier
pkgs.nixfmt
pkgs.jq
];

View file

@ -1,14 +1,3 @@
pre-commit:
parallel: true
commands:
dotnet-format:
glob: "*.cs"
run: dotnet format
prettier:
run: prettier . --write --loglevel warn
nixfmt:
run: nixfmt flake.nix
commit-msg:
commands:
commit-check:

View file

@ -13,6 +13,7 @@ fi
# Clean up everything from the previous runs.
echo "$(basename $0): cleaning project"
dotnet clean
rm -f src/*/bin/Debug/*.nupkg
# Version the file based on the Git repository.
echo "$(basename $0): setting project version"
@ -35,8 +36,8 @@ dotnet pack -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg || exit 1
echo "$(basename $0): publishing NuGet package"
dotnet nuget remove source mfgames.com >& /dev/null
dotnet nuget add source --name mfgames.com --username dmoonfire --password $GITEA_TOKEN https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json --store-password-in-clear-text || exit 1
dotnet nuget push --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1
dotnet nuget add source --name mfgames.com https://src.mfgames.com/api/packages/mfgames-cil/nuget/index.json || exit 1
dotnet nuget push --api-key $GITEA_TOKEN --skip-duplicate --source mfgames.com src/*/bin/Debug/*.nupkg || exit 1
# Tag and push, but only if we don't have a tag.
if ! git tag | grep $SEMVER >& /dev/null

View file

@ -4,7 +4,7 @@
cd $(dirname $0)/..
# Make sure we have the needed executables installed.
for e in dotnet lefthook prettier nixfmt
for e in dotnet lefthook nixfmt
do
if ! which $e >& /dev/null
then

View file

@ -1,13 +0,0 @@
mode: ContinuousDelivery
increment: Inherit
continuous-delivery-fallback-tag: ci
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
assembly-versioning-scheme: MajorMinorPatch
assembly-file-versioning-scheme: MajorMinorPatch
assembly-informational-format: "{InformationalVersion}"
tag-prefix: "[vV]"

View file

@ -1,16 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
using Ical.Net.Serialization;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using MfGames.Nitride.Temporal;
using NodaTime;
@ -27,13 +28,13 @@ namespace MfGames.Nitride.Calendar;
[WithProperties]
public partial class CreateCalender : OperationBase
{
private readonly Timekeeper clock;
private readonly TimeService clock;
private readonly IValidator<CreateCalender> validator;
public CreateCalender(
IValidator<CreateCalender> validator,
Timekeeper clock)
TimeService clock)
{
this.validator = validator;
this.clock = clock;
@ -57,17 +58,21 @@ public partial class CreateCalender : OperationBase
public UPath? Path { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
SplitEntityEnumerations split = input.SplitEntity<Instant>();
IEnumerable<Entity> datedAndCalendars = this.CreateCalendarEntity(split.HasAll);
IEnumerable<Entity> datedAndCalendars =
this.CreateCalendarEntity(split.HasAll);
return datedAndCalendars.Union(split.NotHasAll);
}
private IEnumerable<Entity> CreateCalendarEntity(IEnumerable<Entity> entities)
private IEnumerable<Entity> CreateCalendarEntity(
IEnumerable<Entity> entities)
{
// Create the calendar in the same time zone as the rest of the system.
var calendar = new Ical.Net.Calendar();
@ -76,7 +81,8 @@ public partial class CreateCalender : OperationBase
// Go through the events and add all of them.
var input = entities.ToList();
IEnumerable<CalendarEvent> events = input.Select(this.CreateCalendarEvent);
IEnumerable<CalendarEvent> events =
input.Select(this.CreateCalendarEvent);
calendar.Events.AddRange(events);

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Calendar;
/// <summary>
/// A marker component for identifying an entity that represents a calendar.
/// </summary>
public record IsCalendar
[SingletonComponent]
public partial class IsCalendar
{
public static IsCalendar Instance { get; } = new();
}

View file

@ -10,16 +10,16 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj"/>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="Ical.Net" Version="4.2.0" />
<PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Ical.Net" Version="4.2.0"/>
<PackageReference Include="NodaTime" Version="3.1.2"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,6 +1,6 @@
using Autofac;
using MfGames.Nitride.Temporal;
using MfGames.Nitride.Temporal.Setup;
namespace MfGames.Nitride.Calendar;
@ -8,7 +8,8 @@ public static class NitrideCalendarBuilderExtensions
{
public static NitrideBuilder UseCalendar(this NitrideBuilder builder)
{
return builder.UseTemporal()
return builder
.UseTemporal()
.ConfigureContainer(x => x.RegisterModule<NitrideCalendarModule>());
}
}

View file

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Feeds.Structure;
using MfGames.Nitride.Generators;
using NodaTime;
@ -93,7 +94,9 @@ public partial class CreateAtomFeed : OperationBase
public Func<Entity, Uri>? GetUrl { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,13 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds;
/// <summary>
/// A marker component that indicates this page is a feed.
/// </summary>
public class IsFeed
[SingletonComponent]
public partial class IsFeed
{
public IsFeed()
{
}
public static IsFeed Instance { get; } = new();
}

View file

@ -10,15 +10,15 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj" />
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride.IO\MfGames.Nitride.IO.csproj"/>
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj"/>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="NodaTime" Version="3.1.2" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="NodaTime" Version="3.1.2"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -1,6 +1,6 @@
using Autofac;
using MfGames.Nitride.Temporal;
using MfGames.Nitride.Temporal.Setup;
namespace MfGames.Nitride.Feeds;

View file

@ -1,5 +1,7 @@
using System.Xml.Linq;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
@ -29,7 +31,10 @@ public partial class AtomAuthor
if (!string.IsNullOrEmpty(this.Name))
{
author.Add(new XElement(XmlConstants.AtomNamespace + "name", new XText(this.Name)));
author.Add(
new XElement(
XmlConstants.AtomNamespace + "name",
new XText(this.Name)));
}
return author;

View file

@ -1,6 +1,8 @@
using System;
using System.Xml.Linq;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Feeds.Structure;
/// <summary>
@ -36,7 +38,9 @@ public partial class AtomCategory
throw new NullReferenceException("Category term cannot be null.");
}
var elem = new XElement(XmlConstants.AtomNamespace + "category", new XAttribute("term", this.Term));
var elem = new XElement(
XmlConstants.AtomNamespace + "category",
new XAttribute("term", this.Term));
if (this.Scheme != null)
{

View file

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Xml.Linq;
using MfGames.Nitride.Generators;
using NodaTime;
using static MfGames.Nitride.Feeds.Structure.XmlConstants;

View file

@ -24,7 +24,10 @@ public static class AtomHelper
{
if (!string.IsNullOrWhiteSpace(text))
{
elem.Add(new XElement(XmlConstants.AtomNamespace + name, new XText(text)));
elem.Add(
new XElement(
XmlConstants.AtomNamespace + name,
new XText(text)));
}
}
}

View file

@ -10,10 +10,12 @@ public static class XmlConstants
/// <summary>
/// The XML namespace for Atom feeds.
/// </summary>
public static readonly XNamespace AtomNamespace = "http://www.w3.org/2005/Atom";
public static readonly XNamespace AtomNamespace =
"http://www.w3.org/2005/Atom";
/// <summary>
/// The XML namespace for media.
/// </summary>
public static readonly XNamespace MediaNamespace = "http://search.yahoo.com/mrss/";
public static readonly XNamespace MediaNamespace =
"http://search.yahoo.com/mrss/";
}

View file

@ -1,10 +1,12 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Gemtext;
/// <summary>
/// A marker component for indicating that an entity is Gemtext, the format
/// for text files using the Gemini protocol.
/// </summary>
public record IsGemtext
[SingletonComponent]
public partial class IsGemtext
{
public static IsGemtext Instance { get; } = new();
}

View file

@ -6,6 +6,7 @@ public static class NitrideGemtextBuilderExtensions
{
public static NitrideBuilder UseGemtext(this NitrideBuilder builder)
{
return builder.ConfigureContainer(x => x.RegisterModule<NitrideGemtextModule>());
return builder.ConfigureContainer(
x => x.RegisterModule<NitrideGemtextModule>());
}
}

View file

@ -6,9 +6,9 @@ namespace MfGames.Nitride.Generators;
/// <summary>
/// Internal class that consolidates all of the information needed to generate a
/// file.
/// class for adding With* properties.
/// </summary>
internal class WithPropertyClass
public class ClassAttributeReference
{
/// <summary>
/// Gets the syntax for the class declaration.

View file

@ -0,0 +1,66 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Base class for classes marked with an attribute.
/// </summary>
public abstract class ClassAttributeSourceGeneratorBase<TSyntaxReceiver>
: ISourceGenerator
where TSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
public void Execute(GeneratorExecutionContext context)
{
// Get the generator infrastructure will create a receiver and
// populate it we can retrieve the populated instance via the
// context.
if (context.SyntaxReceiver is not TSyntaxReceiver syntaxReceiver)
{
return;
}
// Report any messages.
foreach (string? message in syntaxReceiver.Messages)
{
context.Warning(
MessageCode.Debug,
Location.Create(
"Temporary.g.cs",
TextSpan.FromBounds(0, 0),
new LinePositionSpan(
new LinePosition(0, 0),
new LinePosition(0, 0))),
"{0}: Syntax Message: {1}",
this.GetType().Name,
message);
}
// If we didn't find anything, then there is nothing to do.
if (syntaxReceiver.ReferenceList.Count == 0)
{
return;
}
// Go through each one.
foreach (ClassAttributeReference reference in syntaxReceiver
.ReferenceList)
{
this.GenerateClassFile(context, reference);
}
}
public void Initialize(GeneratorInitializationContext context)
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(
() => this.CreateSyntaxReceiver(context));
}
protected abstract TSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context);
protected abstract void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference reference);
}

View file

@ -6,18 +6,26 @@ using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace MfGames.Nitride.Generators;
internal class WithPropertySyntaxReceiver : ISyntaxReceiver
public abstract class ClassAttributeSyntaxReceiverBase : ISyntaxReceiver
{
private readonly string attributeName;
private readonly GeneratorInitializationContext context;
public WithPropertySyntaxReceiver(GeneratorInitializationContext context)
public ClassAttributeSyntaxReceiverBase(
GeneratorInitializationContext context,
string attributeName)
{
this.context = context;
this.ClassList = new List<WithPropertyClass>();
this.attributeName = attributeName;
this.ReferenceList = new List<ClassAttributeReference>();
this.Messages = new List<string>();
}
public List<WithPropertyClass> ClassList { get; }
/// <summary>
/// Gets or sets a value indicating whether we should debug parsing attributes.
/// </summary>
public bool DebugAttributes { get; set; }
public List<string> Messages { get; }
@ -26,6 +34,8 @@ internal class WithPropertySyntaxReceiver : ISyntaxReceiver
/// </summary>
public string? Namespace { get; private set; }
public List<ClassAttributeReference> ReferenceList { get; }
public List<UsingDirectiveSyntax> UsingDirectiveList { get; set; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
@ -64,21 +74,30 @@ internal class WithPropertySyntaxReceiver : ISyntaxReceiver
}
// See if the class has our set properties attribute.
bool found = cds.AttributeLists.AsEnumerable()
var attributes = cds.AttributeLists
.AsEnumerable()
.SelectMany(x => x.Attributes)
.Select(x => x.Name.ToString())
.ToList();
bool found = attributes
.Any(
x => x switch
{
"WithProperties" => true,
"WithPropertiesAttribute" => true,
_ => false,
});
x => x == this.attributeName
|| x == $"{this.attributeName}Attribute");
if (this.DebugAttributes)
{
this.Messages.Add(
string.Format(
"Parsing {0} found? {1} from attributes [{2}]",
cds.Identifier,
found,
string.Join(", ", attributes)));
}
if (found)
{
this.ClassList.Add(
new WithPropertyClass
this.ReferenceList.Add(
new ClassAttributeReference
{
Namespace = this.Namespace!,
UsingDirectiveList = this.UsingDirectiveList,

View file

@ -18,7 +18,7 @@ public static class CodeAnalysisExtensions
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object[] parameters)
params object?[] parameters)
{
Error(context, messageCode, null, format, parameters);
}
@ -36,9 +36,14 @@ public static class CodeAnalysisExtensions
MessageCode messageCode,
Location? location,
string format,
params object[] parameters)
params object?[] parameters)
{
context.Message(messageCode, location, DiagnosticSeverity.Error, format, parameters);
context.Message(
messageCode,
location,
DiagnosticSeverity.Error,
format,
parameters);
}
/// <summary>
@ -52,7 +57,7 @@ public static class CodeAnalysisExtensions
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object[] parameters)
params object?[] parameters)
{
Information(context, messageCode, null, format, parameters);
}
@ -70,9 +75,14 @@ public static class CodeAnalysisExtensions
MessageCode messageCode,
Location? location,
string format,
params object[] parameters)
params object?[] parameters)
{
context.Message(messageCode, location, DiagnosticSeverity.Info, format, parameters);
context.Message(
messageCode,
location,
DiagnosticSeverity.Info,
format,
parameters);
}
/// <summary>
@ -86,7 +96,7 @@ public static class CodeAnalysisExtensions
this GeneratorExecutionContext context,
MessageCode messageCode,
string format,
params object[] parameters)
params object?[] parameters)
{
Warning(context, messageCode, null, format, parameters);
}
@ -104,9 +114,14 @@ public static class CodeAnalysisExtensions
MessageCode messageCode,
Location? location,
string format,
params object[] parameters)
params object?[] parameters)
{
context.Message(messageCode, location, DiagnosticSeverity.Warning, format, parameters);
context.Message(
messageCode,
location,
DiagnosticSeverity.Warning,
format,
parameters);
}
/// <summary>
@ -124,7 +139,7 @@ public static class CodeAnalysisExtensions
Location? location,
DiagnosticSeverity severity,
string format,
params object[] parameters)
params object?[] parameters)
{
context.ReportDiagnostic(
Diagnostic.Create(
@ -134,7 +149,10 @@ public static class CodeAnalysisExtensions
severity,
severity,
true,
severity is DiagnosticSeverity.Warning or DiagnosticSeverity.Info ? 4 : 0,
severity is DiagnosticSeverity.Warning
or DiagnosticSeverity.Info
? 4
: 0,
location: location));
}
}

View file

@ -10,10 +10,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.3.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.1"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,91 @@
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace MfGames.Nitride.Generators;
/// <summary>
/// Implements a source generator that creates the additional properties
/// and methods for a singleton component including the constructor and
/// instance methods, along with extension methods for adding them to entities.
/// </summary>
[Generator]
public class SingletonComponentSourceGenerator
: ClassAttributeSourceGeneratorBase<SingletonComponentSyntaxReceiver>
{
protected override SingletonComponentSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context)
{
return new SingletonComponentSyntaxReceiver(context);
}
protected override void GenerateClassFile(
GeneratorExecutionContext context,
ClassAttributeReference unit)
{
// Pull out some fields.
ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class.
StringBuilder buffer = new();
buffer.AppendLine("#nullable enable");
// Copy the using statements from the file.
foreach (UsingDirectiveSyntax? uds in unit.UsingDirectiveList)
{
buffer.AppendLine(uds.ToString());
}
buffer.AppendLine();
// Create the namespace.
SyntaxToken cls = cds.Identifier;
buffer.AppendLine(
string.Join(
"\n",
$"using MfGames.Gallium;",
$"",
$"namespace {unit.Namespace}",
$"{{",
$" public partial class {cls}",
$" {{",
$" static {cls}()",
$" {{",
$" Instance = new {cls}();",
$" }}",
$"",
$" private {cls}()",
$" {{",
$" }}",
$"",
$" public static {cls} Instance {{ get; }}",
$" }}",
$"",
$" public static class {cls}Extensions",
$" {{",
$" public static bool Has{cls}(this Entity entity)",
$" {{",
$" return entity.Has<{cls}>();",
$" }}",
$"",
$" public static Entity Remove{cls}(this Entity entity)",
$" {{",
$" return entity.Remove<{cls}>();",
$" }}",
$"",
$" public static Entity Set{cls}(this Entity entity)",
$" {{",
$" return entity.Set({cls}.Instance);",
$" }}",
$" }}",
$"}}",
""));
// Create the source text and write out the file.
var sourceText = SourceText.From(buffer.ToString(), Encoding.UTF8);
context.AddSource(cls + ".Generated.cs", sourceText);
}
}

View file

@ -0,0 +1,13 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
public class SingletonComponentSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
/// <inheritdoc />
public SingletonComponentSyntaxReceiver(
GeneratorInitializationContext context)
: base(context, "SingletonComponent")
{
}
}

View file

@ -15,58 +15,22 @@ namespace MfGames.Nitride.Generators;
/// together calls.
/// </summary>
[Generator]
public class WithPropertySourceGenerator : ISourceGenerator
public class WithPropertiesSourceGenerator
: ClassAttributeSourceGeneratorBase<WithPropertiesSyntaxReceiver>
{
public void Execute(GeneratorExecutionContext context)
/// <inheritdoc />
protected override WithPropertiesSyntaxReceiver CreateSyntaxReceiver(
GeneratorInitializationContext context)
{
// Get the generator infrastructure will create a receiver and
// populate it we can retrieve the populated instance via the
// context.
var syntaxReceiver = (WithPropertySyntaxReceiver?)context.SyntaxReceiver;
if (syntaxReceiver == null)
{
return;
}
// Report any messages.
foreach (string? message in syntaxReceiver.Messages)
{
context.Information(
MessageCode.Debug,
Location.Create(
"Temporary.g.cs",
TextSpan.FromBounds(0, 0),
new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 0))),
"Generating additional identifier code: {0}",
message);
}
// If we didn't find anything, then there is nothing to do.
if (syntaxReceiver.ClassList.Count == 0)
{
return;
}
// Go through each one.
foreach (WithPropertyClass classInfo in syntaxReceiver.ClassList)
{
this.GenerateClassFile(context, classInfo);
}
return new WithPropertiesSyntaxReceiver(context);
}
public void Initialize(GeneratorInitializationContext context)
{
// Register a factory that can create our custom syntax receiver
context.RegisterForSyntaxNotifications(() => new WithPropertySyntaxReceiver(context));
}
private void GenerateClassFile(
protected override void GenerateClassFile(
GeneratorExecutionContext context,
WithPropertyClass unit)
ClassAttributeReference unit)
{
// Pull out some fields.
ClassDeclarationSyntax? cds = unit.ClassDeclaration;
ClassDeclarationSyntax cds = unit.ClassDeclaration;
// Create the partial class.
StringBuilder buffer = new();
@ -96,7 +60,9 @@ public class WithPropertySourceGenerator : ISourceGenerator
foreach (PropertyDeclarationSyntax pds in properties)
{
// See if we have a setter.
bool found = pds.AccessorList?.Accessors.Any(x => x.Keyword.ToString() == "set") ?? false;
bool found = pds.AccessorList?.Accessors
.Any(x => x.Keyword.ToString() == "set")
?? false;
if (!found)
{
@ -132,7 +98,8 @@ public class WithPropertySourceGenerator : ISourceGenerator
pds.Type));
buffer.AppendLine(" {");
buffer.AppendLine(string.Format(" this.{0} = value;", pds.Identifier));
buffer.AppendLine(
string.Format(" this.{0} = value;", pds.Identifier));
buffer.AppendLine(" return this;");
buffer.AppendLine(" }");
}

View file

@ -0,0 +1,12 @@
using Microsoft.CodeAnalysis;
namespace MfGames.Nitride.Generators;
public class WithPropertiesSyntaxReceiver : ClassAttributeSyntaxReceiverBase
{
/// <inheritdoc />
public WithPropertiesSyntaxReceiver(GeneratorInitializationContext context)
: base(context, "WithProperties")
{
}
}

View file

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using HandlebarsDotNet;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Handlebars;
@ -46,7 +47,9 @@ public partial class ApplyStyleTemplate : OperationBase
public IHandlebars? Handlebars { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Make sure we have sane data.
this.validator.ValidateAndThrow(this);
@ -61,7 +64,8 @@ public partial class ApplyStyleTemplate : OperationBase
{
object model = this.CreateModelCallback!(entity);
string name = this.GetTemplateName!(entity);
HandlebarsTemplate<object, object> template = this.cache.GetNamedTemplate(name);
HandlebarsTemplate<object, object> template =
this.cache.GetNamedTemplate(name);
string result = template(model!);
return entity.SetTextContent(result);

View file

@ -23,12 +23,18 @@ public class ForEachHandlebarsBlock<TModel> : HandlebarsBlockBase
/// <summary>
/// Gets or sets the callback that is called when nothing is found.
/// </summary>
public Action<EncodedTextWriter, BlockHelperOptions, Context, Arguments>? NothingFound { get; set; }
public Action<EncodedTextWriter, BlockHelperOptions, Context, Arguments>?
NothingFound
{
get;
set;
}
/// <inheritdoc />
protected override string HelperName { get; }
public ForEachHandlebarsBlock<TModel> WithGetList(Func<TModel, IEnumerable<object>>? callback)
public ForEachHandlebarsBlock<TModel> WithGetList(
Func<TModel, IEnumerable<object>>? callback)
{
this.GetList = callback;
@ -36,7 +42,8 @@ public class ForEachHandlebarsBlock<TModel> : HandlebarsBlockBase
}
public ForEachHandlebarsBlock<TModel> WithNothingFoundText(
Action<EncodedTextWriter, BlockHelperOptions, Context, Arguments>? callback)
Action<EncodedTextWriter, BlockHelperOptions, Context, Arguments>?
callback)
{
this.NothingFound = callback;

View file

@ -16,13 +16,15 @@ public class HandlebarsTemplateCache
private readonly ModificationSynchronizer locker;
private readonly Dictionary<string, HandlebarsTemplate<object, object>> templates;
private readonly Dictionary<string, HandlebarsTemplate<object, object>>
templates;
public HandlebarsTemplateCache(IHandlebars handlebars)
{
this.handlebars = handlebars;
this.locker = new ModificationSynchronizer();
this.templates = new Dictionary<string, HandlebarsTemplate<object, object>>();
this.templates =
new Dictionary<string, HandlebarsTemplate<object, object>>();
}
/// <summary>
@ -39,7 +41,8 @@ public class HandlebarsTemplateCache
() => !this.templates.ContainsKey(literal),
() =>
{
HandlebarsTemplate<object, object> template = this.handlebars!.Compile(literal);
HandlebarsTemplate<object, object> template =
this.handlebars!.Compile(literal);
this.templates[literal] = template;
@ -56,7 +59,8 @@ public class HandlebarsTemplateCache
/// </summary>
/// <param name="templateName"></param>
/// <returns></returns>
public HandlebarsTemplate<object, object> GetNamedTemplate(string templateName)
public HandlebarsTemplate<object, object> GetNamedTemplate(
string templateName)
{
string template = $"{{{{> {templateName}}}}}";

View file

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Handlebars;
@ -17,7 +19,8 @@ public partial class IdentifyHandlebarsFromComponent : IOperation
{
private readonly IValidator<IdentifyHandlebarsFromComponent> validator;
public IdentifyHandlebarsFromComponent(IValidator<IdentifyHandlebarsFromComponent> validator)
public IdentifyHandlebarsFromComponent(
IValidator<IdentifyHandlebarsFromComponent> validator)
{
this.validator = validator;
}
@ -25,11 +28,15 @@ public partial class IdentifyHandlebarsFromComponent : IOperation
public Func<Entity, bool> HasHandlebarsTest { get; set; } = null!;
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.Select(
(entity) => this.HasHandlebarsTest.Invoke(entity) ? entity.Set(HasHandlebarsTemplate.Instance) : entity);
(entity) => this.HasHandlebarsTest.Invoke(entity)
? entity.Set(HasHandlebarsTemplate.Instance)
: entity);
}
}

View file

@ -2,7 +2,8 @@ using FluentValidation;
namespace MfGames.Nitride.Handlebars;
public class IdentifyHandlebarsFromComponentValidator : AbstractValidator<IdentifyHandlebarsFromComponent>
public class IdentifyHandlebarsFromComponentValidator
: AbstractValidator<IdentifyHandlebarsFromComponent>
{
public IdentifyHandlebarsFromComponentValidator()
{

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
namespace MfGames.Nitride.Handlebars;
@ -13,7 +13,9 @@ namespace MfGames.Nitride.Handlebars;
public class IdentifyHandlebarsFromContent : IOperation
{
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
return input.SelectEntity<ITextContent>(this.ScanContent);
}
@ -22,7 +24,7 @@ public class IdentifyHandlebarsFromContent : IOperation
Entity entity,
ITextContent content)
{
string text = content.GetText();
string text = content.GetTextContentString();
if (text.Contains("{{") && text.Contains("}}"))
{

View file

@ -10,15 +10,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="Handlebars.Net" Version="2.1.2" />
<PackageReference Include="NodaTime.Testing" Version="3.1.2" />
<PackageReference Include="Open.Threading" Version="2.2.1" />
<PackageReference Include="Autofac" Version="6.4.0"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Handlebars.Net" Version="2.1.2"/>
<PackageReference Include="NodaTime.Testing" Version="3.1.2"/>
<PackageReference Include="Open.Threading" Version="2.2.1"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -12,7 +12,8 @@ public static class NitrideHandlebarsBuilderExtensions
public static NitrideBuilder UseHandlebars(this NitrideBuilder builder)
{
return builder
.ConfigureContainer(x => x.RegisterModule<NitrideHandlebarsModule>());
.ConfigureContainer(
x => x.RegisterModule<NitrideHandlebarsModule>());
}
public static NitrideBuilder UseHandlebars(

View file

@ -23,8 +23,10 @@ public class NitrideHandlebarsModule : Module
builder.Register(
(context) =>
{
IHandlebars handlebars = HandlebarsDotNet.Handlebars.Create();
IEnumerable<IHandlebarsLoader> helpers = context.Resolve<IEnumerable<IHandlebarsLoader>>();
IHandlebars handlebars =
HandlebarsDotNet.Handlebars.Create();
IEnumerable<IHandlebarsLoader> helpers =
context.Resolve<IEnumerable<IHandlebarsLoader>>();
foreach (IHandlebarsLoader helper in helpers)
{

View file

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using HandlebarsDotNet;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Handlebars;
@ -39,11 +40,14 @@ public partial class RenderContentTemplate : OperationBase
public Func<Entity, object>? CreateModelCallback { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.SelectEntity<HasHandlebarsTemplate, ITextContent>(this.Apply);
return input.SelectEntity<HasHandlebarsTemplate, ITextContent>(
this.Apply);
}
private Entity Apply(
@ -51,8 +55,9 @@ public partial class RenderContentTemplate : OperationBase
HasHandlebarsTemplate _,
ITextContent content)
{
string text = content.GetText();
HandlebarsTemplate<object, object> template = this.cache.GetLiteralTemplate(text);
string text = content.GetTextContentString();
HandlebarsTemplate<object, object> template =
this.cache.GetLiteralTemplate(text);
object model = this.CreateModelCallback!(entity);
string result = template(model!);

View file

@ -2,7 +2,8 @@ using FluentValidation;
namespace MfGames.Nitride.Handlebars;
public class RenderContentTemplateValidator : AbstractValidator<RenderContentTemplate>
public class RenderContentTemplateValidator
: AbstractValidator<RenderContentTemplate>
{
public RenderContentTemplateValidator()
{

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Net;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
namespace MfGames.Nitride.Html;
@ -14,7 +14,9 @@ namespace MfGames.Nitride.Html;
public class ConvertHtmlEntitiesToUnicode : OperationBase
{
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
return input.SelectEntity<ITextContent>(this.ResolveHtmlEntities);
}
@ -23,7 +25,7 @@ public class ConvertHtmlEntitiesToUnicode : OperationBase
Entity entity,
ITextContent content)
{
string text = content.GetText();
string text = content.GetTextContentString();
string resolved = WebUtility.HtmlDecode(text);
return entity.SetTextContent(resolved);

View file

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using Zio;
namespace MfGames.Nitride.Html;
/// <summary>
/// An operation that identifies Markdown files by their common extensions
/// and converts them to text input while also adding the IsMarkdown
/// component to identify them.
/// </summary>
[WithProperties]
public partial class IdentifyHtml : IOperation
{
private readonly IValidator<IdentifyHtml> validator;
public IdentifyHtml(IValidator<IdentifyHtml> validator)
{
this.validator = validator;
}
public Func<Entity, UPath, bool> IsHtmlTest { get; set; } = null!;
/// <inheritdoc />
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.SelectEntity<UPath, ITextContent>(this.MarkTextEntities)
.SelectEntity<UPath, IBinaryContent>(this.MarkBinaryEntities);
}
private Entity MarkBinaryEntities(
Entity entity,
UPath path,
IBinaryContent binary)
{
// If we aren't a Markdown file, then there is nothing we can do about that.
if (!this.IsHtmlTest(entity, path))
{
return entity;
}
// Convert the file as a binary.
if (binary is ITextContentConvertable textConvertable)
{
entity = entity.SetTextContent(textConvertable.ToTextContent())
.SetIsHtml();
}
else
{
throw new InvalidOperationException(
"Cannot convert a binary content to a text without ITextContentConvertable.");
}
return entity;
}
private Entity MarkTextEntities(
Entity entity,
UPath path,
ITextContent _)
{
return this.IsHtmlTest(entity, path)
? entity.SetIsHtml()
: entity;
}
}

View file

@ -0,0 +1,29 @@
using FluentValidation;
using MfGames.Gallium;
using Zio;
namespace MfGames.Nitride.Html;
public class IdentifyHtmlFromPath : IdentifyHtml
{
public IdentifyHtmlFromPath(IValidator<IdentifyHtml> validator)
: base(validator)
{
this.IsHtmlTest = DefaultIsHtml;
}
private static bool DefaultIsHtml(
Entity entity,
UPath path)
{
return (path.GetExtensionWithDot() ?? string.Empty).ToLowerInvariant()
switch
{
".htm" => true,
".html" => true,
_ => false,
};
}
}

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Html;
/// <summary>
/// A marker component that indicates that the entity is an HTML file.
/// </summary>
public record IsHtml
[SingletonComponent]
public partial class IsHtml
{
public static IsHtml Instance { get; } = new();
}

View file

@ -25,7 +25,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
</ItemGroup>
</Project>

View file

@ -6,6 +6,7 @@ public static class NitrideHtmlBuilderExtensions
{
public static NitrideBuilder UseHtml(this NitrideBuilder builder)
{
return builder.ConfigureContainer(x => x.RegisterModule<NitrideHtmlModule>());
return builder.ConfigureContainer(
x => x.RegisterModule<NitrideHtmlModule>());
}
}

View file

@ -8,5 +8,6 @@ public class NitrideHtmlModule : Module
protected override void Load(ContainerBuilder builder)
{
builder.RegisterOperators(this);
builder.RegisterValidators(this);
}
}

View file

@ -0,0 +1,12 @@
using FluentValidation;
namespace MfGames.Nitride.Html.Validators;
public class IdentifyHtmlValidator : AbstractValidator<IdentifyHtml>
{
public IdentifyHtmlValidator()
{
this.RuleFor(x => x.IsHtmlTest)
.NotNull();
}
}

View file

@ -22,7 +22,9 @@ public class FileEntryTextContent : ITextContent, IBinaryContentConvertable
/// <inheritdoc />
public TextReader GetReader()
{
return new StreamReader(this.entry.Open(FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.UTF8);
return new StreamReader(
this.entry.Open(FileMode.Open, FileAccess.Read, FileShare.Read),
Encoding.UTF8);
}
/// <inheritdoc />

View file

@ -2,14 +2,15 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using DotNet.Globbing;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using Zio;
@ -43,9 +44,10 @@ public partial class ReadFiles : FileSystemOperationBase
/// minimatch pattern (as defined by DotNet.Blob).
/// </summary>
/// <returns>A populated collection of entities.</returns>
public IEnumerable<Entity> Run()
public IEnumerable<Entity> Run(
CancellationToken cancellationToken = default)
{
return this.Run(Array.Empty<Entity>());
return this.Run(Array.Empty<Entity>(), cancellationToken);
}
/// <summary>
@ -53,13 +55,16 @@ public partial class ReadFiles : FileSystemOperationBase
/// minimatch pattern (as defined by DotNet.Blob).
/// </summary>
/// <returns>A populated collection of entities.</returns>
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
var glob = Glob.Parse(this.Pattern);
IEnumerable<FileEntry> files = this.FileSystem.EnumerateFileEntries("/", "*", SearchOption.AllDirectories)
IEnumerable<FileEntry> files = this.FileSystem
.EnumerateFileEntries("/", "*", SearchOption.AllDirectories)
.Where(x => glob.IsMatch(x.Path.ToString()));
IEnumerable<Entity> entities = files.Select(this.ToEntity);

View file

@ -3,12 +3,13 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using Serilog;
@ -48,7 +49,8 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
public Dictionary<Type, Func<IContent, Stream>> StreamFactories
{
get => this.factories;
set => this.factories = value ?? throw new ArgumentNullException(nameof(value));
set => this.factories =
value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
@ -62,8 +64,11 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
/// a path and a registered writer will be written.
/// </summary>
/// <param name="entities">The entities to parse.</param>
/// <param name="cancellationToken"></param>
/// <returns>The same list of entities without changes.</returns>
public override IEnumerable<Entity> Run(IEnumerable<Entity> entities)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> entities,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
@ -102,7 +107,9 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
.ReadToEnd();
var stream = new MemoryStream();
var writer = new StreamWriter(stream, this.TextEncoding ?? Encoding.UTF8);
var writer = new StreamWriter(
stream,
this.TextEncoding ?? Encoding.UTF8);
writer.Write(text);
writer.Flush();
@ -133,7 +140,9 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
// First see if we have a factory for the exact type of content.
IContent content = entity.GetContent();
if (this.factories.TryGetValue(content.GetType(), out Func<IContent, Stream>? getStream))
if (this.factories.TryGetValue(
content.GetType(),
out Func<IContent, Stream>? getStream))
{
Stream stream = getStream(content);
@ -143,7 +152,9 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
// If we have an easy conversion, then use that so we don't have to
// walk up the tree looking for one we do have.
if (content is IBinaryContentConvertable binaryConvertable
&& this.factories.TryGetValue(typeof(IBinaryContent), out Func<IContent, Stream>? binaryContent))
&& this.factories.TryGetValue(
typeof(IBinaryContent),
out Func<IContent, Stream>? binaryContent))
{
Stream stream = binaryContent(binaryConvertable.ToBinaryContent());
@ -151,7 +162,9 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
}
if (content is ITextContentConvertable textConvertable
&& this.factories.TryGetValue(typeof(ITextContent), out Func<IContent, Stream>? textContent))
&& this.factories.TryGetValue(
typeof(ITextContent),
out Func<IContent, Stream>? textContent))
{
Stream stream = textContent(textConvertable.ToTextContent());
@ -167,7 +180,12 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
{
// Check to see if we have any of these types.
Func<IContent, Stream>? found = types
.Select(x => this.factories.TryGetValue(x, out Func<IContent, Stream>? factory) ? factory : null)
.Select(
x => this.factories.TryGetValue(
x,
out Func<IContent, Stream>? factory)
? factory
: null)
.FirstOrDefault(x => x != null);
if (found != null)
@ -179,7 +197,8 @@ public partial class WriteFiles : FileSystemOperationBase, IOperation
// We didn't find one, so add all the parent types and try
// again with the new list.
types = types.SelectMany(x => new[] { x.BaseType }.Union(x.GetInterfaces()))
types = types
.SelectMany(x => new[] { x.BaseType }.Union(x.GetInterfaces()))
.Where(x => x != null)
.Select(x => x!)
.ToList();

View file

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Serilog;
@ -39,13 +41,16 @@ public partial class ClearDirectory : FileSystemOperationBase, IOperation
/// </summary>
public UPath? Path { get; set; }
public IEnumerable<Entity> Run()
public IEnumerable<Entity> Run(
CancellationToken cancellationToken = default)
{
return this.Run(new List<Entity>());
return this.Run(new List<Entity>(), cancellationToken);
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
@ -71,7 +76,8 @@ public partial class ClearDirectory : FileSystemOperationBase, IOperation
// Clear out the contents.
IEnumerable<UPath> files = this.FileSystem.EnumerateFiles(path);
IEnumerable<UPath> directories = this.FileSystem.EnumerateDirectories(path);
IEnumerable<UPath> directories =
this.FileSystem.EnumerateDirectories(path);
foreach (UPath file in files)
{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Threading;
using MfGames.Gallium;
@ -16,5 +17,7 @@ public abstract class FileSystemOperationBase : IOperation
public IFileSystem FileSystem { get; set; }
/// <inheritdoc />
public abstract IEnumerable<Entity> Run(IEnumerable<Entity> input);
public abstract IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default);
}

View file

@ -8,17 +8,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="FluentValidation" Version="11.2.1" />
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="MAB.DotIgnore" Version="3.0.2" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Zio" Version="0.15.0" />
<PackageReference Include="Autofac" Version="6.4.0"/>
<PackageReference Include="DotNet.Glob" Version="3.1.3"/>
<PackageReference Include="FluentValidation" Version="11.2.1"/>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="MAB.DotIgnore" Version="3.0.2"/>
<PackageReference Include="Serilog" Version="2.11.0"/>
<PackageReference Include="Zio" Version="0.15.0"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<!-- Include the source generator -->

View file

@ -6,6 +6,7 @@ public static class NitrideIOBuilderExtensions
{
public static NitrideBuilder UseIO(this NitrideBuilder builder)
{
return builder.ConfigureContainer(x => x.RegisterModule<NitrideIOModule>());
return builder.ConfigureContainer(
x => x.RegisterModule<NitrideIOModule>());
}
}

View file

@ -1,9 +1,9 @@
using System.Collections.Generic;
using MfGames.Gallium;
using MAB.DotIgnore;
using MfGames.Gallium;
using Zio;
namespace MfGames.Nitride.IO;

View file

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
@ -28,11 +30,14 @@ public partial class AddPathPrefix : OperationBase
/// </summary>
public UPath? PathPrefix { get; set; }
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return this.replacePath.WithReplacement(this.RunReplacement)
return this.replacePath
.WithReplacement(this.RunReplacement)
.Run(input);
}

View file

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
@ -31,7 +33,9 @@ public partial class ChangePathExtension : IOperation
/// </summary>
public string? Extension { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -2,7 +2,8 @@ using FluentValidation;
namespace MfGames.Nitride.IO.Paths;
public class ChangePathExtensionValidator : AbstractValidator<ChangePathExtension>
public class ChangePathExtensionValidator
: AbstractValidator<ChangePathExtension>
{
public ChangePathExtensionValidator()
{

View file

@ -3,8 +3,8 @@ using System.Collections.Generic;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Entities;
using MfGames.Nitride.Generators;
using Zio;
@ -41,6 +41,8 @@ public partial class DirectChildPathScanner : EntityScanner
// If we are using directory indexes, skip when we have an index root.
// Otherwise, get the parent and use that as the key.
return path.GetDirectoryIndexPath() == "/" ? null : new[] { path.GetParentDirectoryIndexPath() };
return path.GetDirectoryIndexPath() == "/"
? null
: new[] { path.GetParentDirectoryIndexPath() };
}
}

View file

@ -1,11 +1,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Entities;
using MfGames.Nitride.Generators;
using Serilog;
@ -29,7 +30,9 @@ public partial class LinkDirectChildren : CreateOrUpdateIndex
}
/// <inheritdoc />
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
if (this.Scanner != null!)
{

View file

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
@ -52,7 +54,9 @@ public partial class MoveToIndexPath : OperationBase
};
}
public override IEnumerable<Entity> Run(IEnumerable<Entity> input)
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,8 +1,10 @@
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
@ -31,7 +33,9 @@ public partial class RemovePathPrefix : IOperation
/// </summary>
public UPath PathPrefix { get; set; }
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
@ -35,8 +37,11 @@ public partial class ReplacePath : IOperation
/// will be updated, the others will be passed on as-is.
/// </summary>
/// <param name="input">The list of input entities.</param>
/// <param name="cancellationToken"></param>
/// <returns>The output entities.</returns>
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);

View file

@ -4,9 +4,9 @@ This assembly contains the primary system for reading and writing from the disk,
along with various processes to manipulate paths. It contains three primary
components:
- File System I/O
- Path Normalization
- Disk-Based Content
- File System I/O
- Path Normalization
- Disk-Based Content
## File System I/O

View file

@ -0,0 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Json;
/// <summary>
/// A marker class that indicates that the entity is JSON.
/// </summary>
[SingletonComponent]
public partial class IsJson
{
}

View file

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to parse JSON content.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.4.0"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2"/>
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Generators\MfGames.Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,12 @@
using Autofac;
namespace MfGames.Nitride.Json;
public static class NitrideJsonBuilderExtensions
{
public static NitrideBuilder UseJson(this NitrideBuilder builder)
{
return builder.ConfigureContainer(
x => x.RegisterModule<NitrideJsonModule>());
}
}

View file

@ -0,0 +1,77 @@
using System;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using Newtonsoft.Json;
namespace MfGames.Nitride.Json;
public static class NitrideJsonEntityExtensions
{
/// <summary>
/// Parses the entity text as a JSON file and returns the results.
/// </summary>
public static TType? GetTextContentJson<TType>(
this Entity entity,
Action<JsonSerializerSettings> configure)
{
JsonSerializerSettings settings = new();
configure.Invoke(settings);
return entity.GetTextContentJson<TType>(settings);
}
/// <summary>
/// Parses the entity text as a JSON file and returns the results.
/// </summary>
public static TType? GetTextContentJson<TType>(
this Entity entity,
JsonSerializerSettings? settings = null)
{
string? text = entity.GetTextContentString();
return text != null
? JsonConvert.DeserializeObject<TType>(text, settings)
: default;
}
/// <summary>
/// Sets the text content to the serialized value. If this is null, then
/// the text content is removed. This uses the default serializer which
/// may be configured.
/// </summary>
/// <returns>The same entity for chaining methods.</returns>
public static Entity SetTextContentJson<TType>(
this Entity entity,
TType? value,
Action<JsonSerializerSettings> configure)
{
JsonSerializerSettings settings = new();
configure.Invoke(settings);
return SetTextContentJson(entity, value, settings);
}
/// <summary>
/// Sets the text content to the serialized value using the serializer
/// provided. If the value is null, then the text content is removed.
/// </summary>
/// <returns>The same entity for chaining methods.</returns>
public static Entity SetTextContentJson<TType>(
this Entity entity,
TType? value,
JsonSerializerSettings? settings = null)
{
if (value == null)
{
return entity.Remove<ITextContent>();
}
string json = JsonConvert.SerializeObject(value, settings);
return entity.SetTextContent(json);
}
}

View file

@ -0,0 +1,13 @@
using Autofac;
namespace MfGames.Nitride.Json;
public class NitrideJsonModule : Module
{
/// <inheritdoc />
protected override void Load(ContainerBuilder builder)
{
builder.RegisterOperators(this);
builder.RegisterValidators(this);
}
}

View file

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using Markdig;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Markdown;
@ -28,7 +29,9 @@ public abstract partial class ConvertMarkdownToBase : IOperation
public Action<MarkdownPipelineBuilder>? ConfigureMarkdown { get; set; }
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Validate the inputs.
this.validator.ValidateAndThrow(this);

View file

@ -2,12 +2,10 @@ using System;
using FluentValidation;
using MfGames.Gallium;
using Markdig;
using MfGames.Gallium;
using MfGames.Markdown.Gemtext;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Gemtext;
@ -26,7 +24,8 @@ public class ConvertMarkdownToGemtext : ConvertMarkdownToBase
}
/// <inheritdoc />
public override ConvertMarkdownToGemtext WithConfigureMarkdown(Action<MarkdownPipelineBuilder>? value)
public override ConvertMarkdownToGemtext WithConfigureMarkdown(
Action<MarkdownPipelineBuilder>? value)
{
base.WithConfigureMarkdown(value);
@ -45,7 +44,7 @@ public class ConvertMarkdownToGemtext : ConvertMarkdownToBase
ITextContent markdownContent,
MarkdownPipeline options)
{
string markdown = markdownContent.GetText();
string markdown = markdownContent.GetTextContentString();
string gemtext = MarkdownGemtext.ToGemtext(markdown, options);
var content = new StringTextContent(gemtext);

View file

@ -2,10 +2,9 @@ using System;
using FluentValidation;
using MfGames.Gallium;
using Markdig;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Html;
@ -23,7 +22,8 @@ public class ConvertMarkdownToHtml : ConvertMarkdownToBase
}
/// <inheritdoc />
public override ConvertMarkdownToHtml WithConfigureMarkdown(Action<MarkdownPipelineBuilder>? value)
public override ConvertMarkdownToHtml WithConfigureMarkdown(
Action<MarkdownPipelineBuilder>? value)
{
base.WithConfigureMarkdown(value);
@ -43,7 +43,7 @@ public class ConvertMarkdownToHtml : ConvertMarkdownToBase
MarkdownPipeline options)
{
// Convert the entity to Html.
string markdown = markdownContent.GetText();
string markdown = markdownContent.GetTextContentString();
string html = Markdig.Markdown.ToHtml(markdown, options);
var htmlContent = new StringTextContent(html);

View file

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using MfGames.Nitride.Generators;
using Zio;
@ -29,7 +30,9 @@ public partial class IdentifyMarkdown : IOperation
public Func<Entity, UPath, bool> IsMarkdownTest { get; set; } = null!;
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
@ -68,16 +71,8 @@ public partial class IdentifyMarkdown : IOperation
UPath path,
ITextContent _)
{
// If we aren't a Markdown file, then there is nothing
// we can do about that.
if (!this.IsMarkdownTest(entity, path))
{
return entity;
}
// We are already text, so just mark it as Markdown.
entity = entity.Set(IsMarkdown.Instance);
return entity;
return this.IsMarkdownTest(entity, path)
? entity.SetIsMarkdown()
: entity;
}
}

View file

@ -18,11 +18,12 @@ public class IdentifyMarkdownFromPath : IdentifyMarkdown
Entity entity,
UPath path)
{
return (path.GetExtensionWithDot() ?? string.Empty).ToLowerInvariant() switch
{
".md" => true,
".markdown" => true,
_ => false,
};
return (path.GetExtensionWithDot() ?? string.Empty).ToLowerInvariant()
switch
{
".md" => true,
".markdown" => true,
_ => false,
};
}
}

View file

@ -1,9 +1,11 @@
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Markdown;
/// <summary>
/// A marker class that indicates that the file is a Markdown file.
/// </summary>
public record IsMarkdown
[SingletonComponent]
public partial class IsMarkdown
{
public static IsMarkdown Instance { get; } = new();
}

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
@ -23,7 +24,9 @@ public class MakeSingleLinkListItems : IOperation
}
/// <inheritdoc />
public IEnumerable<Entity> Run(IEnumerable<Entity> input)
public IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
return input
.SelectManyEntity<IsMarkdown>(
@ -38,7 +41,7 @@ public class MakeSingleLinkListItems : IOperation
/// </summary>
private Entity MakeSingleLinkLists(Entity entity)
{
string content = entity.GetText()!;
string content = entity.GetTextContentString()!;
string output = Regex.Replace(
content,
@ -85,9 +88,6 @@ public class MakeSingleLinkListItems : IOperation
match =>
{
string link1 = match.Groups["label"].ToString();
string path1 = string.Format(
"/{0}/",
this.slugs.ToSlug(link1.Split('|').First()));
string label1 = link1.Split('|').Last();
return label1;

View file

@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="MfGames.Gallium" Version="0.3.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Markdig" Version="0.30.3" />
<PackageReference Include="MfGames.Markdown.Gemtext" Version="1.2.2" />
<PackageReference Include="Zio" Version="0.15.0" />

View file

@ -6,6 +6,7 @@ public static class NitrideMarkdownBuilderExtensions
{
public static NitrideBuilder UseMarkdown(this NitrideBuilder builder)
{
return builder.ConfigureContainer(x => x.RegisterModule<NitrideMarkdownModule>());
return builder.ConfigureContainer(
x => x.RegisterModule<NitrideMarkdownModule>());
}
}

View file

@ -1,8 +1,9 @@
using FluentValidation;
namespace MfGames.Nitride.Markdown;
namespace MfGames.Nitride.Markdown.Validators;
public class ConvertMarkdownToBaseValidator : AbstractValidator<ConvertMarkdownToBase>
public class ConvertMarkdownToBaseValidator
: AbstractValidator<ConvertMarkdownToBase>
{
public ConvertMarkdownToBaseValidator()
{

View file

@ -1,6 +1,6 @@
using FluentValidation;
namespace MfGames.Nitride.Markdown;
namespace MfGames.Nitride.Markdown.Validators;
public class IdentifyMarkdownValidator : AbstractValidator<IdentifyMarkdown>
{

View file

@ -21,7 +21,8 @@ public class SimpleSlugConverter : ISlugConverter, IEnumerable<string>
public SimpleSlugConverter(
IDictionary<string, string> replacements,
StringComparison comparison = StringComparison.InvariantCultureIgnoreCase)
StringComparison comparison =
StringComparison.InvariantCultureIgnoreCase)
: this()
{
foreach (KeyValuePair<string, string> pair in replacements)
@ -44,7 +45,8 @@ public class SimpleSlugConverter : ISlugConverter, IEnumerable<string>
public void Add(
string search,
string replace,
StringComparison comparison = StringComparison.InvariantCultureIgnoreCase)
StringComparison comparison =
StringComparison.InvariantCultureIgnoreCase)
{
this.replacements.Add((search, replace, comparison));
}
@ -62,12 +64,15 @@ public class SimpleSlugConverter : ISlugConverter, IEnumerable<string>
// If we have null or whitespace, we have a problem.
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Cannot have a blank or null input", nameof(input));
throw new ArgumentException(
"Cannot have a blank or null input",
nameof(input));
}
// We need to do the replacements before we slugify.
// Perform any additional replacements.
foreach ((string search, string replace, StringComparison comparison) in this.replacements)
foreach ((string search, string replace, StringComparison comparison) in
this.replacements)
{
input = input.Replace(search, replace, comparison);
}

View file

@ -18,7 +18,8 @@ public class UnicodeNormalizingSlugConverter : SimpleSlugConverter
}
/// <inheritdoc />
public UnicodeNormalizingSlugConverter(IDictionary<string, string> replacements)
public UnicodeNormalizingSlugConverter(
IDictionary<string, string> replacements)
: base(replacements)
{
}
@ -29,7 +30,9 @@ public class UnicodeNormalizingSlugConverter : SimpleSlugConverter
// If we have null or whitespace, we have a problem.
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentException("Cannot have a blank or null input", nameof(input));
throw new ArgumentException(
"Cannot have a blank or null input",
nameof(input));
}
// Normalize the Unicode objects.
@ -38,7 +41,8 @@ public class UnicodeNormalizingSlugConverter : SimpleSlugConverter
.Where(this.IsNonSpacingMark)
.ToArray();
string normalized = new string(chars).Normalize(NormalizationForm.FormC);
string normalized =
new string(chars).Normalize(NormalizationForm.FormC);
// Return the base implementation.
return base.ToSlug(normalized);

View file

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentValidation;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// Applies schedules against the list of entities.
/// </summary>
[WithProperties]
public partial class ApplySchedules : OperationBase
{
private readonly IValidator<ApplySchedules> validator;
public ApplySchedules(
IValidator<ApplySchedules> validator,
TimeService timeService)
{
this.TimeService = timeService;
this.validator = validator;
}
/// <summary>
/// Gets or sets the callback to get the schedules for the entity. This is
/// used to allow for per-entity schedules or generic schedules across
/// the entire system. If this returns null, then no schedule will be
/// applied.
/// </summary>
public Func<Entity, IList<ISchedule>?>? GetSchedules { get; set; }
/// <summary>
/// Gets or sets the time service associated with this operation.
/// </summary>
public TimeService TimeService { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
this.validator.ValidateAndThrow(this);
return input.Select(this.Apply);
}
/// <summary>
/// Adds a single schedule into the apply schedules and returns the ApplySchedule
/// class.
/// </summary>
public ApplySchedules WithGetSchedules(Func<Entity, ISchedule> schedule)
{
return this.WithGetSchedules(entity => new[] { schedule(entity) });
}
public ApplySchedules WithGetSchedules<TType>(
Func<Entity, IEnumerable<TType>?> value)
where TType : ISchedule
{
this.GetSchedules = entity => value?
.Invoke(entity)
?.Cast<ISchedule>()
.ToList();
return this;
}
private Entity Apply(Entity entity)
{
// Get the schedule for this entity.
IList<ISchedule>? schedules = this.GetSchedules?.Invoke(entity);
if (schedules == null || schedules.Count == 0)
{
return entity;
}
// Otherwise, apply the schedules to this entity.
foreach (ISchedule schedule in schedules)
{
if (schedule == null! || !schedule.CanApply(entity))
{
continue;
}
entity = schedule.Apply(entity, this.TimeService);
}
return entity;
}
}

View file

@ -0,0 +1,30 @@
using MfGames.Gallium;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A schedule is a specific schedule that can be applied to an entity to make
/// changes based on components.
/// </summary>
public interface ISchedule
{
/// <summary>
/// Applies the schedule changes to the entity and returns the results.
/// </summary>
/// <param name="entity">The entity to update.</param>
/// <param name="timeService">The service to use for time.</param>
/// <returns>
/// The modified entity, if the changes can be applied, otherwise the same
/// entity.
/// </returns>
Entity Apply(
Entity entity,
TimeService timeService);
/// <summary>
/// Determines if a schedule applies to a given entity.
/// </summary>
/// <param name="entity">The entity to test.</param>
/// <returns>True if the schedule applies to the given entity, otherwise false.</returns>
bool CanApply(Entity entity);
}

View file

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using MfGames.Gallium;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// An indexed-based schedule that uses the regular expression to get a
/// numerical value and then uses that to determine the schedule.
/// </summary>
public partial class IndexedPathRegexSchedule<TSchedule> : PathRegexScheduleBase
where TSchedule : IndexedSchedule
{
public IndexedPathRegexSchedule()
{
this.Indexes = new Dictionary<int, TSchedule>();
}
/// <summary>
/// Gets or sets the dictionary that indexes the various numerical indexes
/// to provide the scheduling.
/// </summary>
public Dictionary<int, TSchedule> Indexes { get; set; }
public IndexedPathRegexSchedule<TSchedule> WithIndexes(
Dictionary<int, TSchedule> value)
{
this.Indexes = value;
return this;
}
/// <inheritdoc />
protected override Entity Apply(
Entity entity,
int number,
TimeService timeService)
{
// Figure out the entry in the index.
var applicableKeys = this.Indexes.Keys
.Where(a => a <= number)
.ToList();
if (applicableKeys.Count == 0)
{
return entity;
}
int startIndex = applicableKeys.Max(a => a);
TSchedule schedule = this.Indexes[startIndex];
// Pass everything into the schedule to perform the applying.
int startOffset = number - startIndex;
return schedule.Apply(entity, startOffset, timeService);
}
}

View file

@ -0,0 +1,79 @@
using System;
using MfGames.Gallium;
using NodaTime;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// Describes a simplified schedule object that contains a start and period
/// for the schedule and is designed to work with the numbers provided by
/// the <see cref="IndexedPathRegexSchedule" />.
/// </summary>
public partial class IndexedSchedule
{
/// <summary>
/// Gets or sets the period between each matching entity. The period is
/// amount of time between each item based on the number. So the first will
/// be on the ScheduleDate, the second SchedulePeriod later, the third
/// SchedulePeriod after the second, etc. More precisely, the date for
/// any item TimeSpan * ((int)CaptureGroup + (int)CaptureOffset).
/// </summary>
public string? SchedulePeriod { get; set; }
/// <summary>
/// Gets or sets the schedule period as a TimeSpan object. This is converted
/// from SchedulePeriod using https://github.com/pengowray/TimeSpanParser.
/// If the value is null or blank or "immediate", then this will be instant
/// (TimeSpan.Zero).
/// </summary>
public virtual TimeSpan? SchedulePeriodTimeSpan
{
get => TimeSpanHelper.Parse(this.SchedulePeriod);
set => this.SchedulePeriod = value?.ToString();
}
/// <summary>
/// Gets or sets when the first item is scheduled.
/// </summary>
public DateTime? ScheduleStart { get; set; }
public virtual Entity Apply(
Entity entity,
int number,
TimeService timeService)
{
// If we have a "never", then we skip it.
TimeSpan? period = this.SchedulePeriodTimeSpan;
DateTime? start = this.ScheduleStart;
if (period == null || start == null)
{
return entity;
}
// Figure out the time from the start.
DateTime when = start.Value + period.Value * number;
Instant instant = timeService.CreateInstant(when);
// If the time hasn't past, then we don't apply it.
Instant now = timeService.Clock.GetCurrentInstant();
return instant > now
? entity
: this.Apply(entity, number, instant);
}
/// <summary>
/// Applies the schedule to the entity based on the number and instant
/// given.
/// </summary>
protected virtual Entity Apply(
Entity entity,
int number,
Instant instant)
{
return entity.Set(instant);
}
}

View file

@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Description>An extension to Nitride static site generator to add a scheduling system for posts.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="6.4.0" />
<PackageReference Include="MfGames.Gallium" Version="0.4.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="TimeSpanParserUtil" Version="1.2.0" />
<PackageReference Include="YamlDotNet" Version="12.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Temporal\MfGames.Nitride.Temporal.csproj" />
<ProjectReference Include="..\MfGames.Nitride\MfGames.Nitride.csproj" />
</ItemGroup>
<!-- Include the source generator -->
<PropertyGroup>
<EmitCompilerGeneratedFiles>True</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MfGames.Nitride.Generators\MfGames.Nitride.Generators.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,118 @@
using System;
using System.Text.RegularExpressions;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using Zio;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A common base for a schedule that is built off the path associated with
/// the entity.
/// </summary>
[WithProperties]
public abstract partial class PathRegexScheduleBase : ISchedule
{
protected PathRegexScheduleBase()
{
this.PathRegex = @"^.*?(\d+)[^\d]*$";
this.GetPath = entity => entity.Get<UPath>().ToString();
this.CaptureGroup = 1;
}
/// <summary>
/// Gets or sets the group number of the capture group with 1 being being the
/// first group in PathRegex.
/// </summary>
public int CaptureGroup { get; set; }
/// <summary>
/// Gets or sets the offset to make the resulting capture group a zero-based
/// number.
/// </summary>
public int CaptureOffset { get; set; }
/// <summary>
/// Gets or sets the method for retrieving the path for the entity.
/// </summary>
public Func<Entity, string> GetPath { get; set; }
/// <summary>
/// Gets or sets the regular expression to identify a matching path.
/// </summary>
public string? PathRegex { get; set; }
/// <inheritdoc />
public virtual Entity Apply(
Entity entity,
TimeService timeService)
{
// Get the path and match it.
string path = this.GetPath(entity);
Regex? regex = this.GetRegex();
Match match = regex?.Match(path)
?? throw new NullReferenceException(
"PathRegex was not configured for the "
+ this.GetType().Name
+ ".");
if (!match.Success)
{
return entity;
}
if (match.Groups.Count < 2)
{
throw new InvalidOperationException(
"There must be at least one capture group in '"
+ this.PathRegex
+ "'.");
}
// Figure out the index/number of this entry.
string numberValue = match.Groups[this.CaptureGroup].Value;
if (!int.TryParse(numberValue, out int number))
{
throw new FormatException(
path + ": Cannot parse '" + numberValue + "' as integer.");
}
number += this.CaptureOffset;
// Pass it onto the extending class.
return this.Apply(entity, number, timeService);
}
/// <inheritdoc />
public virtual bool CanApply(Entity entity)
{
string path = this.GetPath(entity);
Regex? regex = this.GetRegex();
Match match = regex?.Match(path)
?? throw new NullReferenceException(
"PathRegex was not configured for the "
+ this.GetType().Name
+ ".");
return match.Success;
}
/// <summary>
/// Applies the schedule according to the normalized number (after
/// CaptureOffset).
/// </summary>
protected abstract Entity Apply(
Entity entity,
int number,
TimeService timeService);
private Regex? GetRegex()
{
return this.PathRegex == null
? null
: new Regex(this.PathRegex);
}
}

View file

@ -0,0 +1,86 @@
using System;
using MfGames.Gallium;
using MfGames.Nitride.Generators;
using NodaTime;
namespace MfGames.Nitride.Temporal.Schedules;
/// <summary>
/// A schedule that uses the `UPath` of the entity to determine an offset from
/// a starting point and calculates the information from there.
/// </summary>
[WithProperties]
public partial class PeriodicPathRegexSchedule : PathRegexScheduleBase
{
public PeriodicPathRegexSchedule()
{
this.CaptureOffset = -1;
}
/// <summary>
/// Gets or sets the period between each matching entity. The period is
/// amount of time between each item based on the number. So the first will
/// be on the ScheduleDate, the second SchedulePeriod later, the third
/// SchedulePeriod after the second, etc. More precisely, the date for
/// any item TimeSpan * ((int)CaptureGroup + (int)CaptureOffset).
/// </summary>
public string? SchedulePeriod { get; set; }
/// <summary>
/// Gets or sets the schedule period as a TimeSpan object. This is converted
/// from SchedulePeriod using https://github.com/pengowray/TimeSpanParser.
/// If the value is null or blank or "immediate", then this will be instant
/// (TimeSpan.Zero).
/// </summary>
public virtual TimeSpan? SchedulePeriodTimeSpan
{
get => TimeSpanHelper.Parse(this.SchedulePeriod);
set => this.SchedulePeriod = value?.ToString();
}
/// <summary>
/// Gets or sets when the first item is scheduled.
/// </summary>
public DateTime? ScheduleStart { get; set; }
/// <inheritdoc />
protected override Entity Apply(
Entity entity,
int number,
TimeService timeService)
{
// If we have a "never", then we skip it.
TimeSpan? period = this.SchedulePeriodTimeSpan;
DateTime? start = this.ScheduleStart;
if (period == null || start == null)
{
return entity;
}
// Figure out the time from the start.
DateTime when = start.Value + period.Value * number;
Instant instant = timeService.CreateInstant(when);
// If the time hasn't past, then we don't apply it.
Instant now = timeService.Clock.GetCurrentInstant();
return instant > now
? entity
: this.Apply(entity, number, instant);
}
/// <summary>
/// Applies the schedule to the entity based on the number and instant
/// given.
/// </summary>
protected virtual Entity Apply(
Entity entity,
int number,
Instant instant)
{
return entity.Set(instant);
}
}

View file

@ -0,0 +1,235 @@
# Schedules
Scheduling posts and entries is fairly common process with blob posts but the
methods for creating those schedules can vary drastically because it requires
site- or project-specific elements such as which components or models to change
and how. This package provides one approach to scheduling that "applies"
changes (as described in JSON or YAML) to a given object based on the current
date (as provided by the `MfGames.Nitride.Temporal` package).
A schedule is not needed when the date is in the path or inside a component. Using
`MfGames.Nitride.Temporal.SetFromComponent` or `MfGames.Nitride.Temporal.SetFromPath`
would be more than sufficient. This is for dynamically changing entities based on
the date based on `TimeService`.
## Configuring
To use the modules, either the `NitrideTemporalSchedulesModule` can be added or
the builder extension method can be used.
```csharp
NitrideBuilder builder;
builder.UseTemporalSchedules();
```
## ISchedule
A schedule is a class that implements `ISchedule` which has the following methods:
- CanApply(Entity) ⟶ bool
- This returns true if the schedule can apply to the given entity.
- Apply(Entity) ⟶ Entity
- This makes the changes for the schedule on the entity and returns the results.
- If the entity doesn't apply, then it should return the entity without changes.
## ApplySchedules
The primary operation is `ApplySchedules` which take a sequence of schedule objects
and applies each one in turn against every entity given to the operation.
```csharp
IEnumerable<Entity> entities;
IList<ISchedule> schedules;
ApplySchedules op;
return op
.WithSchedules(schedules)
.Run(entities);
```
The following properties are available (along with their corresponding `With*` methods):
- `IList<ISchedule> Schedules`
- Contains the list of schedules to apply against entities.
- Defaults to an empty list.
- `TimeService TimeService`
- Used to determine when the site is being generated.
- Defaults to the one provided by `MfGames.Nitride.Temporal`.
## Provided Schedules
A number of schedules are provided as part of this library.
### Schedule Periods
In all cases, the `SchedulePeriod` is a string that parses into a `TimeSpan` object
that determines the amount of time between two successive entities of the same
schedule. It is parsed using [TimeSpanParser](https://github.com/pengowray/TimeSpanParser)
and allow for `TimeSpan` formatting (such as "1.00:00:00"), descriptions such as
"4 days" or "1 month". In addition, it also handles `instant` (non-case sensitive)
for a zero length time or effectively all at once or `never` (also not case sensitive)
to never apply to it.
### Periodic Path Regex Schedules
A common pattern for using schedules is to dole out a numerical series of posts over
a period of time. For example, a weekly posting of chapters or comic strips. In these
cases, there is usually a number in the path such as `chapter-01` or `comic-0244`. The
schedule starts at a certain point and then continues every day or week as needed.
The `PeriodicPathRegexSchedule` encapsulates this pattern. It uses the `UPath` component
of the entity and compares it against a regular expression that captures the numerical
part of the path. If it matches, then the schedule sets the date equal to the starting
point plus the "period" for every one past the first.
In this example:
```csharp
ApplySchedule op;
var entities = new List<Entity>
{
new Entity().Set((UPath) "chapter-01.md"),
new Entity().Set((UPath) "chapter-02.md"),
new Entity().Set((UPath) "chapter-03.md"),
};
var schedules = new List<ISchedule>
{
new PeriodicPathRegexSchedule
{
PathRegex = "chapter-(\d+),
ScheduleStart = DateTime.Parse("2023-01-01"),
SchedulePeriod = "1 week",
// Alternatively, SchedulePeriodTimeSpan = TimeSpan.FromDays(7),
},
}
return entities.Run(op.WithSchedules(schedules));
```
This will have chapter-01 have an `Instant` component set to 2023-01-01, the second
chapter will be set to 2023-01-08, and the third at 2023-01-15.
`PeriodicPathRegexSchedule` has the following properties:
- `Func<Entity, string> GetPath`
- An override to allow retrieving a different function.
- Defaults to `entity.Get<UPath>().ToString()`
- `Regex PathRegex`
- The regular expression that retrieves the number.
- Defaults to `^.*?(\d+)[^\d]*$` which grabs the last number found.
- `DateTime ScheduleStart`
- The date that the schedule starts.
- No default.
- `string SchedulePeriod`
- Parsed using https://github.com/pengowray/TimeSpanParser
- Supports any `TimeSpan` value, also "2 weeks" and Humanizer formatted values
- `TimeSpan SchedulePeriodTimeSpan`
- Parsed from `SchedulePeriod`
- `int CaptureGroup`
- The numerical index of the capture group.
- Defaults to `1` because the first match in Regex is 1.
- `int CaptureOffset`
- The offset to make the capture group a zero-based number.
- Defaults to `-1` which makes `chapter-01` the first entry.
There is also a virtual method for applying the schedule.
- `protected Entity ApplySchedule(Entity entity, int number, Instant instant)`
- The callback method for applying the schedule to the entity.
- This defaults to `return entity.Set(instant)`.
#### Overriding Logic
The default operation of a `PeriodicPathRegexSchedule` is to only set the `Instant`
component of the entity. The class can be extended to have more site-specific
entries including adding more properties to the schedule and applying them.
```csharp
/// <summary>A model for the YAML front matter on a page.</summary>
public class PageModel
{
/// <summary>Gets or sets the access key for the page.</summary>
public string? Access { get; set; }
/// <summary>Gets or sets the optional schedule for this page.</summary>
List<PageSchedule>? Schedules { get; set; }
}
/// <summary>A schedule specific to this project.</summary>
public class PageSchedule : NumericalPathSchedule
{
public PageSchedule()
{
// Set the default to weekly.
this.SchedulePeriod = SchedulePeriod.Week;
}
/// <summary>Gets or sets the access key for the page.</summary>
public Access { get; set; }
protected override Entity Apply(Entity entity, int number, Instant instant)
{
var model = entity.Get<PageModel>();
model.Access = this.Access;
return entity.Set(instant, model);
}
}
```
Commonly, this schedule will be put into a JSON or YAML file.
```yaml
schedules:
# Patron and Ko-Fi subscribers get it all at once
- pathRegex: chapters/chapter-(\d+)
scheduleStart: 2025-01-01
schedulePeriod: instant # Because we overrode the default to be weekly.
access: subscribers
# The first fifteen chapters (01-15) were released at the rate of one
# per week starting in 2024. This will replace all the schedules above
# it.
- path: chapters/chapter-(0\d|1[1-5])
scheduleStart: 2030-01-01
access: public
```
### Indexed Path Regex Schedules
A variant on the periodic schedule is the `IndexedPathRegexSchedule`. This evolved
from the periodic one in order to simplify the regular expressions at the expense
of having a more complex structure. It uses the same regular expression as
`PeriodicPathRegexSchedule` including `CaptureGroup` (still defaults to 1) and
`CaptureOffset` (which defaults to 0) but instead of having the schedule date
and period, those are relegated to a `Dictionary<int, IndexedSchedule>` (or a
class that extends `IndexedSchedule`).
Using the below example:
```yaml
schedules:
pathRegex: chapters/chapter-(\d+)
indexes:
1:
scheduleStart: 2025-01-01
schedulePeriod: instant
access: subscribers
10:
scheduleStart: 2030-01-01
schedulePeriod: 1 week
access: subscribers
30:
schedulePeriod: never
```
For a given calculated number (such as `chapter-01` being `1`), the schedule picks the
highest index that is not replaced by a higher number. So, chapters 1-9 would use the 2025
date and be instantly available on that date while chapters 10 and higher would use the
2030 date and be doled out a week at a time.
While this example doesn't use the top level as a sequence, it could be used to handle
multiple overlapping schedules (like the first where two changes), but it is more
verbose while describing it.
For the inner schedule, the number is normalized to be 0-based automatically for each one
so the above example would have 1, 10, and 30 all pass in `0` into the `Apply` function.

Some files were not shown because too many files have changed in this diff Show more