Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ A Blazor-based static site generator
```csharp
using ScissorHands.Web;

var app = await new ScissorHandsApplication<MainLayout, IndexView, PostView, PageView>(args)
.VerifyCommandArguments()
.BuildAsync();
var app = new ScissorHandsApplicationBuilder(args)
.AddLayouts<MainLayout, IndexView, PostView, PageView, NotFoundView>()
.Build();
await app.RunAsync();
```

Expand Down
10 changes: 5 additions & 5 deletions src/ScissorHands.Theme/IndexViewBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ public abstract class IndexViewBase : ComponentBase
/// <summary>
/// Gets or sets the list of <see cref="ContentDocument"/> instances.
/// </summary>
[Parameter]
public IEnumerable<ContentDocument> Documents { get; set; } = [];
[CascadingParameter]
public IEnumerable<ContentDocument>? Documents { get; set; }

/// <summary>
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
/// </summary>
[Parameter]
[CascadingParameter]
public IEnumerable<PluginManifest>? Plugins { get; set; }

/// <summary>
/// Gets or sets the <see cref="ThemeManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public ThemeManifest? Theme { get; set; }

/// <summary>
/// Gets or sets the <see cref="SiteManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public SiteManifest? Site { get; set; }
}
10 changes: 5 additions & 5 deletions src/ScissorHands.Theme/NotFoundViewBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,24 @@ public abstract class NotFoundViewBase : ComponentBase
/// Gets or sets the <see cref="ContentDocument"/> instance.
/// If a page document with the slug <c>404.html</c> exists, it will be provided here.
/// </summary>
[Parameter]
public ContentDocument Document { get; set; } = new();
[CascadingParameter]
public ContentDocument? Document { get; set; }

/// <summary>
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
/// </summary>
[Parameter]
[CascadingParameter]
public IEnumerable<PluginManifest>? Plugins { get; set; }

/// <summary>
/// Gets or sets the <see cref="ThemeManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public ThemeManifest? Theme { get; set; }

/// <summary>
/// Gets or sets the <see cref="SiteManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public SiteManifest? Site { get; set; }
}
10 changes: 5 additions & 5 deletions src/ScissorHands.Theme/PageViewBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ public abstract class PageViewBase : ComponentBase
/// <summary>
/// Gets or sets the <see cref="ContentDocument"/> instance.
/// </summary>
[Parameter]
public ContentDocument Document { get; set; } = new();
[CascadingParameter]
public ContentDocument? Document { get; set; }

/// <summary>
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
/// </summary>
[Parameter]
[CascadingParameter]
public IEnumerable<PluginManifest>? Plugins { get; set; }

/// <summary>
/// Gets or sets the <see cref="ThemeManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public ThemeManifest? Theme { get; set; }

/// <summary>
/// Gets or sets the <see cref="SiteManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public SiteManifest? Site { get; set; }
}
10 changes: 5 additions & 5 deletions src/ScissorHands.Theme/PostViewBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ public abstract class PostViewBase : ComponentBase
/// <summary>
/// Gets or sets the <see cref="ContentDocument"/> instance.
/// </summary>
[Parameter]
public ContentDocument Document { get; set; } = new();
[CascadingParameter]
public ContentDocument? Document { get; set; }

/// <summary>
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
/// </summary>
[Parameter]
[CascadingParameter]
public IEnumerable<PluginManifest>? Plugins { get; set; }

/// <summary>
/// Gets or sets the <see cref="ThemeManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public ThemeManifest? Theme { get; set; }

/// <summary>
/// Gets or sets the <see cref="SiteManifest"/> instance.
/// </summary>
[Parameter]
[CascadingParameter]
public SiteManifest? Site { get; set; }
}
76 changes: 42 additions & 34 deletions src/ScissorHands.Web/Generators/StaticSiteGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public sealed class StaticSiteGenerator(
IPluginRunner pluginRunner,
IThemeService themeService,
IComponentRenderer renderer,
IAppPaths paths,
IFileSystem fileSystem,
IAppPaths paths,
IFileSystem fileSystem,
SiteManifest options,
ILogger<StaticSiteGenerator> logger) : IStaticSiteGenerator
{
Expand Down Expand Up @@ -69,39 +69,9 @@ public async Task BuildAsync<TMainLayout, TIndexView, TPostView, TPageView, TNot
var notFoundDocument = documents.SingleOrDefault(d => d.Kind == ContentKind.Page && string.Equals(d.Metadata.Slug, PAGE_NOT_FOUND_SLUG, StringComparison.OrdinalIgnoreCase));
await RenderNotFoundAsync<TNotFoundView>(notFoundDocument, plugins, theme, destination, layoutType, cancellationToken);

foreach (var document in documents)
foreach (var document in documents.Where(d => IsNotFoundPage(d) == false))
{
cancellationToken.ThrowIfCancellationRequested();

if (document.Kind == ContentKind.Page && string.Equals(document.Metadata.Slug, PAGE_NOT_FOUND_SLUG, StringComparison.OrdinalIgnoreCase))
{
continue;
}

var preProcessed = await _pluginRunner.RunPreMarkdownAsync(document, cancellationToken);
var html = await _markdownService.ToHtmlAsync(preProcessed.Markdown, cancellationToken: cancellationToken);
preProcessed.Html = html;
var postMarkdown = await _pluginRunner.RunPostMarkdownAsync(preProcessed, cancellationToken);

var parameters = new Dictionary<string, object?>
{
["Document"] = postMarkdown,
["Plugins"] = plugins,
["Theme"] = theme,
["Site"] = _options
};

var rendered = postMarkdown.Kind switch
{
ContentKind.Page => await _renderer.RenderAsync<TPageView>(layoutType, parameters, cancellationToken),
_ => await _renderer.RenderAsync<TPostView>(layoutType, parameters, cancellationToken)
};

var finalHtml = await _pluginRunner.RunPostHtmlAsync(rendered, postMarkdown, cancellationToken);
var outputPath = ResolveOutputPath(destination, postMarkdown.Metadata.Slug);
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(outputPath)!);
await _fileSystem.File.WriteAllTextAsync(outputPath, finalHtml, Encoding.UTF8, cancellationToken);
_logger.LogInformation("Wrote {OutputPath}", outputPath);
await RenderDocumentAsync<TPostView, TPageView>(document, plugins, theme, destination, layoutType, cancellationToken);
}

