├── test
├── Statik.Tests
│ ├── Embedded
│ │ ├── file1.txt
│ │ ├── file2.txt
│ │ └── nested
│ │ │ ├── file3.txt
│ │ │ ├── file4.txt
│ │ │ └── nested2
│ │ │ └── file5.txt
│ ├── WorkingDirectorySession.cs
│ ├── Statik.Tests.csproj
│ ├── HelperTests.cs
│ ├── HostingTests.cs
│ ├── WebBuilderTests.cs
│ ├── ExporterTests.cs
│ ├── PageDirectoryLoaderTests.cs
│ └── EmbeddedFileProviderTests.cs
├── Directory.Build.props
├── Statik.Mvc.Tests
│ ├── Statik.Mvc.Tests.csproj
│ └── MvcTests.cs
└── Statik.Files.Tests
│ ├── Statik.Files.Tests.csproj
│ ├── EmbeddedFileProviderHostTests.cs
│ └── FileProviderTests.cs
├── examples
├── Statik.Examples.Pages
│ ├── pages
│ │ ├── index.md
│ │ ├── nested-dir
│ │ │ ├── index.md
│ │ │ └── yet-another.md
│ │ └── another-page.md
│ ├── Views
│ │ ├── _ViewStart.cshtml
│ │ ├── _ViewImports.cshtml
│ │ ├── Shared
│ │ │ └── _Layout.cshtml
│ │ └── Pages
│ │ │ └── Index.cshtml
│ ├── Statik.Examples.Pages.csproj
│ ├── Models
│ │ └── PageModel.cs
│ ├── Controllers
│ │ └── PagesController.cs
│ └── Program.cs
├── Directory.Build.props
├── Statik.Examples.Simple
│ ├── Statik.Examples.Simple.csproj
│ └── Program.cs
└── Statik.Examples.Mvc
│ ├── Statik.Examples.Mvc.csproj
│ ├── ExampleController.cs
│ └── Program.cs
├── src
├── Directory.Build.props
├── Statik
│ ├── Properties.cs
│ ├── Hosting
│ │ ├── IVirtualHost.cs
│ │ ├── IWebHost.cs
│ │ ├── IHostExporter.cs
│ │ ├── IHostBuilder.cs
│ │ ├── IHost.cs
│ │ ├── IHostModule.cs
│ │ └── Impl
│ │ │ ├── Startup.cs
│ │ │ ├── AppBaseAppendMessageHandler.cs
│ │ │ ├── HostBuilder.cs
│ │ │ └── HostExporter.cs
│ ├── StatikDefaults.cs
│ ├── IPageAccessor.cs
│ ├── Web
│ │ ├── IWebBuilderFactory.cs
│ │ ├── Impl
│ │ │ ├── WebBuilderFactory.cs
│ │ │ └── WebBuilder.cs
│ │ ├── IWebBuilder.cs
│ │ ├── Page.cs
│ │ └── IPageRegistry.cs
│ ├── Pages
│ │ ├── IPageDirectoryLoader.cs
│ │ ├── PageDirectoryLoaderOptions.cs
│ │ ├── PageDirectoryLoaderExtensions.cs
│ │ ├── PageTreeItem.cs
│ │ └── Impl
│ │ │ └── PageDirectoryLoader.cs
│ ├── Embedded
│ │ ├── IAssemblyResourceResolver.cs
│ │ ├── EmbeddedFile.cs
│ │ ├── Impl
│ │ │ └── AssemblyResourceResolver.cs
│ │ ├── EmbeddedDirectoryInfo.cs
│ │ ├── EmbeddedFileInfo.cs
│ │ ├── EnumerableDirectoryContents.cs
│ │ └── EmbeddedFileProvider.cs
│ ├── HttpContextExtensions.cs
│ ├── Statik.csproj
│ ├── RedirectExtensions.cs
│ ├── Statik.cs
│ └── StatikHelpers.cs
├── Statik.Markdown
│ ├── IMarkdownParser.cs
│ ├── IMarkdownRenderer.cs
│ ├── Statik.Markdown.csproj
│ ├── MarkdownParseResult.cs
│ └── Impl
│ │ ├── MarkdownRenderer.cs
│ │ └── MarkdownParser.cs
├── Statik.Files
│ ├── Statik.Files.csproj
│ ├── RegisterOptions.cs
│ └── WebBuilderExtensions.cs
└── Statik.Mvc
│ ├── Statik.Mvc.csproj
│ ├── FormRouteDataAttribute.cs
│ └── WebBuilderExtensions.cs
├── .gitmodules
├── Statik.sln.DotSettings
├── LICENSE
├── README.md
├── .gitignore
└── Statik.sln
/test/Statik.Tests/Embedded/file1.txt:
--------------------------------------------------------------------------------
1 | file1content
--------------------------------------------------------------------------------
/test/Statik.Tests/Embedded/file2.txt:
--------------------------------------------------------------------------------
1 | file2content
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/pages/index.md:
--------------------------------------------------------------------------------
1 | Test page
--------------------------------------------------------------------------------
/test/Statik.Tests/Embedded/nested/file3.txt:
--------------------------------------------------------------------------------
1 | file3content
--------------------------------------------------------------------------------
/test/Statik.Tests/Embedded/nested/file4.txt:
--------------------------------------------------------------------------------
1 | file4content
--------------------------------------------------------------------------------
/test/Statik.Tests/Embedded/nested/nested2/file5.txt:
--------------------------------------------------------------------------------
1 | file5content
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/pages/nested-dir/index.md:
--------------------------------------------------------------------------------
1 | Nested index
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/pages/another-page.md:
--------------------------------------------------------------------------------
1 | Another test page
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/pages/nested-dir/yet-another.md:
--------------------------------------------------------------------------------
1 | Yet another page, nested
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Views/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "_Layout";
3 | }
4 |
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/Statik/Properties.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly:InternalsVisibleTo("Statik.Tests")]
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "build/scripts/Buildary"]
2 | path = build/scripts/Buildary
3 | url = https://github.com/pauldotknopf/dotnet-buildary.git
4 |
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Views/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @using Statik.Examples.Pages.Models
2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 |
--------------------------------------------------------------------------------
/src/Statik/Hosting/IVirtualHost.cs:
--------------------------------------------------------------------------------
1 | namespace Statik.Hosting
2 | {
3 | public interface IVirtualHost : IHost
4 | {
5 |
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/IWebHost.cs:
--------------------------------------------------------------------------------
1 | namespace Statik.Hosting
2 | {
3 | public interface IWebHost : IHost
4 | {
5 | void Listen();
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Statik/StatikDefaults.cs:
--------------------------------------------------------------------------------
1 | namespace Statik
2 | {
3 | public class StatikDefaults
4 | {
5 | public const int DefaultPort = 8000;
6 | }
7 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @RenderBody()
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Statik/IPageAccessor.cs:
--------------------------------------------------------------------------------
1 | using Statik.Web;
2 |
3 | namespace Statik
4 | {
5 | public interface IPageAccessor
6 | {
7 | Page Page { get; }
8 | }
9 | }
--------------------------------------------------------------------------------
/test/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | false
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | false
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Statik/Web/IWebBuilderFactory.cs:
--------------------------------------------------------------------------------
1 | using Statik.Web;
2 |
3 | namespace Statik.Web
4 | {
5 | public interface IWebBuilderFactory
6 | {
7 | IWebBuilder CreateWebBuilder();
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Statik.Markdown/IMarkdownParser.cs:
--------------------------------------------------------------------------------
1 | namespace Statik.Markdown
2 | {
3 | public interface IMarkdownParser
4 | {
5 | MarkdownParseResult Parse(string markdown) where T : class;
6 | }
7 | }
--------------------------------------------------------------------------------
/src/Statik.Markdown/IMarkdownRenderer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Statik.Markdown
4 | {
5 | public interface IMarkdownRenderer
6 | {
7 | string Render(string markdown, Func linkRewriter = null);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Statik.Files/Statik.Files.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Statik/Pages/IPageDirectoryLoader.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.FileProviders;
2 |
3 | namespace Statik.Pages
4 | {
5 | public interface IPageDirectoryLoader
6 | {
7 | PageTreeItem LoadFiles(IFileProvider fileProvider, PageDirectoryLoaderOptions options);
8 | }
9 | }
--------------------------------------------------------------------------------
/src/Statik/Embedded/IAssemblyResourceResolver.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace Statik.Embedded
5 | {
6 | public interface IAssemblyResourceResolver
7 | {
8 | string[] GetManifestResourceNames();
9 |
10 | Stream GetManifestResourceStream(string name);
11 | }
12 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Simple/Statik.Examples.Simple.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net6.0
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/examples/Statik.Examples.Mvc/Statik.Examples.Mvc.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net6.0
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/Statik/Hosting/IHostExporter.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace Statik.Hosting
4 | {
5 | public interface IHostExporter
6 | {
7 | Task Export(IHost host, string destinationDirectory);
8 |
9 | Task ExportParallel(IHost host, string destinationDirectory, int? maxThreads = null);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/Statik.Markdown/Statik.Markdown.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/Statik/Hosting/IHostBuilder.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 |
3 | namespace Statik.Hosting
4 | {
5 | public interface IHostBuilder
6 | {
7 | IWebHost BuildWebHost(int port, PathString appBase, params IHostModule[] modules);
8 |
9 | IVirtualHost BuildVirtualHost(PathString appBase, params IHostModule[] modules);
10 | }
11 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Mvc/ExampleController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace Statik.Examples.Mvc
5 | {
6 | public class ExampleController : Controller
7 | {
8 | public ActionResult Index()
9 | {
10 | return Content($"The time is {DateTime.Now.ToLongTimeString()}");
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/Statik/Embedded/EmbeddedFile.cs:
--------------------------------------------------------------------------------
1 | namespace Statik.Embedded
2 | {
3 | internal class EmbeddedFile
4 | {
5 | public EmbeddedFile(string path, string resourceName)
6 | {
7 | Path = path;
8 | ResourceName = resourceName;
9 | }
10 |
11 | public string Path { get; }
12 |
13 | public string ResourceName { get; }
14 | }
15 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/IHost.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Net.Http;
4 | using Statik.Web;
5 |
6 | namespace Statik.Hosting
7 | {
8 | public interface IHost : IDisposable
9 | {
10 | IReadOnlyCollection Pages { get; }
11 |
12 | HttpClient CreateClient();
13 |
14 | IServiceProvider ServiceProvider { get; }
15 | }
16 | }
--------------------------------------------------------------------------------
/Statik.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | True
--------------------------------------------------------------------------------
/src/Statik.Markdown/MarkdownParseResult.cs:
--------------------------------------------------------------------------------
1 | namespace Statik.Markdown
2 | {
3 | public class MarkdownParseResult
4 | {
5 | public MarkdownParseResult(T yaml, string markdown)
6 | {
7 | Yaml = yaml;
8 | Markdown = markdown;
9 | }
10 |
11 | public T Yaml { get; }
12 |
13 | public string Markdown { get; }
14 | }
15 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Views/Pages/Index.cshtml:
--------------------------------------------------------------------------------
1 | @model PageModel
2 |
3 | @Html.Raw(Model.Content)
4 |
5 | @if (Model.TreeItem.Children.Count > 0)
6 | {
7 |
8 | @foreach (var child in Model.TreeItem.Children)
9 | {
10 | -
11 |
12 | @child.Path
13 |
14 |
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Statik.Examples.Pages.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | Exe
4 | net6.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/Statik.Files/RegisterOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.FileProviders;
2 | using Microsoft.Extensions.FileSystemGlobbing;
3 |
4 | namespace Statik.Files
5 | {
6 | public class RegisterOptions
7 | {
8 | public BuildStateDelegate State { get; set; }
9 |
10 | public Matcher Matcher { get; set; }
11 |
12 | public delegate object BuildStateDelegate(string requestPrefix, string requestFullPath, string filePath, IFileInfo file, IFileProvider fileProvider);
13 | }
14 | }
--------------------------------------------------------------------------------
/src/Statik/HttpContextExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 | using Statik.Web;
3 |
4 | namespace Statik
5 | {
6 | public static class HttpContextExtensions
7 | {
8 | public static Page GetCurrentPage(this HttpContext httpContext)
9 | {
10 | if (!httpContext.Items.TryGetValue("_statikPageAccessor", out var pageAccessor)) return null;
11 | if (pageAccessor is IPageAccessor accessor) return accessor.Page;
12 |
13 | return null;
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/src/Statik/Web/Impl/WebBuilderFactory.cs:
--------------------------------------------------------------------------------
1 | using Statik.Hosting;
2 |
3 | namespace Statik.Web.Impl
4 | {
5 | public class WebBuilderFactory : IWebBuilderFactory
6 | {
7 | readonly IHostBuilder _hostBuilder;
8 |
9 | public WebBuilderFactory(IHostBuilder hostBuilder)
10 | {
11 | _hostBuilder = hostBuilder;
12 | }
13 |
14 | public IWebBuilder CreateWebBuilder()
15 | {
16 | return new WebBuilder(_hostBuilder);
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/IHostModule.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Statik.Web;
6 |
7 | namespace Statik.Hosting
8 | {
9 | public interface IHostModule
10 | {
11 | void Configure(IApplicationBuilder app, IWebHostEnvironment env);
12 |
13 | void ConfigureServices(IServiceCollection services);
14 |
15 | IReadOnlyCollection Pages { get; }
16 | }
17 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Models/PageModel.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.FileProviders;
2 | using Statik.Pages;
3 |
4 | namespace Statik.Examples.Pages.Models
5 | {
6 | public class PageModel
7 | {
8 | public PageModel(PageTreeItem treeItem, string content)
9 | {
10 | TreeItem = treeItem;
11 | Content = content;
12 | }
13 |
14 | public PageTreeItem TreeItem { get; }
15 |
16 | public string Content { get; }
17 | }
18 | }
--------------------------------------------------------------------------------
/src/Statik.Mvc/Statik.Mvc.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Statik/Web/IWebBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Http;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Statik.Hosting;
6 |
7 | namespace Statik.Web
8 | {
9 | public interface IWebBuilder
10 | {
11 | void Register(string path, Func action, object state = null, bool extractExactPath = false);
12 |
13 | void RegisterServices(Action action);
14 |
15 | IWebHost BuildWebHost(string appBase = null, int port = StatikDefaults.DefaultPort);
16 |
17 | IVirtualHost BuildVirtualHost(string appBase = null);
18 | }
19 | }
--------------------------------------------------------------------------------
/src/Statik/Embedded/Impl/AssemblyResourceResolver.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Reflection;
3 |
4 | namespace Statik.Embedded.Impl
5 | {
6 | public class AssemblyResourceResolver : IAssemblyResourceResolver
7 | {
8 | readonly Assembly _assembly;
9 |
10 | public AssemblyResourceResolver(Assembly assembly)
11 | {
12 | _assembly = assembly;
13 | }
14 |
15 | public string[] GetManifestResourceNames()
16 | {
17 | return _assembly.GetManifestResourceNames();
18 | }
19 |
20 | public Stream GetManifestResourceStream(string name)
21 | {
22 | return _assembly.GetManifestResourceStream(name);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/test/Statik.Mvc.Tests/Statik.Mvc.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
9 | all
10 | runtime; build; native; contentfiles; analyzers; buildtransitive
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/Statik/Web/Page.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Http;
4 |
5 | namespace Statik.Web
6 | {
7 | public class Page
8 | {
9 | internal Page(string path,
10 | Func action,
11 | object state,
12 | bool extractExactPath)
13 | {
14 | Path = path;
15 | Action = action;
16 | State = state;
17 | ExtractExactPath = extractExactPath;
18 | }
19 |
20 | public string Path { get; }
21 |
22 | internal Func Action { get; }
23 |
24 | public object State { get; }
25 |
26 | public bool ExtractExactPath { get; }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/Statik/Pages/PageDirectoryLoaderOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.FileProviders;
3 | using Microsoft.Extensions.FileSystemGlobbing;
4 |
5 | namespace Statik.Pages
6 | {
7 | public class PageDirectoryLoaderOptions
8 | {
9 | public Matcher IndexPageMatcher { get; set; }
10 |
11 | public Matcher NormalPageMatcher { get; set; }
12 |
13 | public Func PageSlug { get; set; }
14 |
15 | internal PageDirectoryLoaderOptions Clone()
16 | {
17 | return new PageDirectoryLoaderOptions
18 | {
19 | IndexPageMatcher = IndexPageMatcher,
20 | NormalPageMatcher = NormalPageMatcher,
21 | PageSlug = PageSlug
22 | };
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/Statik/Statik.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/Statik.Files.Tests/Statik.Files.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
9 | all
10 | runtime; build; native; contentfiles; analyzers; buildtransitive
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/test/Statik.Tests/WorkingDirectorySession.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace Statik.Tests
5 | {
6 | public class WorkingDirectorySession : IDisposable
7 | {
8 | public WorkingDirectorySession()
9 | {
10 | var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
11 | while (System.IO.Directory.Exists(path))
12 | path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Path.GetFileNameWithoutExtension(Path.GetRandomFileName()));
13 | System.IO.Directory.CreateDirectory(path);
14 | Directory = path;
15 | }
16 |
17 | public string Directory { get; }
18 |
19 | public void Dispose()
20 | {
21 | System.IO.Directory.Delete(Directory, true);
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/src/Statik/Embedded/EmbeddedDirectoryInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Microsoft.Extensions.FileProviders;
4 |
5 | namespace Statik.Embedded
6 | {
7 | internal class EmbeddedDirectoryInfo : IFileInfo
8 | {
9 | private readonly string _path;
10 |
11 | public EmbeddedDirectoryInfo(string path)
12 | {
13 | _path = path;
14 | }
15 |
16 | public bool Exists => true;
17 |
18 | public long Length => -1;
19 |
20 | public string PhysicalPath => null;
21 |
22 | public string Name => Path.GetFileName(_path);
23 |
24 | public DateTimeOffset LastModified => DateTimeOffset.Now;
25 |
26 | public bool IsDirectory => true;
27 |
28 | public Stream CreateReadStream()
29 | {
30 | throw new InvalidOperationException("Cannot create a stream for a directory.");
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Statik/Web/IPageRegistry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace Statik.Web
6 | {
7 | public interface IPageRegistry
8 | {
9 | IReadOnlyCollection GetPaths();
10 |
11 | Page GetPage(string path);
12 |
13 | Page FindOne(PageMatchDelegate match);
14 |
15 | Task FindOne(PageMatchDelegateAsync match);
16 |
17 | List FindMany(PageMatchDelegate match);
18 |
19 | Task> FindMany(PageMatchDelegateAsync match);
20 |
21 | void ForEach(PageActionDelegate action);
22 |
23 | Task ForEach(PageActionDelegateAsync action);
24 | }
25 |
26 | public delegate bool PageMatchDelegate(Page page);
27 |
28 | public delegate Task PageMatchDelegateAsync(Page page);
29 |
30 | public delegate void PageActionDelegate(Page page);
31 |
32 | public delegate Task PageActionDelegateAsync(Page page);
33 | }
--------------------------------------------------------------------------------
/src/Statik.Mvc/FormRouteDataAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Mvc.ModelBinding;
4 |
5 | namespace Statik.Mvc
6 | {
7 | public class FromRouteDataAttribute : Attribute, IBinderTypeProviderMetadata, IBindingSourceMetadata
8 | {
9 | public FromRouteDataAttribute()
10 | {
11 | BinderType = typeof(Binder);
12 | }
13 |
14 | public BindingSource BindingSource => BindingSource.Custom;
15 |
16 | public Type BinderType { get; }
17 |
18 | private class Binder : IModelBinder
19 | {
20 | public Task BindModelAsync(ModelBindingContext bindingContext)
21 | {
22 | var routeData = bindingContext.ActionContext.RouteData.Values;
23 | bindingContext.Result = ModelBindingResult.Success(routeData[bindingContext.FieldName]);
24 | return Task.CompletedTask;
25 | }
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/Statik.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | all
12 | runtime; build; native; contentfiles; analyzers; buildtransitive
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Statik/Pages/PageDirectoryLoaderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.FileProviders;
3 | using Microsoft.Extensions.FileSystemGlobbing;
4 |
5 | namespace Statik.Pages
6 | {
7 | public static class PageDirectoryLoaderExtensions
8 | {
9 | public static PageTreeItem LoadFiles(this IPageDirectoryLoader pageDirectoryLoader,
10 | IFileProvider fileProvider,
11 | string pageGlob,
12 | string indexGlob)
13 | {
14 | var pageMatcher = new Matcher(StringComparison.OrdinalIgnoreCase);
15 | pageMatcher.AddInclude(pageGlob);
16 | var indexMatcher = new Matcher(StringComparison.OrdinalIgnoreCase);
17 | indexMatcher.AddInclude(indexGlob);
18 | return pageDirectoryLoader.LoadFiles(fileProvider, new PageDirectoryLoaderOptions
19 | {
20 | NormalPageMatcher = pageMatcher,
21 | IndexPageMatcher = indexMatcher
22 | });
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Paul Knopf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Controllers/PagesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.Extensions.FileProviders;
6 | using Statik.Examples.Pages.Models;
7 | using Statik.Pages;
8 |
9 | namespace Statik.Examples.Pages.Controllers
10 | {
11 | public class PagesController : Controller
12 | {
13 | public async Task Index()
14 | {
15 | var treeItem = RouteData.Values["treeItem"] as PageTreeItem;
16 | if(treeItem == null) throw new InvalidOperationException();
17 |
18 | string content = null;
19 | if (!treeItem.Data.IsDirectory)
20 | {
21 | using (var stream = treeItem.Data.CreateReadStream())
22 | using (var streamReader = new StreamReader(stream))
23 | content = await streamReader.ReadToEndAsync();
24 | content = Markdig.Markdown.ToHtml(content);
25 | }
26 |
27 | var model = new PageModel(treeItem, content);
28 |
29 | return View(model);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Statik/RedirectExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Http;
3 | using Statik.Web;
4 |
5 | namespace Statik
6 | {
7 | public static class RedirectExtensions
8 | {
9 | public static void Redirect(this IWebBuilder webBuilder, string from, string to, object state = null, bool extractExactPath = false)
10 | {
11 | webBuilder.Register(from, async (context) =>
12 | {
13 | context.Response.ContentType = "text/html";
14 | await context.Response.WriteAsync($@"
15 |
16 |
17 | Redirecting…
18 |
19 |
20 |
21 |
22 | Redirecting…
23 | Click here if you are not redirected.
24 | ");
25 |
26 | },
27 | state,
28 | extractExactPath);
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/Impl/Startup.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace Statik.Hosting.Impl
7 | {
8 | internal class Startup
9 | {
10 | List _modules;
11 |
12 | public Startup(List modules)
13 | {
14 | _modules = modules;
15 | }
16 |
17 | // This method gets called by the runtime. Use this method to add services to the container.
18 | public void ConfigureServices(IServiceCollection services)
19 | {
20 | foreach(var module in _modules)
21 | {
22 | module.ConfigureServices(services);
23 | }
24 | }
25 |
26 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
27 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
28 | {
29 | app.UseDeveloperExceptionPage();
30 | foreach(var module in _modules)
31 | {
32 | module.Configure(app, env);
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Simple/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Http;
4 |
5 | namespace Statik.Examples.Simple
6 | {
7 | class Program
8 | {
9 | static void Main(string[] args)
10 | {
11 | var webBuilder = Statik.GetWebBuilder();
12 |
13 | webBuilder.Register("/hello", async context =>
14 | {
15 | await context.Response.WriteAsync($"The time is {DateTime.Now.ToLongTimeString()}");
16 | });
17 |
18 | using (var host = webBuilder.BuildWebHost())
19 | {
20 | host.Listen();
21 | Console.WriteLine($"Listening on port {StatikDefaults.DefaultPort}...");
22 | Console.WriteLine($"Try visiting http://localhost:{StatikDefaults.DefaultPort}/hello");
23 | Console.WriteLine("Press enter to exit...");
24 | Console.ReadLine();
25 |
26 | // NOTE: You can export this host to a directory.
27 | // Useful for GitHub pages, etc.
28 | // await Statik.ExportHost(host, "./somewhere");
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Statik.Markdown/Impl/MarkdownRenderer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Markdig;
4 | using Markdig.Renderers;
5 |
6 | namespace Statik.Markdown.Impl
7 | {
8 | public class MarkdownRenderer : IMarkdownRenderer
9 | {
10 | private readonly MarkdownPipeline _pipeline;
11 |
12 | public MarkdownRenderer(MarkdownPipeline pipeline = null)
13 | {
14 | if (pipeline == null)
15 | {
16 | pipeline = new MarkdownPipelineBuilder()
17 | .UseAdvancedExtensions()
18 | .Build();
19 | }
20 |
21 | _pipeline = pipeline;
22 | }
23 |
24 | public string Render(string markdown, Func linkRewriter = null)
25 | {
26 | var writer = new StringWriter();
27 | var renderer = new HtmlRenderer(writer);
28 | renderer.LinkRewriter = linkRewriter;
29 |
30 | _pipeline.Setup(renderer);
31 |
32 | var document = Markdig.Markdown.Parse(markdown, _pipeline);
33 | renderer.Render(document);
34 | writer.Flush();
35 |
36 | return writer.ToString();
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Mvc/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Statik.Mvc;
4 |
5 | namespace Statik.Examples.Mvc
6 | {
7 | class Program
8 | {
9 | static void Main(string[] args)
10 | {
11 | var webBuilder = Statik.GetWebBuilder();
12 |
13 | webBuilder.RegisterMvcServices();
14 | webBuilder.RegisterMvc("/hello", new
15 | {
16 | controller = "Example",
17 | action = "Index",
18 | other = 1 /* You can pass other values to the action, traditional MVC */
19 | });
20 |
21 | using (var host = webBuilder.BuildWebHost())
22 | {
23 | host.Listen();
24 | Console.WriteLine($"Listening on port {StatikDefaults.DefaultPort}...");
25 | Console.WriteLine($"Try visiting http://localhost:{StatikDefaults.DefaultPort}/hello");
26 | Console.WriteLine("Press enter to exit...");
27 | Console.ReadLine();
28 |
29 | // NOTE: You can export this host to a directory.
30 | // Useful for GitHub pages, etc.
31 | // await Statik.ExportHost(host, "./somewhere");
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/HelperTests.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 | using Xunit;
3 |
4 | namespace Statik.Tests
5 | {
6 | public class HelperTests
7 | {
8 | [Theory]
9 | [InlineData("/test", "test2", "/test/test2")]
10 | [InlineData("/test", "/test2", "/test2")]
11 | [InlineData("/test", "../test2", "/test2")]
12 | [InlineData("/test", "../test2/../test3", "/test3")]
13 | [InlineData("/test?query=ignored", "test2", "/test/test2")]
14 | [InlineData("/test?query=ignored", "test2?query=preserved", "/test/test2?query=preserved")]
15 | [InlineData("/test?#ignored", "test2", "/test/test2")]
16 | [InlineData("/test?#ignored", "test2#preserved", "/test/test2#preserved")]
17 | public void Can_resolve_relative_path(string input, string relative, string expected)
18 | {
19 | StatikHelpers.ResolvePathPart(input, relative).Should().Be(expected);
20 | }
21 |
22 | [Theory]
23 | [InlineData("test", "test")]
24 | [InlineData("--test--", "test")]
25 | [InlineData(".test.", "test")]
26 | [InlineData("test----test", "test-test")]
27 | public void Can_convert_string_to_slug(string input, string expected)
28 | {
29 | StatikHelpers.ConvertStringToSlug(input).Should().Be(expected);
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/src/Statik/Pages/PageTreeItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 |
5 | namespace Statik.Pages
6 | {
7 | public class PageTreeItem
8 | {
9 | public PageTreeItem()
10 | {
11 | Children = new List>();
12 | }
13 |
14 | public PageTreeItem(T data, string path, string filePath)
15 | : this()
16 | {
17 | Data = data;
18 | Path = path;
19 | FilePath = filePath;
20 | }
21 |
22 | public T Data { get; set; }
23 |
24 | public string Path { get; set; }
25 |
26 | public string FilePath { get; set; }
27 |
28 | public List> Children { get; set; }
29 |
30 | public PageTreeItem Parent { get; set; }
31 |
32 | public async Task> Convert(Func, Task> convert)
33 | {
34 | var newTree = new PageTreeItem
35 | {
36 | Path = Path,
37 | FilePath = FilePath,
38 | Data = await convert(this)
39 | };
40 |
41 | foreach (var child in Children)
42 | {
43 | newTree.Children.Add(await child.Convert(convert));
44 | }
45 |
46 | return newTree;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/test/Statik.Mvc.Tests/MvcTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Statik.Hosting.Impl;
5 | using Statik.Web;
6 | using Statik.Web.Impl;
7 | using Xunit;
8 |
9 | namespace Statik.Mvc.Tests
10 | {
11 | public class MvcTests
12 | {
13 | private IWebBuilder _webBuilder;
14 |
15 | public MvcTests()
16 | {
17 | _webBuilder = new WebBuilder(new HostBuilder());
18 | _webBuilder.RegisterMvcServices();
19 | }
20 |
21 | [Fact]
22 | public async Task Can_call_mvc_action()
23 | {
24 | _webBuilder.RegisterMvc("/somewhere", new
25 | {
26 | controller = "Test",
27 | action = "Index"
28 | });
29 | using(var host = _webBuilder.BuildVirtualHost())
30 | {
31 | using(var client = host.CreateClient())
32 | {
33 | var responseMessage = await client.GetAsync("/somewhere");
34 | responseMessage.EnsureSuccessStatusCode();
35 | var response = await responseMessage.Content.ReadAsStringAsync();
36 |
37 | Assert.Equal("test", response);
38 | }
39 | }
40 | }
41 | }
42 |
43 | public class TestController : Controller
44 | {
45 | public ActionResult Index()
46 | {
47 | return Content("test", "application/text");
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/examples/Statik.Examples.Pages/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Microsoft.Extensions.FileProviders;
4 | using Statik.Mvc;
5 | using Statik.Pages;
6 |
7 | namespace Statik.Examples.Pages
8 | {
9 | class Program
10 | {
11 | static void Main(string[] args)
12 | {
13 | var webBuilder = Statik.GetWebBuilder();
14 | webBuilder.RegisterMvcServices();
15 |
16 | var pagesDirectory = Path.Combine(Directory.GetCurrentDirectory(), "pages");
17 |
18 | var rootTreeItem = Statik.GetPageDirectoryLoader().LoadFiles(new PhysicalFileProvider(pagesDirectory),
19 | "*.md",
20 | "index.md");
21 |
22 | void RegisterTreeItem(PageTreeItem treeItem)
23 | {
24 | if (!treeItem.Data.IsDirectory)
25 | {
26 | webBuilder.RegisterMvc(treeItem.Path,
27 | new
28 | {
29 | controller = "Pages",
30 | action = "Index",
31 | treeItem
32 | });
33 | }
34 |
35 | foreach (var child in treeItem.Children)
36 | {
37 | RegisterTreeItem(child);
38 | }
39 | }
40 |
41 | RegisterTreeItem(rootTreeItem);
42 |
43 | using (var host = webBuilder.BuildWebHost())
44 | {
45 | host.Listen();
46 | Console.WriteLine($"Listening on port {StatikDefaults.DefaultPort}...");
47 | Console.WriteLine($"Try visiting http://localhost:{StatikDefaults.DefaultPort}/hello");
48 | Console.WriteLine("Press enter to exit...");
49 | Console.ReadLine();
50 |
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Statik/Embedded/EmbeddedFileInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 | using Microsoft.Extensions.FileProviders;
5 |
6 | namespace Statik.Embedded
7 | {
8 | internal class EmbeddedFileInfo : IFileInfo
9 | {
10 | readonly EmbeddedFile _file;
11 | readonly IAssemblyResourceResolver _assembly;
12 |
13 | public EmbeddedFileInfo(EmbeddedFile file, IAssemblyResourceResolver assembly)
14 | {
15 | _file = file;
16 | _assembly = assembly;
17 | }
18 |
19 | public bool Exists => true;
20 |
21 | public long Length
22 | {
23 | get
24 | {
25 | EnsureExists();
26 |
27 | using(var stream = _assembly.GetManifestResourceStream(_file.ResourceName))
28 | // ReSharper disable PossibleNullReferenceException
29 | return stream.Length;
30 | // ReSharper restore PossibleNullReferenceException
31 | }
32 | }
33 |
34 | public string PhysicalPath => null;
35 |
36 | public string Name
37 | {
38 | get
39 | {
40 | EnsureExists();
41 |
42 | return Path.GetFileName(_file.Path);
43 | }
44 | }
45 |
46 | public DateTimeOffset LastModified
47 | {
48 | get
49 | {
50 | EnsureExists();
51 |
52 | // TODO:
53 | return DateTime.Now;
54 | }
55 | }
56 |
57 | public bool IsDirectory => false;
58 |
59 | public Stream CreateReadStream()
60 | {
61 | EnsureExists();
62 |
63 | return _assembly.GetManifestResourceStream(_file.ResourceName);
64 | }
65 |
66 | private void EnsureExists()
67 | {
68 | if(!Exists) throw new InvalidOperationException("File doesn't exist.");
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/src/Statik/Statik.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Statik.Hosting;
5 | using Statik.Hosting.Impl;
6 | using Statik.Pages;
7 | using Statik.Pages.Impl;
8 | using Statik.Web;
9 | using Statik.Web.Impl;
10 |
11 | namespace Statik
12 | {
13 | public static class Statik
14 | {
15 | private static readonly object Lock = new object();
16 | private static IServiceProvider _serviceProvider;
17 |
18 | public static IWebBuilder GetWebBuilder()
19 | {
20 | EnsureServiceProvider();
21 | return _serviceProvider.GetRequiredService().CreateWebBuilder();
22 | }
23 |
24 | public static IPageDirectoryLoader GetPageDirectoryLoader()
25 | {
26 | EnsureServiceProvider();
27 | return _serviceProvider.GetRequiredService();
28 | }
29 |
30 | public static Task ExportHost(IHost host, string directory)
31 | {
32 | EnsureServiceProvider();
33 | return _serviceProvider.GetRequiredService().Export(host, directory);
34 | }
35 |
36 | public static void RegisterServices(IServiceCollection services)
37 | {
38 | services.AddSingleton();
39 | services.AddSingleton();
40 | services.AddSingleton();
41 | services.AddSingleton();
42 | }
43 |
44 | private static void EnsureServiceProvider()
45 | {
46 | if (_serviceProvider != null) return;
47 | lock (Lock)
48 | {
49 | if (_serviceProvider != null) return;
50 |
51 | var services = new ServiceCollection();
52 |
53 | RegisterServices(services);
54 |
55 | _serviceProvider = services.BuildServiceProvider();
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/Impl/AppBaseAppendMessageHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net.Http;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Http;
8 |
9 | namespace Statik.Hosting.Impl
10 | {
11 | public class AppBaseAppendMessageHandler : HttpMessageHandler
12 | {
13 | private readonly HttpClient _innerHttpClient;
14 | private readonly PathString _appBase;
15 |
16 | public AppBaseAppendMessageHandler(HttpClient innerHttpClient, PathString appBase)
17 | {
18 | _innerHttpClient = innerHttpClient;
19 | _appBase = appBase;
20 | }
21 |
22 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
23 | {
24 | var copy = new HttpRequestMessage(request.Method, request.RequestUri);
25 |
26 | copy.Content = request.Content;
27 |
28 | foreach (var v in request.Headers)
29 | {
30 | copy.Headers.TryAddWithoutValidation(v.Key, v.Value);
31 | }
32 |
33 | foreach (var v in request.Options)
34 | {
35 | copy.Options.TryAdd(v.Key, v.Value);
36 | }
37 |
38 | copy.Version = request.Version;
39 |
40 | if (!_appBase.HasValue) return _innerHttpClient.SendAsync(copy, cancellationToken);
41 |
42 | var pathString = _appBase;
43 | pathString = pathString.Add(copy.RequestUri.PathAndQuery);
44 |
45 | copy.RequestUri = new Uri(copy.RequestUri.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped) + pathString);
46 |
47 |
48 | return _innerHttpClient.SendAsync(copy, cancellationToken);
49 | }
50 |
51 | protected override void Dispose(bool disposing)
52 | {
53 | base.Dispose(disposing);
54 |
55 | if (disposing)
56 | {
57 | _innerHttpClient.Dispose();
58 | }
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Statik.Markdown/Impl/MarkdownParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 | using Markdig;
7 | using Markdig.Syntax;
8 | using YamlDotNet.Serialization;
9 |
10 | namespace Statik.Markdown.Impl
11 | {
12 | public class MarkdownParser : IMarkdownParser
13 | {
14 | public MarkdownParseResult Parse(string markdown) where T : class
15 | {
16 | if (string.IsNullOrEmpty(markdown))
17 | {
18 | return new MarkdownParseResult(null, null);
19 | }
20 |
21 | markdown = Regex.Replace(markdown, @"(\r\n)|(\n\r)|(\n\r)|(\r)", Environment.NewLine);
22 |
23 | var builder = new MarkdownPipelineBuilder();
24 | builder.Extensions.Add(new Markdig.Extensions.Yaml.YamlFrontMatterExtension());
25 | var pipeline = builder.Build();
26 | var document = Markdig.Markdown.Parse(markdown, pipeline);
27 | var yamlBlocks = document.Descendants()
28 | .ToList();
29 |
30 | if(yamlBlocks.Count == 0)
31 | {
32 | return new MarkdownParseResult(null, markdown);
33 | }
34 |
35 | if(yamlBlocks.Count > 1)
36 | {
37 | throw new InvalidOperationException();
38 | }
39 |
40 | var yamlBlock = yamlBlocks.First();
41 |
42 | var yamlBlockIterator = yamlBlock.Lines.ToCharIterator();
43 | var yamlString = new StringBuilder();
44 | while (yamlBlockIterator.CurrentChar != '\0')
45 | {
46 | yamlString.Append(yamlBlockIterator.CurrentChar);
47 | yamlBlockIterator.NextChar();
48 | }
49 |
50 | var yamlDeserializer = new DeserializerBuilder().Build();
51 | var yamlObject = yamlDeserializer.Deserialize(new StringReader(yamlString.ToString()));
52 |
53 | markdown = markdown.Substring(yamlBlock.Span.End + 1);
54 | if(markdown.StartsWith(Environment.NewLine))
55 | markdown = markdown.Substring(Environment.NewLine.Length);
56 |
57 | return new MarkdownParseResult(yamlObject, markdown);
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/Statik/Embedded/EnumerableDirectoryContents.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Reflection;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.Extensions.FileProviders;
8 |
9 | namespace Statik.Embedded
10 | {
11 | internal class EnumerableDirectoryContents : IDirectoryContents
12 | {
13 | readonly List _entries;
14 | readonly string _subPath;
15 | readonly IAssemblyResourceResolver _assembly;
16 | List _matched = null;
17 |
18 | public EnumerableDirectoryContents(List entries, string subPath, IAssemblyResourceResolver assembly)
19 | {
20 | _entries = entries;
21 | _subPath = subPath;
22 | _assembly = assembly;
23 | }
24 |
25 | public bool Exists
26 | {
27 | get
28 | {
29 | EnsureMatched();
30 | return _matched.Count > 0;
31 | }
32 | }
33 |
34 | public IEnumerator GetEnumerator()
35 | {
36 | EnsureMatched();
37 | return _matched.GetEnumerator();
38 | }
39 |
40 | IEnumerator IEnumerable.GetEnumerator()
41 | {
42 | EnsureMatched();
43 | return _entries.GetEnumerator();
44 | }
45 |
46 | private void EnsureMatched()
47 | {
48 | if (_matched == null)
49 | {
50 | var matched = new List();
51 |
52 | var directories = new List();
53 | foreach (var entry in _entries.Where(x => x.Path.StartsWith(_subPath)))
54 | {
55 | var remaining = entry.Path.Substring(_subPath.Length).TrimStart('/');
56 | if (remaining.IndexOf("/", StringComparison.Ordinal) >= 0)
57 | {
58 | // This is a directory
59 | var directory = $"/{remaining.Substring(0, remaining.IndexOf("/", StringComparison.OrdinalIgnoreCase))}";
60 | if(!directories.Contains(directory))
61 | directories.Add(directory);
62 | }
63 | else
64 | {
65 | matched.Add(new EmbeddedFileInfo(entry, _assembly));
66 | }
67 | }
68 |
69 | foreach (var directory in directories)
70 | {
71 | matched.Add(new EmbeddedDirectoryInfo(directory));
72 | }
73 |
74 | _matched = matched;
75 | }
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Statik
2 |
3 | A **dead simple** static site generator, with **no features**, for .NET.
4 |
5 | [](http://www.nuget.org/packages/Statik/)
6 | [](http://www.nuget.org/packages/Statik.Mvc/)
7 | [](http://www.nuget.org/packages/Statik.Files/)
8 |
9 | ## No features?
10 |
11 | This is a simple tool/library. There are no opinions or abstractions, aside from the abstraction needed to host and export content. There is nothing preventing you, the developer, from doing what you want with your project. Parse and render markdown files in a directory for a blog? Build a user manual? What ever you want, you can do.
12 |
13 | ## Why?
14 |
15 | I've spent many hours fighting static site generators (Hugo, Gatsby, Jekyll, etc). The time taken to learn the plumbing of a framework to do very simple tasks often doesn't compare to the time it would take me to do the same task myself if there were no opinionated abstractions bogging me down. In the end, I always wind up biting the bullet, telling myself "Hey, It will generate the static website for me, I must plow through".
16 |
17 | ## What does Statik look like?
18 |
19 | ```c#
20 | var webBuilder = Statik.GetWebBuilder();
21 |
22 | // This is where your meat-and-potatoes go.
23 | webBuilder.Register("/hello", async context =>
24 | {
25 | await context.Response.WriteAsync($"The time is {DateTime.Now.ToLongTimeString()}");
26 | });
27 |
28 | // All of your endpoints get registered here.
29 | // You could scan a directory for markdown files,
30 | // or register a directory of stylesheets and images.
31 | webBuilder.RegisterDirectory("./assets");
32 |
33 | using (var host = webBuilder.BuildWebHost())
34 | {
35 | host.Listen();
36 | Console.WriteLine("Listening on port 8000...");
37 | Console.WriteLine("Try visiting http://localhost:8000/hello");
38 | Console.WriteLine("Press enter to exit...");
39 | Console.ReadLine();
40 |
41 | // NOTE: You can export this host to a directory.
42 | // Useful for GitHub pages, etc.
43 | // await Statik.ExportHost(host, "./somewhere");
44 | }
45 | ```
46 |
47 | You can also use MVC controllers to handle endpoints.
48 |
49 | ```c#
50 | public class ExampleController : Controller
51 | {
52 | public ActionResult Index(string markdownFile)
53 | {
54 | return Content($"The time is {DateTime.Now.ToLongTimeString()}");
55 | }
56 | }
57 |
58 | webBuilder.RegisterMvc("/blog/hello-world", new
59 | {
60 | controller = "Example",
61 | action = "Index",
62 | markdownFile = "./somewhere/markdown.md"
63 | });
64 | ```
65 |
--------------------------------------------------------------------------------
/src/Statik/Embedded/EmbeddedFileProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Reflection;
5 | using Microsoft.Extensions.FileProviders;
6 | using Microsoft.Extensions.Primitives;
7 | using Statik.Embedded.Impl;
8 |
9 | namespace Statik.Embedded
10 | {
11 | public class EmbeddedFileProvider : IFileProvider
12 | {
13 | static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars()
14 | .Where(c => c != '/' && c != '\\').ToArray();
15 | readonly IAssemblyResourceResolver _assembly;
16 | readonly List _files;
17 |
18 | public EmbeddedFileProvider(IAssemblyResourceResolver assembly, string prefix)
19 | {
20 | _assembly = assembly;
21 | _files = _assembly.GetManifestResourceNames()
22 | .Where(x => x.StartsWith(prefix))
23 | .Select(x =>
24 | {
25 | // This bit will will create convert the resource name
26 | // "this.is.a.resource.jpg" to a path of "/this/is/a/resource.jpg".
27 | var parts = x.Substring(prefix.Length).Split('.');
28 | var directory = parts.Take(parts.Length - 2);
29 | var fileName = parts.Skip(parts.Length - 2);
30 | var path = Path.DirectorySeparatorChar + Path.Combine(Path.Combine(directory.ToArray()), string.Join(".", fileName));
31 |
32 | return new EmbeddedFile(path, x);
33 | })
34 | .ToList();
35 | }
36 |
37 | public EmbeddedFileProvider(Assembly assembly, string prefix)
38 | :this(new AssemblyResourceResolver(assembly), prefix)
39 | {
40 |
41 | }
42 |
43 | public IFileInfo GetFileInfo(string subpath)
44 | {
45 | if (string.IsNullOrEmpty(subpath)) return new NotFoundFileInfo(subpath);
46 |
47 | var file = _files.SingleOrDefault(x => x.Path.Equals(subpath));
48 |
49 | if (file == null) return new NotFoundFileInfo(subpath);
50 |
51 | return new EmbeddedFileInfo(file, _assembly);
52 | }
53 |
54 | public IDirectoryContents GetDirectoryContents(string subpath)
55 | {
56 | if (string.IsNullOrEmpty(subpath)) return NotFoundDirectoryContents.Singleton;
57 |
58 | return new EnumerableDirectoryContents(_files, subpath, _assembly);
59 | }
60 |
61 | public IChangeToken Watch(string filter)
62 | {
63 | return NullChangeToken.Singleton;
64 | }
65 |
66 | private static bool HasInvalidPathChars(string path)
67 | {
68 | return path.IndexOfAny(InvalidFileNameChars) != -1;
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/HostingTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Collections.ObjectModel;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Builder;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Moq;
9 | using Statik.Hosting;
10 | using Statik.Hosting.Impl;
11 | using Statik.Web;
12 | using Xunit;
13 |
14 | namespace Statik.Tests
15 | {
16 | public class HostingTests
17 | {
18 | readonly IHostBuilder _hostBuilder;
19 |
20 | public HostingTests()
21 | {
22 | _hostBuilder = new HostBuilder();
23 | }
24 |
25 | [Fact]
26 | public async Task Can_create_web_host()
27 | {
28 | using(var host = _hostBuilder.BuildWebHost(5002, "", BuildTestHostModule()))
29 | {
30 | host.Listen();
31 | using(var client = host.CreateClient())
32 | {
33 | var responseMessage = await client.GetAsync("/test");
34 | responseMessage.EnsureSuccessStatusCode();
35 | var response = await responseMessage.Content.ReadAsStringAsync();
36 |
37 | Assert.Equal("Hello, World! /test ", response);
38 | }
39 | }
40 | }
41 |
42 | [Fact]
43 | public async Task Can_create_virtual_host()
44 | {
45 | using(var host = _hostBuilder.BuildVirtualHost("", BuildTestHostModule()))
46 | {
47 | using(var client = host.CreateClient())
48 | {
49 | var responseMessage = await client.GetAsync("/test");
50 | responseMessage.EnsureSuccessStatusCode();
51 | var response = await responseMessage.Content.ReadAsStringAsync();
52 |
53 | Assert.Equal("Hello, World! /test ", response);
54 | }
55 | }
56 | }
57 |
58 | private IHostModule BuildTestHostModule()
59 | {
60 | var hostModule = new Mock();
61 | hostModule.Setup(x => x.Configure(It.IsAny(), It.IsAny()))
62 | .Callback((IApplicationBuilder app, IWebHostEnvironment env) => {
63 | app.Run(async context => {
64 | await context.Response.WriteAsync("Hello, World! " + context.Request.Path + " " + context.Request.PathBase);
65 | });
66 | });
67 | hostModule.Setup(x => x.ConfigureServices(It.IsAny()));
68 | hostModule.Setup(x => x.Pages).Returns(new ReadOnlyCollection(new List
69 | {
70 | new Page("/test", null, null, false)
71 | }));
72 | return hostModule.Object;
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/src/Statik/StatikHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text.RegularExpressions;
5 | using Microsoft.AspNetCore.Http;
6 |
7 | namespace Statik
8 | {
9 | public class StatikHelpers
10 | {
11 | public static string ConvertStringToSlug(string value)
12 | {
13 | if (string.IsNullOrEmpty(value))
14 | {
15 | throw new ArgumentOutOfRangeException(nameof(value));
16 | }
17 |
18 | var result = Regex.Replace(value, @"[^A-Za-z0-9_~]+", "-");
19 |
20 | if (result.EndsWith("-"))
21 | {
22 | result = result.Substring(0, result.Length - 1);
23 | }
24 |
25 | if (result.StartsWith("-"))
26 | {
27 | result = result.Substring(1);
28 | }
29 |
30 | return result;
31 | }
32 |
33 | public static string ResolvePathPart(string path, string relative)
34 | {
35 | // Preserve the ending query and segment to append it to the result.
36 | string queryAndSegment = null;
37 | {
38 | var index = Math.Min(
39 | relative.IndexOf("#", StringComparison.OrdinalIgnoreCase),
40 | relative.IndexOf("?", StringComparison.InvariantCultureIgnoreCase));
41 | if (index > -1)
42 | {
43 | queryAndSegment = relative.Substring(index);
44 | relative = relative.Substring(0, index);
45 | }
46 | }
47 |
48 | if (relative.StartsWith("/"))
49 | path = "/";
50 |
51 | if (path.Contains("?"))
52 | path = path.Substring(0, path.IndexOf("?", StringComparison.OrdinalIgnoreCase));
53 |
54 | var fullPathParts = new PathString(path.StartsWith("/") ? path : $"/{path}")
55 | .Add(relative.StartsWith("/") ? relative : $"/{relative}")
56 | .Value.Split('/')
57 | // First entry is empty, because path starts with "/".
58 | .Skip(1)
59 | .ToList();
60 |
61 | var stack = new List();
62 |
63 | foreach (var part in fullPathParts)
64 | {
65 | if (part == "..")
66 | {
67 | if(stack.Count == 0)
68 | throw new Exception($"Invalid path '{relative}'");
69 | stack.RemoveAt(stack.Count - 1);
70 | }else if (part == ".")
71 | {
72 | // Do nothing
73 | }
74 | else
75 | {
76 | stack.Add(part);
77 | }
78 | }
79 |
80 | return $"/{string.Join("/", stack.ToArray())}{queryAndSegment}";
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/src/Statik.Mvc/WebBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using System.Reflection.Emit;
6 | using Microsoft.AspNetCore.Hosting;
7 | using Microsoft.AspNetCore.Mvc;
8 | using Microsoft.AspNetCore.Mvc.Abstractions;
9 | using Microsoft.AspNetCore.Mvc.Infrastructure;
10 | using Microsoft.AspNetCore.Routing;
11 | using Microsoft.Extensions.DependencyInjection;
12 | using Statik.Web;
13 |
14 | namespace Statik.Mvc
15 | {
16 | public static class WebBuilderExtensions
17 | {
18 | public static void RegisterMvcServices(this IWebBuilder webBuilder, params Assembly[] additionalParts)
19 | {
20 | var callingAssembly = Assembly.GetCallingAssembly();
21 | webBuilder.RegisterServices(services =>
22 | {
23 | var parts = additionalParts.ToList();
24 | if (!parts.Contains(callingAssembly))
25 | {
26 | parts.Add(callingAssembly);
27 | }
28 |
29 | var b = services.AddMvc();
30 | foreach (var part in parts)
31 | {
32 | b.AddApplicationPart(part);
33 | }
34 |
35 | b.AddRazorRuntimeCompilation();
36 | });
37 | }
38 |
39 | public static void RegisterMvc(this IWebBuilder webBuilder, string path, object routeData, object state = null)
40 | {
41 | webBuilder.Register(path, async context =>
42 | {
43 | var actionSelector = context.RequestServices.GetRequiredService();
44 | var actionInvokerFactory = context.RequestServices.GetRequiredService();
45 |
46 | var routeContext = new RouteContext(context);
47 | if (routeData != null)
48 | {
49 | foreach(var value in new RouteValueDictionary(routeData))
50 | {
51 | routeContext.RouteData.Values[value.Key] = value.Value;
52 | }
53 | }
54 |
55 | var candidates = actionSelector.SelectCandidates(routeContext);
56 | if (candidates == null || candidates.Count == 0)
57 | {
58 | throw new Exception("No actions matched");
59 | }
60 |
61 | var actionDescriptor = actionSelector.SelectBestCandidate(routeContext, candidates);
62 | if (actionDescriptor == null)
63 | {
64 | throw new Exception("No actions matched");
65 | }
66 |
67 | var actionContext = new ActionContext(context, routeContext.RouteData, actionDescriptor);
68 |
69 | var invoker = actionInvokerFactory.CreateInvoker(actionContext);
70 | if (invoker == null)
71 | {
72 | throw new InvalidOperationException("Couldn't create invoker");
73 | }
74 |
75 | await invoker.InvokeAsync();
76 | },
77 | state);
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/WebBuilderTests.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Http;
3 | using Statik.Web;
4 | using Xunit;
5 |
6 | namespace Statik.Tests
7 | {
8 | public class WebBuilderTests
9 | {
10 | readonly IWebBuilder _webBuilder;
11 |
12 | public WebBuilderTests()
13 | {
14 | _webBuilder = new Web.Impl.WebBuilder(new Hosting.Impl.HostBuilder());
15 | }
16 |
17 | [Fact]
18 | public async Task Can_use_with_app_base_with_web()
19 | {
20 | _webBuilder.Register("/test", async context =>
21 | {
22 | await context.Response.WriteAsync("Hello, World! " + context.Request.Path + " " + context.Request.PathBase);
23 | });
24 |
25 | using(var host = _webBuilder.BuildWebHost("", 5003))
26 | {
27 | host.Listen();
28 | using(var client = host.CreateClient())
29 | {
30 | var responseMessage = await client.GetAsync("/test");
31 | responseMessage.EnsureSuccessStatusCode();
32 | var response = await responseMessage.Content.ReadAsStringAsync();
33 |
34 | Assert.Equal("Hello, World! /test ", response);
35 | }
36 | }
37 |
38 | using(var host = _webBuilder.BuildWebHost("/appbase", 5003))
39 | {
40 | host.Listen();
41 | using(var client = host.CreateClient())
42 | {
43 | var responseMessage = await client.GetAsync("/test");
44 | responseMessage.EnsureSuccessStatusCode();
45 | var response = await responseMessage.Content.ReadAsStringAsync();
46 |
47 | Assert.Equal("Hello, World! /test /appbase", response);
48 | }
49 | }
50 | }
51 |
52 | [Fact]
53 | public async Task Can_use_with_app_base_with_virtual()
54 | {
55 | _webBuilder.Register("/test", async context =>
56 | {
57 | await context.Response.WriteAsync("Hello, World! " + context.Request.Path + " " + context.Request.PathBase);
58 | });
59 |
60 | using(var host = _webBuilder.BuildVirtualHost(""))
61 | {
62 | using(var client = host.CreateClient())
63 | {
64 | var responseMessage = await client.GetAsync("/test");
65 | responseMessage.EnsureSuccessStatusCode();
66 | var response = await responseMessage.Content.ReadAsStringAsync();
67 |
68 | Assert.Equal("Hello, World! /test ", response);
69 | }
70 | }
71 |
72 | using(var host = _webBuilder.BuildVirtualHost("/appbase"))
73 | {
74 | using(var client = host.CreateClient())
75 | {
76 | var responseMessage = await client.GetAsync("/test");
77 | responseMessage.EnsureSuccessStatusCode();
78 | var response = await responseMessage.Content.ReadAsStringAsync();
79 |
80 | Assert.Equal("Hello, World! /test /appbase", response);
81 | }
82 | }
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/ExporterTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using FluentAssertions;
5 | using Microsoft.AspNetCore.Http;
6 | using Statik.Hosting;
7 | using Statik.Hosting.Impl;
8 | using Statik.Web;
9 | using Xunit;
10 |
11 | namespace Statik.Tests
12 | {
13 | public class ExporterTests : IDisposable
14 | {
15 | readonly IWebBuilder _webBuilder;
16 | readonly IHostExporter _hostExporter;
17 | readonly string _directory;
18 |
19 | public ExporterTests()
20 | {
21 | _webBuilder = new Web.Impl.WebBuilder(new HostBuilder());
22 | _hostExporter = new HostExporter();
23 | _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Guid.NewGuid().ToString());
24 | Directory.CreateDirectory(_directory);
25 | }
26 |
27 | [Fact]
28 | public async Task Can_export_path_without_extension()
29 | {
30 | _webBuilder.Register("/test",
31 | context => context.Response.WriteAsync("test content"));
32 | using (var host = _webBuilder.BuildVirtualHost())
33 | {
34 | await _hostExporter.Export(host, _directory);
35 | }
36 |
37 | File.Exists(Path.Combine(_directory, "test", "index.html")).Should().BeTrue();
38 | }
39 |
40 | [Fact]
41 | public async Task Can_export_path_with_extension()
42 | {
43 | _webBuilder.Register("/test.css",
44 | context => context.Response.WriteAsync("test content"));
45 | using (var host = _webBuilder.BuildVirtualHost())
46 | {
47 | await _hostExporter.Export(host, _directory);
48 | }
49 |
50 | File.Exists(Path.Combine(_directory, "test.css")).Should().BeTrue();
51 | File.ReadAllText(Path.Combine(_directory, "test.css")).Should().Be("test content");
52 | }
53 |
54 | [Fact]
55 | public async Task Can_export_path_without_extension_as_exact_path()
56 | {
57 | _webBuilder.Register("/test",
58 | context => context.Response.WriteAsync("test content"),
59 | extractExactPath: true);
60 | using (var host = _webBuilder.BuildVirtualHost())
61 | {
62 | await _hostExporter.Export(host, _directory);
63 | }
64 |
65 | File.Exists(Path.Combine(_directory, "test")).Should().BeTrue();
66 | File.ReadAllText(Path.Combine(_directory, "test")).Should().Be("test content");
67 | }
68 |
69 | [Fact]
70 | public async Task Can_export_parallel()
71 | {
72 | for (var x = 0; x < 40; x++)
73 | {
74 | var _ = x;
75 | _webBuilder.Register($"/test{_}.txt", async context => { await context.Response.WriteAsync($"test content {_}"); });
76 | }
77 |
78 | using (var host = _webBuilder.BuildVirtualHost())
79 | {
80 | await _hostExporter.ExportParallel(host, _directory);
81 | }
82 |
83 | for (var x = 0; x < 40; x++)
84 | {
85 | File.Exists(Path.Combine(_directory, $"test{x}.txt")).Should().BeTrue();
86 | File.ReadAllText(Path.Combine(_directory, $"test{x}.txt")).Should().Be($"test content {x}");
87 | }
88 | }
89 |
90 |
91 | public void Dispose()
92 | {
93 | Directory.Delete(_directory, true);
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/src/Statik/Pages/Impl/PageDirectoryLoader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.Extensions.FileProviders;
7 | using Microsoft.Extensions.FileSystemGlobbing;
8 |
9 | namespace Statik.Pages.Impl
10 | {
11 | public class PageDirectoryLoader : IPageDirectoryLoader
12 | {
13 | public PageTreeItem LoadFiles(IFileProvider fileProvider, PageDirectoryLoaderOptions options)
14 | {
15 | if(options == null) throw new ArgumentNullException(nameof(options));
16 | options = options.Clone();
17 | if(options.PageSlug == null)
18 | options.PageSlug = fileInfo => StatikHelpers.ConvertStringToSlug(Path.GetFileNameWithoutExtension(fileInfo.Name));
19 | return LoadDirectory(fileProvider, "/", null, options);
20 | }
21 |
22 | private PageTreeItem LoadDirectory(IFileProvider fileProvider, string basePath, IFileInfo parentDirectory, PageDirectoryLoaderOptions options)
23 | {
24 | PageTreeItem root = null;
25 | var files = fileProvider.GetDirectoryContents(basePath)
26 | .ToList();
27 |
28 | var children = new List>();
29 |
30 | // Load all the files
31 | foreach (var file in files.Where(x => !x.IsDirectory))
32 | {
33 | if (options.IndexPageMatcher != null && options.IndexPageMatcher.Match(file.Name).HasMatches)
34 | {
35 | // This is the index page.
36 | root = new PageTreeItem(file, basePath, Path.Combine(basePath, file.Name));
37 | }
38 | else
39 | {
40 | var path = basePath == "/"
41 | ? $"/{options.PageSlug(file)}"
42 | : $"{basePath}/{options.PageSlug(file)}";
43 |
44 | if (options.NormalPageMatcher != null)
45 | {
46 | if (options.NormalPageMatcher.Match(file.Name).HasMatches)
47 | {
48 | children.Add(new PageTreeItem(file, path, Path.Combine(basePath, file.Name)));
49 | }
50 | }
51 | else
52 | {
53 | children.Add(new PageTreeItem(file, path, Path.Combine(basePath, file.Name)));
54 | }
55 | }
56 | }
57 |
58 | if(root == null)
59 | root = new PageTreeItem(parentDirectory, basePath, basePath);
60 |
61 | root.Children.AddRange(children);
62 |
63 | // Load all the child directories
64 | foreach (var directory in files.Where(x => x.IsDirectory))
65 | {
66 | var path = new PathString().Add(basePath)
67 | .Add("/" + directory.Name);
68 | var treeItem = LoadDirectory(fileProvider, path, directory, options);
69 | if (treeItem != null)
70 | {
71 | if ((treeItem.Data == null || treeItem.Data.IsDirectory) && treeItem.Children.Count == 0)
72 | {
73 | continue;
74 | }
75 |
76 | root.Children.Add(treeItem);
77 | }
78 | }
79 |
80 | foreach (var child in root.Children)
81 | {
82 | child.Parent = root;
83 | }
84 |
85 | return root;
86 | }
87 | }
88 | }
--------------------------------------------------------------------------------
/test/Statik.Files.Tests/EmbeddedFileProviderHostTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Http;
4 | using Moq;
5 | using Statik.Embedded;
6 | using Statik.Hosting.Impl;
7 | using Statik.Tests;
8 | using Statik.Web;
9 | using Statik.Web.Impl;
10 | using Xunit;
11 |
12 | namespace Statik.Files.Tests
13 | {
14 | public class EmbeddedFileProviderHostTests
15 | {
16 | [Fact]
17 | public void Can_register_embedded_files()
18 | {
19 | var embeddedFiles = new EmbeddedFileProvider(typeof(EmbeddedFileProviderTests).Assembly, "Statik.Tests.Embedded");
20 | var webBuilder = new Mock();
21 |
22 | webBuilder.Setup(x => x.Register("/file1.txt", It.IsAny>(), null, true));
23 | webBuilder.Setup(x => x.Register("/file2.txt", It.IsAny>(), null, true));
24 | webBuilder.Setup(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true));
25 | webBuilder.Setup(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true));
26 | webBuilder.Setup(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true));
27 |
28 | webBuilder.Object.RegisterFileProvider(embeddedFiles);
29 |
30 | webBuilder.Verify(x => x.Register("/file1.txt", It.IsAny>(), null, true), Times.Exactly(1));
31 | webBuilder.Verify(x => x.Register("/file2.txt", It.IsAny>(), null, true), Times.Exactly(1));
32 | webBuilder.Verify(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true), Times.Exactly(1));
33 | webBuilder.Verify(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true), Times.Exactly(1));
34 | webBuilder.Verify(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true), Times.Exactly(1));
35 | }
36 |
37 | [Fact]
38 | public async Task Can_serve_embedded_files()
39 | {
40 | var embeddedFiles = new EmbeddedFileProvider(typeof(EmbeddedFileProviderTests).Assembly, "Statik.Tests.Embedded");
41 | var webBuilder = new WebBuilder(new HostBuilder());
42 |
43 | webBuilder.RegisterFileProvider(embeddedFiles);
44 |
45 | using(var host = webBuilder.BuildVirtualHost())
46 | {
47 | using(var client = host.CreateClient())
48 | {
49 | var responseMessage = await client.GetAsync("/file1.txt");
50 | responseMessage.EnsureSuccessStatusCode();
51 | var response = await responseMessage.Content.ReadAsStringAsync();
52 |
53 | Assert.Equal("file1content", response);
54 |
55 | responseMessage = await client.GetAsync("/nested/nested2/file5.txt");
56 | responseMessage.EnsureSuccessStatusCode();
57 | response = await responseMessage.Content.ReadAsStringAsync();
58 |
59 | Assert.Equal("file5content", response);
60 | }
61 | }
62 |
63 | using(var host = webBuilder.BuildVirtualHost("/appbase"))
64 | {
65 | using(var client = host.CreateClient())
66 | {
67 | var responseMessage = await client.GetAsync("/file1.txt");
68 | responseMessage.EnsureSuccessStatusCode();
69 | var response = await responseMessage.Content.ReadAsStringAsync();
70 |
71 | Assert.Equal("file1content", response);
72 |
73 | responseMessage = await client.GetAsync("/nested/nested2/file5.txt");
74 | responseMessage.EnsureSuccessStatusCode();
75 | response = await responseMessage.Content.ReadAsStringAsync();
76 |
77 | Assert.Equal("file5content", response);
78 | }
79 | }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/Impl/HostBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Linq;
5 | using System.Net.Http;
6 | using System.Reflection;
7 | using Microsoft.AspNetCore;
8 | using Microsoft.AspNetCore.Hosting;
9 | using Microsoft.AspNetCore.Http;
10 | using Microsoft.AspNetCore.TestHost;
11 | using Microsoft.Extensions.DependencyInjection;
12 | using Microsoft.Extensions.Logging;
13 | using Statik.Web;
14 |
15 | namespace Statik.Hosting.Impl
16 | {
17 | public class HostBuilder : IHostBuilder
18 | {
19 | public IWebHost BuildWebHost(int port, PathString appBase, params IHostModule[] modules)
20 | {
21 | return new InternalWebHost(appBase, modules.ToList(), port);
22 | }
23 |
24 | public IVirtualHost BuildVirtualHost(PathString appBase, params IHostModule[] modules)
25 | {
26 | return new InternalVirtualHost(appBase, modules.ToList());
27 | }
28 |
29 | private class InternalWebHost : IWebHost
30 | {
31 | readonly Microsoft.AspNetCore.Hosting.IWebHost _webHost;
32 | readonly PathString _appBase;
33 | readonly int _port;
34 |
35 | public InternalWebHost(
36 | PathString appBase,
37 | List modules,
38 | int port)
39 | {
40 | _appBase = appBase;
41 | _port = port;
42 | Pages = new ReadOnlyCollection(modules.SelectMany(x => x.Pages).ToList());
43 | _webHost = WebHost.CreateDefaultBuilder(new string[]{})
44 | .UseUrls($"http://*:{port}")
45 | .UseSetting(WebHostDefaults.ApplicationKey, Assembly.GetEntryAssembly().GetName().Name)
46 | .ConfigureLogging(factory => {
47 | factory.AddConsole();
48 | })
49 | .ConfigureServices(services => {
50 | services.AddSingleton(modules);
51 | })
52 | .UseStartup()
53 | .Build();
54 | }
55 |
56 | public IReadOnlyCollection Pages { get; }
57 |
58 | public HttpClient CreateClient()
59 | {
60 | var inner = new HttpClient() {
61 | BaseAddress = new Uri($"http://localhost:{_port}")
62 | };
63 | var wrapper = new HttpClient(new AppBaseAppendMessageHandler(inner, _appBase));
64 | wrapper.BaseAddress = inner.BaseAddress;
65 | return wrapper;
66 | }
67 |
68 | public IServiceProvider ServiceProvider => _webHost.Services;
69 |
70 | public void Listen()
71 | {
72 | _webHost.Start();
73 | }
74 |
75 | public void Dispose()
76 | {
77 | _webHost.Dispose();
78 | }
79 | }
80 |
81 | private class InternalVirtualHost : IVirtualHost
82 | {
83 | readonly PathString _appBase;
84 | readonly TestServer _testServer;
85 |
86 | public InternalVirtualHost(
87 | PathString appBase,
88 | List modules)
89 | {
90 | _appBase = appBase;
91 | Pages = new ReadOnlyCollection(modules.SelectMany(x => x.Pages).ToList());
92 | _testServer = new TestServer(new WebHostBuilder()
93 | .UseSetting(WebHostDefaults.ApplicationKey, Assembly.GetEntryAssembly().GetName().Name)
94 | .ConfigureLogging(factory => { factory.AddConsole(); })
95 | .ConfigureServices(services =>
96 | {
97 | services.AddSingleton(modules);
98 | })
99 | .UseStartup());
100 | }
101 |
102 | public IReadOnlyCollection Pages { get; }
103 |
104 | public HttpClient CreateClient()
105 | {
106 | var inner = _testServer.CreateClient();
107 | var wrapper = new HttpClient(new AppBaseAppendMessageHandler(inner, _appBase));
108 | wrapper.BaseAddress = inner.BaseAddress;
109 | return wrapper;
110 | }
111 |
112 | public IServiceProvider ServiceProvider => _testServer.Host.Services;
113 |
114 | public void Dispose()
115 | {
116 | _testServer.Dispose();
117 | }
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/Statik.Files/WebBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Threading.Tasks;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.AspNetCore.Hosting;
5 | using Microsoft.AspNetCore.Http;
6 | using Microsoft.AspNetCore.StaticFiles;
7 | using Microsoft.Extensions.FileProviders;
8 | using Microsoft.Extensions.Logging;
9 | using Microsoft.Extensions.Options;
10 | using Microsoft.Extensions.DependencyInjection;
11 | using Microsoft.Extensions.FileSystemGlobbing;
12 | using Statik.Web;
13 |
14 | namespace Statik.Files
15 | {
16 | public static class WebBuilderExtensions
17 | {
18 | public static void RegisterDirectory(this IWebBuilder webBuilder, string directory, RegisterOptions options = null)
19 | {
20 | RegisterFileProvider(webBuilder, new PhysicalFileProvider(directory), options);
21 | }
22 |
23 | public static void RegisterDirectory(this IWebBuilder webBuilder, PathString prefix, string directory, RegisterOptions options = null)
24 | {
25 | RegisterFileProvider(webBuilder, prefix, new PhysicalFileProvider(directory), options);
26 | }
27 |
28 | public static void RegisterFileProvider(this IWebBuilder webBuilder, IFileProvider fileProvider, RegisterOptions options = null)
29 | {
30 | RegisterFileProvider(webBuilder, "/", fileProvider, options);
31 | }
32 |
33 | public static void RegisterFileProvider(this IWebBuilder webBuilder, PathString prefix, IFileProvider fileProvider, RegisterOptions options = null)
34 | {
35 | var contents = fileProvider.GetDirectoryContents("/");
36 | if(contents == null || !contents.Exists) return;
37 |
38 | foreach(var file in contents)
39 | {
40 | webBuilder.RegisterFileInfo(fileProvider, prefix, "/", file, options);
41 | }
42 | }
43 |
44 | private static void RegisterFileInfo(this IWebBuilder webBuilder, IFileProvider fileProvider, PathString prefix, string basePath, IFileInfo fileInfo, RegisterOptions options)
45 | {
46 | if (fileInfo.IsDirectory)
47 | {
48 | var content = fileProvider.GetDirectoryContents(Path.Combine(basePath, fileInfo.Name));
49 |
50 | if (content == null || !content.Exists)
51 | {
52 | return;
53 | }
54 |
55 | foreach (var child in content)
56 | {
57 | webBuilder.RegisterFileInfo(fileProvider, prefix, Path.Combine(basePath, fileInfo.Name), child, options);
58 | }
59 | }
60 | else
61 | {
62 | var filePath = Path.Combine(basePath, fileInfo.Name);
63 | var requestPath = new PathString().Add(prefix)
64 | .Add(filePath);
65 |
66 | if (options != null && options.Matcher != null)
67 | {
68 | if (!options.Matcher.Match(filePath.Substring(1)).HasMatches)
69 | {
70 | // We are ignoring this file
71 | return;
72 | }
73 | }
74 |
75 | var builtState = options?.State?.Invoke(prefix, requestPath, filePath, fileInfo, fileProvider);
76 |
77 | webBuilder.Register(requestPath, async context =>
78 | {
79 | var env = context.RequestServices.GetRequiredService();
80 |
81 | var statileFileOptions = Options.Create(new StaticFileOptions());
82 | statileFileOptions.Value.FileProvider = fileProvider;
83 | statileFileOptions.Value.ServeUnknownFileTypes = true;
84 |
85 | var loggerFactory = context.RequestServices.GetRequiredService();
86 | var middleware = new StaticFileMiddleware(_ => Task.CompletedTask, env, statileFileOptions, loggerFactory);
87 |
88 | var oldPath = context.Request.Path;
89 | try
90 | {
91 | context.Request.Path = filePath;
92 | await middleware.Invoke(context);
93 | }
94 | finally
95 | {
96 | context.Request.Path = oldPath;
97 | }
98 | },
99 | builtState,
100 | /*don't convert "/file" to "/file/index.html"*/
101 | true);
102 | }
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------
/src/Statik/Hosting/Impl/HostExporter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Http;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using System.Threading.Tasks.Dataflow;
7 | using Statik.Web;
8 |
9 | namespace Statik.Hosting.Impl
10 | {
11 | public class HostExporter : IHostExporter
12 | {
13 | public async Task Export(IHost host, string destinationDirectory)
14 | {
15 | await PrepareDirectory(destinationDirectory);
16 |
17 | var context = new SemaphoreSlim(1, 1);
18 |
19 | foreach(var page in host.Pages)
20 | {
21 | using(var client = host.CreateClient())
22 | {
23 | var destination = $"{destinationDirectory}{page.Path}";
24 | if (!page.ExtractExactPath)
25 | {
26 | if (string.IsNullOrEmpty(Path.GetExtension(destination)))
27 | {
28 | destination += "/index.html";
29 | }
30 | }
31 | await SaveUrlToFile(client, page, destination, context);
32 | }
33 | }
34 | }
35 |
36 | public async Task ExportParallel(IHost host, string destinationDirectory, int? maxThreads = null)
37 | {
38 | await PrepareDirectory(destinationDirectory);
39 |
40 | if (!maxThreads.HasValue)
41 | {
42 | maxThreads = Environment.ProcessorCount;
43 | }
44 |
45 | if (maxThreads < 0)
46 | {
47 | throw new ArgumentOutOfRangeException(nameof(maxThreads));
48 | }
49 |
50 | var context = new SemaphoreSlim(1, 1);
51 |
52 | var exportPage = new ActionBlock(async page =>
53 | {
54 | using(var client = host.CreateClient())
55 | {
56 | var destination = $"{destinationDirectory}{page.Path}";
57 | if (!page.ExtractExactPath)
58 | {
59 | if (string.IsNullOrEmpty(Path.GetExtension(destination)))
60 | {
61 | destination += "/index.html";
62 | }
63 | }
64 | await SaveUrlToFile(client, page, destination, context);
65 | }
66 | }, new ExecutionDataflowBlockOptions
67 | {
68 | MaxDegreeOfParallelism = maxThreads.Value
69 | });
70 |
71 | foreach (var page in host.Pages)
72 | {
73 | await exportPage.SendAsync(page);
74 | }
75 |
76 | exportPage.Complete();
77 |
78 | await exportPage.Completion;
79 | }
80 |
81 | private async Task PrepareDirectory(string directory)
82 | {
83 | if(!await Task.Run(() => Directory.Exists(directory)))
84 | {
85 | await Task.Run(() => Directory.CreateDirectory(directory));
86 | }
87 |
88 | // Clean all the files currently in the directory
89 | foreach(var fileToDelete in await Task.Run(() => Directory.GetFiles(directory)))
90 | {
91 | await Task.Run(() => File.Delete(fileToDelete));
92 | }
93 | foreach(var directoryToDelete in await Task.Run(() => Directory.GetDirectories(directory)))
94 | {
95 | await Task.Run(() => Directory.Delete(directoryToDelete, true));
96 | }
97 | }
98 |
99 | private async Task SaveUrlToFile(HttpClient client, Page page, string file, SemaphoreSlim context)
100 | {
101 | // Ensure the file's parent directories are created.
102 | var parentDirectory = Path.GetDirectoryName(file);
103 |
104 | // Lock here to prevent multiple threads from creating the same directory.
105 | await context.WaitAsync();
106 | try
107 | {
108 | if (!string.IsNullOrEmpty(parentDirectory))
109 | {
110 | if (!(await Task.Run(() => Directory.Exists(parentDirectory))))
111 | {
112 | await Task.Run(() => Directory.CreateDirectory(parentDirectory));
113 | }
114 | }
115 | }
116 | finally
117 | {
118 | context.Release();
119 | }
120 |
121 | var response = await client.GetAsync(page.Path);
122 | response.EnsureSuccessStatusCode();
123 | using(var requestStream = await response.Content.ReadAsStreamAsync())
124 | {
125 | using(var fileStream = await Task.Run(() => File.OpenWrite(file)))
126 | await requestStream.CopyToAsync(fileStream);
127 | }
128 | }
129 | }
130 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/PageDirectoryLoaderTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using FluentAssertions;
4 | using Microsoft.Extensions.FileProviders;
5 | using Statik.Pages;
6 | using Xunit;
7 |
8 | namespace Statik.Tests
9 | {
10 | public class PageDirectoryLoaderTests : IDisposable
11 | {
12 | readonly IPageDirectoryLoader _pageDirectoryLoader;
13 | readonly string _directory;
14 |
15 | public PageDirectoryLoaderTests()
16 | {
17 | _pageDirectoryLoader = Statik.GetPageDirectoryLoader();
18 | _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Guid.NewGuid().ToString());
19 | Directory.CreateDirectory(_directory);
20 | }
21 |
22 | [Fact]
23 | public void Can_load_with_path_set_correctly()
24 | {
25 | File.WriteAllText(Path.Combine(_directory, "index.md"), "content");
26 | File.WriteAllText(Path.Combine(_directory, "file.md"), "content");
27 | Directory.CreateDirectory(Path.Combine(_directory, "test", "nested"));
28 | File.WriteAllText(Path.Combine(_directory, "test", "nested", "index.md"), "content");
29 | File.WriteAllText(Path.Combine(_directory, "test", "nested", "file2.md"), "content");
30 |
31 | var treeItem = _pageDirectoryLoader.LoadFiles(new PhysicalFileProvider(_directory),
32 | "*.md",
33 | "index.md");
34 |
35 | treeItem.FilePath.Should().Be("/index.md");
36 | treeItem.Children[0].FilePath.Should().Be("/file.md");
37 | treeItem.Children[1].FilePath.Should().Be("/test");
38 | treeItem.Children[1].Children.Count.Should().Be(1);
39 | treeItem.Children[1].Children[0].FilePath.Should().Be("/test/nested/index.md");
40 | treeItem.Children[1].Children[0].Children.Count.Should().Be(1);
41 | treeItem.Children[1].Children[0].Children[0].FilePath.Should().Be("/test/nested/file2.md");
42 | }
43 |
44 | [Fact]
45 | public void Can_load_pages_with_base_path_set_correctly()
46 | {
47 | File.WriteAllText(Path.Combine(_directory, "index.md"), "content");
48 | File.WriteAllText(Path.Combine(_directory, "file.md"), "content");
49 | Directory.CreateDirectory(Path.Combine(_directory, "test"));
50 | File.WriteAllText(Path.Combine(_directory, "test", "index.md"), "content");
51 | File.WriteAllText(Path.Combine(_directory, "test", "file2.md"), "content");
52 |
53 | var treeItem = _pageDirectoryLoader.LoadFiles(new PhysicalFileProvider(_directory),
54 | "*.md",
55 | "index.md");
56 |
57 | treeItem.Path.Should().Be("/");
58 | treeItem.Children[0].Path.Should().Be("/file");
59 | treeItem.Children[1].Path.Should().Be("/test");
60 | treeItem.Children[1].Children[0].Path.Should().Be("/test/file2");
61 | }
62 |
63 | [Fact]
64 | public void Can_set_parent_node_properly()
65 | {
66 | File.WriteAllText(Path.Combine(_directory, "index.md"), "content");
67 | Directory.CreateDirectory(Path.Combine(_directory, "child", "grandchild"));
68 | File.WriteAllText(Path.Combine(_directory, "child", "grandchild", "index.md"), "content");
69 |
70 | var treeItem = _pageDirectoryLoader.LoadFiles(new PhysicalFileProvider(_directory),
71 | "*.md",
72 | "index.md");
73 |
74 | treeItem.Parent.Should().BeNull();
75 | treeItem.Children.Count.Should().Be(1);
76 | treeItem.Children[0].Parent.Should().Be(treeItem);
77 | treeItem.Children[0].Children.Count.Should().Be(1);
78 | treeItem.Children[0].Children[0].Parent.Should().Be(treeItem.Children[0]);
79 | }
80 |
81 | [Fact]
82 | public void Can_create_empty_node_if_intermediate_child_has_no_page()
83 | {
84 | File.WriteAllText(Path.Combine(_directory, "index.md"), "content");
85 | Directory.CreateDirectory(Path.Combine(_directory, "child", "grandchild"));
86 | File.WriteAllText(Path.Combine(_directory, "child", "grandchild", "index.md"), "content");
87 |
88 | var treeItem = _pageDirectoryLoader.LoadFiles(new PhysicalFileProvider(_directory),
89 | "*.md",
90 | "index.md");
91 |
92 | treeItem.Path.Should().Be("/");
93 | treeItem.Children.Count.Should().Be(1);
94 | treeItem.Children[0].Data.IsDirectory.Should().Be(true);
95 | treeItem.Children[0].Data.Name.Should().Be("child");
96 | treeItem.Children[0].Children.Count.Should().Be(1);
97 | treeItem.Children[0].Children[0].Path.Should().Be("/child/grandchild");
98 | }
99 |
100 | [Fact]
101 | public void Can_ignore_directories_with_no_files()
102 | {
103 | File.WriteAllText(Path.Combine(_directory, "index.md"), "content");
104 | Directory.CreateDirectory(Path.Combine(_directory, "child", "grandchild"));
105 |
106 | var treeItem = _pageDirectoryLoader.LoadFiles(new PhysicalFileProvider(_directory),
107 | "*.md",
108 | "index.md");
109 |
110 | treeItem.Path.Should().Be("/");
111 | treeItem.Children.Should().HaveCount(0);
112 | }
113 |
114 | public void Dispose()
115 | {
116 | Directory.Delete(_directory, true);
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/test/Statik.Tests/EmbeddedFileProviderTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using Microsoft.Extensions.FileProviders;
5 | using Moq;
6 | using Statik.Embedded;
7 | using Xunit;
8 |
9 | namespace Statik.Tests
10 | {
11 | public class EmbeddedFileProviderTests
12 | {
13 | private IFileProvider _fileProvider;
14 |
15 | public EmbeddedFileProviderTests()
16 | {
17 | _fileProvider = BuildFileProvider("Statik.Tests.Embedded");
18 | }
19 |
20 | [Fact]
21 | public void Can_get_valid_file()
22 | {
23 | var file = _fileProvider.GetFileInfo("/file1.txt");
24 |
25 | Assert.True(file.Exists);
26 | Assert.Equal("file1.txt", file.Name);
27 | }
28 |
29 | [Fact]
30 | public void Can_get_invalid_file()
31 | {
32 | var file = _fileProvider.GetFileInfo("/non-existant.txt");
33 |
34 | Assert.False(file.Exists);
35 | }
36 |
37 | [Fact]
38 | public void Can_get_valid_directory()
39 | {
40 | var directory = _fileProvider.GetDirectoryContents("/nested");
41 |
42 | Assert.True(directory.Exists);
43 | }
44 |
45 | [Fact]
46 | public void Can_get_invalid_directory()
47 | {
48 | var directory = _fileProvider.GetDirectoryContents("/non-existant");
49 |
50 | Assert.False(directory.Exists);
51 | }
52 |
53 | [Fact]
54 | public void Can_get_valid_directory_with_both_file_and_directory()
55 | {
56 | var directory = _fileProvider.GetDirectoryContents("/");
57 |
58 | Assert.True(directory.Exists);
59 |
60 | var files = directory.ToList();
61 |
62 | Assert.Equal(3, files.Count);
63 |
64 | foreach (var file in files)
65 | {
66 | switch (file.Name)
67 | {
68 | case "file1.txt":
69 | Assert.False(file.IsDirectory);
70 | break;
71 | case "file2.txt":
72 | Assert.False(file.IsDirectory);
73 | break;
74 | case "nested":
75 | Assert.True(file.IsDirectory);
76 | break;
77 | default:
78 | Assert.True(false, $"Invalid file name {file.Name}");
79 | break;
80 | }
81 | }
82 |
83 | directory = _fileProvider.GetDirectoryContents("/nested");
84 |
85 | Assert.True(directory.Exists);
86 |
87 | files = directory.ToList();
88 |
89 | Assert.Equal(3, files.Count);
90 |
91 | foreach (var file in files)
92 | {
93 | switch (file.Name)
94 | {
95 | case "file3.txt":
96 | Assert.False(file.IsDirectory);
97 | break;
98 | case "file4.txt":
99 | Assert.False(file.IsDirectory);
100 | break;
101 | case "nested2":
102 | Assert.True(file.IsDirectory);
103 | break;
104 | default:
105 | Assert.True(false, $"Invalid file name {file.Name}");
106 | break;
107 | }
108 | }
109 | }
110 |
111 | [Fact]
112 | public void Can_get_files_at_more_root_prefix()
113 | {
114 | _fileProvider = BuildFileProvider("Statik.Tests", BuildTestAssemblyResourceResolver());
115 |
116 | var directory = _fileProvider.GetDirectoryContents("/");
117 | Assert.True(directory.Exists);
118 |
119 | var files = directory.ToList();
120 | Assert.Equal(2, files.Count);
121 |
122 | Assert.Equal("Embedded", files[0].Name);
123 | Assert.Equal("Some", files[1].Name);
124 | }
125 |
126 | private IAssemblyResourceResolver BuildTestAssemblyResourceResolver()
127 | {
128 | var assembly = new Mock();
129 | assembly.Setup(x => x.GetManifestResourceNames()).Returns(new[]
130 | {
131 | "Statik.Tests.Embedded.file1.txt",
132 | "Statik.Tests.Embedded.file2.txt",
133 | "Statik.Tests.Embedded.nested.file3.txt",
134 | "Statik.Tests.Embedded.nested.file4.txt",
135 | "Statik.Tests.Embedded.nested.nested2.file5.txt",
136 | "Statik.Tests.Some.Other.Prefix.file1.txt"
137 | });
138 | assembly.Setup(x => x.GetManifestResourceStream(It.IsAny()))
139 | .Returns(new Func(fileName =>
140 | {
141 | var stream = new MemoryStream();
142 | using (var writer = new StreamWriter(stream))
143 | {
144 | writer.Write(fileName);
145 | }
146 |
147 | return stream;
148 | }));
149 | return assembly.Object;
150 | }
151 |
152 | private IFileProvider BuildFileProvider(string prefix, IAssemblyResourceResolver assembly = null)
153 | {
154 | if (assembly == null)
155 | assembly = BuildTestAssemblyResourceResolver();
156 | return new Embedded.EmbeddedFileProvider(assembly, prefix);
157 | }
158 | }
159 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 |
--------------------------------------------------------------------------------
/Statik.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | #
4 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9FD852C3-67B7-46C0-9834-553A3E581B70}"
5 | EndProject
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik", "src\Statik\Statik.csproj", "{748296CB-88BD-43CA-8C67-FC804487139E}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{9492CE6B-0EDD-4016-BB40-628F0955F867}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Tests", "test\Statik.Tests\Statik.Tests.csproj", "{3E5863A4-4C00-4E02-8596-C837C0566DBC}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Files", "src\Statik.Files\Statik.Files.csproj", "{1B5E4387-5F40-45E4-821C-1DB80A876665}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Files.Tests", "test\Statik.Files.Tests\Statik.Files.Tests.csproj", "{51A83061-EE20-4CF8-891F-C04F142E4760}"
15 | EndProject
16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Mvc", "src\Statik.Mvc\Statik.Mvc.csproj", "{529BA049-420B-4B9D-A884-217CEFBEF1C0}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Mvc.Tests", "test\Statik.Mvc.Tests\Statik.Mvc.Tests.csproj", "{5216B3AC-4443-41B9-B2F2-80BD5A757CD3}"
19 | EndProject
20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{CE9B8FB2-637F-4B0C-85C3-4C9C541F325A}"
21 | EndProject
22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Examples.Simple", "examples\Statik.Examples.Simple\Statik.Examples.Simple.csproj", "{6E349214-6AF4-474D-B270-F15C7EB03A47}"
23 | EndProject
24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Examples.Mvc", "examples\Statik.Examples.Mvc\Statik.Examples.Mvc.csproj", "{3EF13426-0E60-4819-B5A1-E89A169CDFA3}"
25 | EndProject
26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Examples.Pages", "examples\Statik.Examples.Pages\Statik.Examples.Pages.csproj", "{33E2DB92-C979-4669-914B-6D707885B401}"
27 | EndProject
28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Statik.Markdown", "src\Statik.Markdown\Statik.Markdown.csproj", "{4528D702-0EE9-462B-9DCE-D34886B9885A}"
29 | EndProject
30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "build\scripts\Build.csproj", "{4496C128-36ED-4799-9614-F30C70C7BA78}"
31 | EndProject
32 | Global
33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
34 | Debug|Any CPU = Debug|Any CPU
35 | Release|Any CPU = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
38 | {748296CB-88BD-43CA-8C67-FC804487139E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {748296CB-88BD-43CA-8C67-FC804487139E}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {748296CB-88BD-43CA-8C67-FC804487139E}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {748296CB-88BD-43CA-8C67-FC804487139E}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {3E5863A4-4C00-4E02-8596-C837C0566DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {3E5863A4-4C00-4E02-8596-C837C0566DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {3E5863A4-4C00-4E02-8596-C837C0566DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {3E5863A4-4C00-4E02-8596-C837C0566DBC}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {1B5E4387-5F40-45E4-821C-1DB80A876665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {1B5E4387-5F40-45E4-821C-1DB80A876665}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {1B5E4387-5F40-45E4-821C-1DB80A876665}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {1B5E4387-5F40-45E4-821C-1DB80A876665}.Release|Any CPU.Build.0 = Release|Any CPU
50 | {51A83061-EE20-4CF8-891F-C04F142E4760}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {51A83061-EE20-4CF8-891F-C04F142E4760}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {51A83061-EE20-4CF8-891F-C04F142E4760}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {51A83061-EE20-4CF8-891F-C04F142E4760}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {529BA049-420B-4B9D-A884-217CEFBEF1C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55 | {529BA049-420B-4B9D-A884-217CEFBEF1C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
56 | {529BA049-420B-4B9D-A884-217CEFBEF1C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
57 | {529BA049-420B-4B9D-A884-217CEFBEF1C0}.Release|Any CPU.Build.0 = Release|Any CPU
58 | {5216B3AC-4443-41B9-B2F2-80BD5A757CD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59 | {5216B3AC-4443-41B9-B2F2-80BD5A757CD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
60 | {5216B3AC-4443-41B9-B2F2-80BD5A757CD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
61 | {5216B3AC-4443-41B9-B2F2-80BD5A757CD3}.Release|Any CPU.Build.0 = Release|Any CPU
62 | {6E349214-6AF4-474D-B270-F15C7EB03A47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63 | {6E349214-6AF4-474D-B270-F15C7EB03A47}.Debug|Any CPU.Build.0 = Debug|Any CPU
64 | {6E349214-6AF4-474D-B270-F15C7EB03A47}.Release|Any CPU.ActiveCfg = Release|Any CPU
65 | {6E349214-6AF4-474D-B270-F15C7EB03A47}.Release|Any CPU.Build.0 = Release|Any CPU
66 | {3EF13426-0E60-4819-B5A1-E89A169CDFA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67 | {3EF13426-0E60-4819-B5A1-E89A169CDFA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
68 | {3EF13426-0E60-4819-B5A1-E89A169CDFA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
69 | {3EF13426-0E60-4819-B5A1-E89A169CDFA3}.Release|Any CPU.Build.0 = Release|Any CPU
70 | {33E2DB92-C979-4669-914B-6D707885B401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71 | {33E2DB92-C979-4669-914B-6D707885B401}.Debug|Any CPU.Build.0 = Debug|Any CPU
72 | {33E2DB92-C979-4669-914B-6D707885B401}.Release|Any CPU.ActiveCfg = Release|Any CPU
73 | {33E2DB92-C979-4669-914B-6D707885B401}.Release|Any CPU.Build.0 = Release|Any CPU
74 | {4528D702-0EE9-462B-9DCE-D34886B9885A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75 | {4528D702-0EE9-462B-9DCE-D34886B9885A}.Debug|Any CPU.Build.0 = Debug|Any CPU
76 | {4528D702-0EE9-462B-9DCE-D34886B9885A}.Release|Any CPU.ActiveCfg = Release|Any CPU
77 | {4528D702-0EE9-462B-9DCE-D34886B9885A}.Release|Any CPU.Build.0 = Release|Any CPU
78 | {4496C128-36ED-4799-9614-F30C70C7BA78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79 | {4496C128-36ED-4799-9614-F30C70C7BA78}.Debug|Any CPU.Build.0 = Debug|Any CPU
80 | {4496C128-36ED-4799-9614-F30C70C7BA78}.Release|Any CPU.ActiveCfg = Release|Any CPU
81 | {4496C128-36ED-4799-9614-F30C70C7BA78}.Release|Any CPU.Build.0 = Release|Any CPU
82 | EndGlobalSection
83 | GlobalSection(NestedProjects) = preSolution
84 | {748296CB-88BD-43CA-8C67-FC804487139E} = {9FD852C3-67B7-46C0-9834-553A3E581B70}
85 | {3E5863A4-4C00-4E02-8596-C837C0566DBC} = {9492CE6B-0EDD-4016-BB40-628F0955F867}
86 | {1B5E4387-5F40-45E4-821C-1DB80A876665} = {9FD852C3-67B7-46C0-9834-553A3E581B70}
87 | {51A83061-EE20-4CF8-891F-C04F142E4760} = {9492CE6B-0EDD-4016-BB40-628F0955F867}
88 | {529BA049-420B-4B9D-A884-217CEFBEF1C0} = {9FD852C3-67B7-46C0-9834-553A3E581B70}
89 | {5216B3AC-4443-41B9-B2F2-80BD5A757CD3} = {9492CE6B-0EDD-4016-BB40-628F0955F867}
90 | {6E349214-6AF4-474D-B270-F15C7EB03A47} = {CE9B8FB2-637F-4B0C-85C3-4C9C541F325A}
91 | {3EF13426-0E60-4819-B5A1-E89A169CDFA3} = {CE9B8FB2-637F-4B0C-85C3-4C9C541F325A}
92 | {33E2DB92-C979-4669-914B-6D707885B401} = {CE9B8FB2-637F-4B0C-85C3-4C9C541F325A}
93 | {4528D702-0EE9-462B-9DCE-D34886B9885A} = {9FD852C3-67B7-46C0-9834-553A3E581B70}
94 | EndGlobalSection
95 | EndGlobal
96 |
--------------------------------------------------------------------------------
/src/Statik/Web/Impl/WebBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Builder;
7 | using Microsoft.AspNetCore.Hosting;
8 | using Microsoft.AspNetCore.Http;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using Statik.Hosting;
11 |
12 | namespace Statik.Web.Impl
13 | {
14 | public class WebBuilder : IWebBuilder
15 | {
16 | readonly Dictionary _pages = new Dictionary();
17 | readonly List> _serviceActions = new List>();
18 | readonly IHostBuilder _hostBuilder;
19 |
20 | public WebBuilder(IHostBuilder hostBuilder)
21 | {
22 | _hostBuilder = hostBuilder;
23 | }
24 |
25 | public void Register(string path, Func action, object state, bool extractExactPath)
26 | {
27 | _pages.Add(path, new Page(path, action, state, extractExactPath));
28 | }
29 |
30 | public void RegisterServices(Action action)
31 | {
32 | _serviceActions.Add(action);
33 | }
34 |
35 | public Hosting.IWebHost BuildWebHost(string appBase = null, int port = StatikDefaults.DefaultPort)
36 | {
37 | return _hostBuilder.BuildWebHost(
38 | port,
39 | appBase,
40 | new HostModule(
41 | appBase,
42 | _pages.ToDictionary(x => x.Key, x => x.Value),
43 | _serviceActions.ToList()));
44 | }
45 |
46 | public IVirtualHost BuildVirtualHost(string appBase = null)
47 | {
48 | return _hostBuilder.BuildVirtualHost(
49 | appBase,
50 | new HostModule(
51 | appBase,
52 | _pages.ToDictionary(x => x.Key, x => x.Value),
53 | _serviceActions.ToList()));
54 | }
55 |
56 | class PageAccessor : IPageAccessor
57 | {
58 | public PageAccessor(Page page)
59 | {
60 | Page = page;
61 | }
62 |
63 | public Page Page { get; }
64 | }
65 |
66 | class HostModule : IHostModule, IPageRegistry
67 | {
68 | readonly PathString _appBase;
69 | readonly Dictionary _pages;
70 | readonly List> _serviceActions;
71 |
72 | public HostModule(
73 | string appBase,
74 | Dictionary pages,
75 | List> serviceActions)
76 | {
77 | _appBase = appBase;
78 | _pages = pages;
79 | _serviceActions = serviceActions;
80 | Pages = new ReadOnlyCollection(_pages.Values.ToList());
81 | }
82 |
83 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
84 | {
85 | app.Use(async (context, next) => {
86 |
87 | async Task AttemptRunPage(HttpContext c)
88 | {
89 | _pages.TryGetValue(c.Request.Path, out var page);
90 | if (page == null) return false;
91 | context.Items["_statikPageAccessor"] = new PageAccessor(page);
92 | await page.Action(context);
93 | context.Items["_statikPageAccessor"] = null;
94 | return true;
95 | }
96 |
97 | if (!_appBase.HasValue)
98 | {
99 | // No app base configured, just find and execute our page.
100 | if (!await AttemptRunPage(context))
101 | {
102 | await next();
103 | }
104 | }
105 | else
106 | {
107 | // Only serve requests at the app base.
108 | if (context.Request.Path.StartsWithSegments(_appBase, out var matchedPath, out var remainingPath))
109 | {
110 | var originalPath = context.Request.Path;
111 | var originalPathBase = context.Request.PathBase;
112 | context.Request.Path = remainingPath;
113 | context.Request.PathBase = originalPathBase.Add(matchedPath);
114 |
115 | bool ran;
116 |
117 | try
118 | {
119 | ran = await AttemptRunPage(context);
120 | }
121 | finally
122 | {
123 | context.Request.Path = originalPath;
124 | context.Request.PathBase = originalPathBase;
125 | }
126 |
127 | if (!ran)
128 | {
129 | await next();
130 | }
131 | }
132 | else
133 | {
134 | await next();
135 | }
136 | }
137 | });
138 | }
139 |
140 | public void ConfigureServices(IServiceCollection services)
141 | {
142 | services.AddSingleton(this);
143 | foreach(var serviceAction in _serviceActions)
144 | {
145 | serviceAction(services);
146 | }
147 | }
148 |
149 | public IReadOnlyCollection Pages { get; }
150 |
151 | public IReadOnlyCollection GetPaths()
152 | {
153 | return new ReadOnlyCollection(Pages.Select(x => x.Path).ToList());
154 | }
155 |
156 | public Page GetPage(string path)
157 | {
158 | return _pages.TryGetValue(path, out Page page) ? page : null;
159 | }
160 |
161 | public Page FindOne(PageMatchDelegate match)
162 | {
163 | foreach (var value in _pages.Values)
164 | {
165 | if (match(value)) return value;
166 | }
167 |
168 | return null;
169 | }
170 |
171 | public async Task FindOne(PageMatchDelegateAsync match)
172 | {
173 | foreach (var value in _pages.Values)
174 | {
175 | if (await match(value)) return value;
176 | }
177 |
178 | return null;
179 | }
180 |
181 | public List FindMany(PageMatchDelegate match)
182 | {
183 | var result = new List();
184 |
185 | foreach (var value in _pages.Values)
186 | {
187 | if(match(value)) result.Add(value);
188 | }
189 |
190 | return result;
191 | }
192 |
193 | public async Task> FindMany(PageMatchDelegateAsync match)
194 | {
195 | var result = new List();
196 |
197 | foreach (var value in _pages.Values)
198 | {
199 | if(await match(value)) result.Add(value);
200 | }
201 |
202 | return result;
203 | }
204 |
205 | public void ForEach(PageActionDelegate action)
206 | {
207 | foreach (var value in _pages.Values)
208 | {
209 | action(value);
210 | }
211 | }
212 |
213 | public async Task ForEach(PageActionDelegateAsync action)
214 | {
215 | foreach (var value in _pages.Values)
216 | {
217 | await action(value);
218 | }
219 | }
220 | }
221 | }
222 | }
--------------------------------------------------------------------------------
/test/Statik.Files.Tests/FileProviderTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.Extensions.FileProviders;
6 | using Microsoft.Extensions.FileSystemGlobbing;
7 | using Moq;
8 | using Statik.Hosting;
9 | using Statik.Hosting.Impl;
10 | using Statik.Tests;
11 | using Statik.Web;
12 | using Statik.Web.Impl;
13 | using Statik.Files;
14 | using Xunit;
15 |
16 | namespace Statik.Files.Tests
17 | {
18 | public class FileProviderTests
19 | {
20 | readonly IWebBuilder _webBuilder;
21 |
22 | public FileProviderTests()
23 | {
24 | _webBuilder = new WebBuilder(new HostBuilder());
25 | }
26 |
27 | [Fact]
28 | public void Can_register_files()
29 | {
30 | using (var testDirectory = new WorkingDirectorySession())
31 | {
32 | File.WriteAllText(Path.Combine(testDirectory.Directory, "file1.txt"), "test");
33 | File.WriteAllText(Path.Combine(testDirectory.Directory, "file2.txt"), "test");
34 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested"));
35 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "file3.txt"), "test");
36 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "file4.txt"), "test");
37 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested", "nested2"));
38 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "nested2", "file5.txt"), "test");
39 |
40 | var webBuilder = new Mock();
41 |
42 | webBuilder.Setup(x => x.Register("/file1.txt", It.IsAny>(), null, true));
43 | webBuilder.Setup(x => x.Register("/file2.txt", It.IsAny>(), null, true));
44 | webBuilder.Setup(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true));
45 | webBuilder.Setup(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true));
46 | webBuilder.Setup(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true));
47 |
48 | webBuilder.Object.RegisterDirectory(testDirectory.Directory);
49 |
50 | webBuilder.Verify(x => x.Register("/file1.txt", It.IsAny>(), null, true), Times.Exactly(1));
51 | webBuilder.Verify(x => x.Register("/file2.txt", It.IsAny>(), null, true), Times.Exactly(1));
52 | webBuilder.Verify(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true), Times.Exactly(1));
53 | webBuilder.Verify(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true), Times.Exactly(1));
54 | webBuilder.Verify(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true), Times.Exactly(1));
55 | }
56 | }
57 |
58 | [Fact]
59 | public void Can_register_files_with_include()
60 | {
61 | using (var testDirectory = new WorkingDirectorySession())
62 | {
63 | File.WriteAllText(Path.Combine(testDirectory.Directory, "file1.txt"), "test");
64 | File.WriteAllText(Path.Combine(testDirectory.Directory, "file2.txt"), "test");
65 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested"));
66 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "file3.txt"), "test");
67 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "file4.txt"), "test");
68 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested", "nested2"));
69 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "nested2", "file5.txt"), "test");
70 |
71 | var webBuilder = new Mock();
72 | var options = new RegisterOptions();
73 | options.Matcher = new Matcher();
74 | options.Matcher.AddInclude("**/file3.*");
75 |
76 | webBuilder.Setup(x => x.Register("/file1.txt", It.IsAny>(), null, true));
77 | webBuilder.Setup(x => x.Register("/file2.txt", It.IsAny>(), null, true));
78 | webBuilder.Setup(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true));
79 | webBuilder.Setup(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true));
80 | webBuilder.Setup(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true));
81 |
82 | webBuilder.Object.RegisterDirectory(testDirectory.Directory, options);
83 |
84 | webBuilder.Verify(x => x.Register("/file1.txt", It.IsAny>(), null, true), Times.Never);
85 | webBuilder.Verify(x => x.Register("/file2.txt", It.IsAny>(), null, true), Times.Never);
86 | webBuilder.Verify(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true), Times.Exactly(1));
87 | webBuilder.Verify(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true), Times.Never);
88 | webBuilder.Verify(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true), Times.Never);
89 | }
90 | }
91 |
92 | [Fact]
93 | public void Can_register_files_with_exclude()
94 | {
95 | using (var testDirectory = new WorkingDirectorySession())
96 | {
97 | File.WriteAllText(Path.Combine(testDirectory.Directory, "file1.txt"), "test");
98 | File.WriteAllText(Path.Combine(testDirectory.Directory, "file2.txt"), "test");
99 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested"));
100 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "file3.txt"), "test");
101 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "file4.txt"), "test");
102 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested", "nested2"));
103 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "nested2", "file5.txt"), "test");
104 |
105 | var webBuilder = new Mock();
106 | var options = new RegisterOptions();
107 | options.Matcher = new Matcher();
108 | options.Matcher.AddInclude("**/*");
109 | options.Matcher.AddExclude("**/file3.*");
110 |
111 | webBuilder.Setup(x => x.Register("/file1.txt", It.IsAny>(), null, true));
112 | webBuilder.Setup(x => x.Register("/file2.txt", It.IsAny>(), null, true));
113 | webBuilder.Setup(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true));
114 | webBuilder.Setup(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true));
115 | webBuilder.Setup(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true));
116 |
117 | webBuilder.Object.RegisterDirectory(testDirectory.Directory, options);
118 |
119 | webBuilder.Verify(x => x.Register("/file1.txt", It.IsAny>(), null, true), Times.Exactly(1));
120 | webBuilder.Verify(x => x.Register("/file2.txt", It.IsAny>(), null, true), Times.Exactly(1));
121 | webBuilder.Verify(x => x.Register("/nested/file3.txt", It.IsAny>(), null, true), Times.Never);
122 | webBuilder.Verify(x => x.Register("/nested/file4.txt", It.IsAny>(), null, true), Times.Exactly(1));
123 | webBuilder.Verify(x => x.Register("/nested/nested2/file5.txt", It.IsAny>(), null, true), Times.Exactly(1));
124 | }
125 | }
126 |
127 | [Fact]
128 | public async Task Can_serve_files()
129 | {
130 | using (var testDirectory = new WorkingDirectorySession())
131 | {
132 | File.WriteAllText(Path.Combine(testDirectory.Directory, "test.txt"), "test");
133 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested"));
134 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "test2.txt"), "test2");
135 |
136 | _webBuilder.RegisterDirectory(testDirectory.Directory);
137 |
138 | using(var host = _webBuilder.BuildVirtualHost())
139 | {
140 | using(var client = host.CreateClient())
141 | {
142 | var responseMessage = await client.GetAsync("/test.txt");
143 | responseMessage.EnsureSuccessStatusCode();
144 | var response = await responseMessage.Content.ReadAsStringAsync();
145 |
146 | Assert.Equal("test", response);
147 |
148 | responseMessage = await client.GetAsync("/nested/test2.txt");
149 | responseMessage.EnsureSuccessStatusCode();
150 | response = await responseMessage.Content.ReadAsStringAsync();
151 |
152 | Assert.Equal("test2", response);
153 | }
154 | }
155 |
156 | using(var host = _webBuilder.BuildVirtualHost("/appbase"))
157 | {
158 | using(var client = host.CreateClient())
159 | {
160 | var responseMessage = await client.GetAsync("/test.txt");
161 | responseMessage.EnsureSuccessStatusCode();
162 | var response = await responseMessage.Content.ReadAsStringAsync();
163 |
164 | Assert.Equal("test", response);
165 |
166 | responseMessage = await client.GetAsync("/nested/test2.txt");
167 | responseMessage.EnsureSuccessStatusCode();
168 | response = await responseMessage.Content.ReadAsStringAsync();
169 |
170 | Assert.Equal("test2", response);
171 | }
172 | }
173 | }
174 | }
175 |
176 | [Fact]
177 | public async Task Can_serve_files_at_path()
178 | {
179 | using (var testDirectory = new WorkingDirectorySession())
180 | {
181 | File.WriteAllText(Path.Combine(testDirectory.Directory, "test.txt"), "test");
182 | Directory.CreateDirectory(Path.Combine(testDirectory.Directory, "nested"));
183 | File.WriteAllText(Path.Combine(testDirectory.Directory, "nested", "test2.txt"), "test2");
184 |
185 | _webBuilder.RegisterDirectory("/prefix", testDirectory.Directory);
186 |
187 | using(var host = _webBuilder.BuildVirtualHost())
188 | {
189 | using(var client = host.CreateClient())
190 | {
191 | var responseMessage = await client.GetAsync("/prefix/test.txt");
192 | responseMessage.EnsureSuccessStatusCode();
193 | var response = await responseMessage.Content.ReadAsStringAsync();
194 |
195 | Assert.Equal("test", response);
196 |
197 | responseMessage = await client.GetAsync("/prefix/nested/test2.txt");
198 | responseMessage.EnsureSuccessStatusCode();
199 | response = await responseMessage.Content.ReadAsStringAsync();
200 |
201 | Assert.Equal("test2", response);
202 | }
203 | }
204 |
205 | using(var host = _webBuilder.BuildVirtualHost("/appbase"))
206 | {
207 | using(var client = host.CreateClient())
208 | {
209 | var responseMessage = await client.GetAsync("/prefix/test.txt");
210 | responseMessage.EnsureSuccessStatusCode();
211 | var response = await responseMessage.Content.ReadAsStringAsync();
212 |
213 | Assert.Equal("test", response);
214 |
215 | responseMessage = await client.GetAsync("/prefix/nested/test2.txt");
216 | responseMessage.EnsureSuccessStatusCode();
217 | response = await responseMessage.Content.ReadAsStringAsync();
218 |
219 | Assert.Equal("test2", response);
220 | }
221 | }
222 | }
223 | }
224 | }
225 | }
--------------------------------------------------------------------------------