CopyContentAssets(destination);
Expand Down Expand Up @@ -190,6 +160,44 @@ private async Task RenderNotFoundAsync<TNotFoundView>(ContentDocument? notFoundD
_logger.LogInformation("Wrote {OutputPath}", outputPath);
}

private async Task RenderDocumentAsync<TPostView, TPageView>(ContentDocument document, IEnumerable<PluginManifest> plugins, ThemeManifest theme, string destination, Type layoutType, CancellationToken cancellationToken)
where TPostView : ScissorHands.Theme.PostViewBase
where TPageView : ScissorHands.Theme.PageViewBase
{
cancellationToken.ThrowIfCancellationRequested();

var preProcessed = await _pluginRunner.RunPreMarkdownAsync(document, cancellationToken);
var html = await _markdownService.ToHtmlAsync(preProcessed.Markdown, cancellationToken: cancellationToken);
preProcessed.Html = html;
var postMarkdown = await _pluginRunner.RunPostMarkdownAsync(preProcessed, cancellationToken);

var parameters = new Dictionary<string, object?>
{
["Document"] = postMarkdown,
["Plugins"] = plugins,
["Theme"] = theme,
["Site"] = _options
};

var rendered = postMarkdown.Kind switch
{
ContentKind.Page => await _renderer.RenderAsync<TPageView>(layoutType, parameters, cancellationToken),
_ => await _renderer.RenderAsync<TPostView>(layoutType, parameters, cancellationToken)
};

var finalHtml = await _pluginRunner.RunPostHtmlAsync(rendered, postMarkdown, cancellationToken);
var outputPath = ResolveOutputPath(destination, postMarkdown.Metadata.Slug);
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(outputPath)!);
await _fileSystem.File.WriteAllTextAsync(outputPath, finalHtml, Encoding.UTF8, cancellationToken);
_logger.LogInformation("Wrote {OutputPath}", outputPath);
}

private static bool IsNotFoundPage(ContentDocument document)
{
return document.Kind == ContentKind.Page &&
string.Equals(document.Metadata.Slug, PAGE_NOT_FOUND_SLUG, StringComparison.OrdinalIgnoreCase) == true;
}

private static string ResolveOutputPath(string root, string slug)
{
if (string.IsNullOrWhiteSpace(slug))
Expand Down
43 changes: 43 additions & 0 deletions src/ScissorHands.Web/Renderers/ComponentRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
using System.Reflection;

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

using ScissorHands.Theme;

namespace ScissorHands.Web.Renderers;

/// <summary>
Expand All @@ -15,6 +19,39 @@ public sealed class ComponentRenderer(IServiceScopeFactory scopeFactory, ILogger
private readonly IServiceScopeFactory _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
private readonly ILoggerFactory _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));

// Lazy initialization of cascading parameter names discovered via reflection
private static readonly Lazy<HashSet<string>> _cascadingParameterNames = new(() =>
{
var parameterNames = new HashSet<string>(StringComparer.Ordinal);

// Discover all types in the ScissorHands.Theme assembly that have cascading parameters
var themeAssembly = typeof(PageViewBase).Assembly;

try
{
var allTypes = themeAssembly.GetExportedTypes();

foreach (var type in allTypes)
{
// Find all properties with CascadingParameter attribute
var cascadingProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
.Where(p => p.GetCustomAttribute<CascadingParameterAttribute>() is not null);

foreach (var property in cascadingProperties)
{
parameterNames.Add(property.Name);
}
}
}
catch (ReflectionTypeLoadException)
{
// If type loading fails, fall back to empty set
// This should not happen in normal operation, but provides safety
}

return parameterNames;
});

/// <inheritdoc />
public async Task<string> RenderAsync<TComponent>(Type layoutType, IDictionary<string, object?> parameters, CancellationToken cancellationToken = default)
where TComponent : IComponent
Expand All @@ -31,8 +68,14 @@ public async Task<string> RenderAsync<TComponent>(Type layoutType, IDictionary<s
{
builder.OpenComponent<TComponent>(0);
var seq = 1;

foreach (var kvp in parameters)
{
if (_cascadingParameterNames.Value.Contains(kvp.Key))
{
continue;
}

builder.AddAttribute(seq++, kvp.Key, kvp.Value);
}
builder.CloseComponent();
Expand Down
Loading