Skip to content

Commit f0d94b8

Browse files
justinyooCopilotCopilot
authored
Add cascading parameters for theme views (#74)
* Update to cascading parameters * Refactor StaticSiteGenerator * Refactor ScissorHandsApplication to builder * Add more tests * Update README * Update src/ScissorHands.Web/Generators/StaticSiteGenerator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix null-checking logic in IndexView.razor (#76) * Initial plan * Fix null-checking logic in IndexView.razor Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> * Use reflection to discover cascading parameters in ComponentRenderer (#75) * Initial plan * Refactor ComponentRenderer to use reflection for cascading parameter detection Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> * Improve reflection to auto-discover cascading parameters from entire Theme assembly Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> * Add error handling and use GetExportedTypes for safer reflection Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> * Improve reflection-based method invocation with caching and error handling (#77) * Initial plan * Add robust error handling and caching for reflection-based method invocation Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> * Add thread-safety and improve exception handling Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> * Address final code review feedback - constants and stack trace preservation Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: justinyoo <1538528+justinyoo@users.noreply.github.com>
1 parent 5aa0ada commit f0d94b8

20 files changed

Lines changed: 609 additions & 131 deletions

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ A Blazor-based static site generator
2828
```csharp
2929
using ScissorHands.Web;
3030
31-
var app = await new ScissorHandsApplication<MainLayout, IndexView, PostView, PageView>(args)
32-
.VerifyCommandArguments()
33-
.BuildAsync();
31+
var app = new ScissorHandsApplicationBuilder(args)
32+
.AddLayouts<MainLayout, IndexView, PostView, PageView, NotFoundView>()
33+
.Build();
3434
await app.RunAsync();
3535
```
3636

src/ScissorHands.Theme/IndexViewBase.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@ public abstract class IndexViewBase : ComponentBase
1313
/// <summary>
1414
/// Gets or sets the list of <see cref="ContentDocument"/> instances.
1515
/// </summary>
16-
[Parameter]
17-
public IEnumerable<ContentDocument> Documents { get; set; } = [];
16+
[CascadingParameter]
17+
public IEnumerable<ContentDocument>? Documents { get; set; }
1818

1919
/// <summary>
2020
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
2121
/// </summary>
22-
[Parameter]
22+
[CascadingParameter]
2323
public IEnumerable<PluginManifest>? Plugins { get; set; }
2424

2525
/// <summary>
2626
/// Gets or sets the <see cref="ThemeManifest"/> instance.
2727
/// </summary>
28-
[Parameter]
28+
[CascadingParameter]
2929
public ThemeManifest? Theme { get; set; }
3030

3131
/// <summary>
3232
/// Gets or sets the <see cref="SiteManifest"/> instance.
3333
/// </summary>
34-
[Parameter]
34+
[CascadingParameter]
3535
public SiteManifest? Site { get; set; }
3636
}

src/ScissorHands.Theme/NotFoundViewBase.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,24 @@ public abstract class NotFoundViewBase : ComponentBase
1414
/// Gets or sets the <see cref="ContentDocument"/> instance.
1515
/// If a page document with the slug <c>404.html</c> exists, it will be provided here.
1616
/// </summary>
17-
[Parameter]
18-
public ContentDocument Document { get; set; } = new();
17+
[CascadingParameter]
18+
public ContentDocument? Document { get; set; }
1919

2020
/// <summary>
2121
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
2222
/// </summary>
23-
[Parameter]
23+
[CascadingParameter]
2424
public IEnumerable<PluginManifest>? Plugins { get; set; }
2525

2626
/// <summary>
2727
/// Gets or sets the <see cref="ThemeManifest"/> instance.
2828
/// </summary>
29-
[Parameter]
29+
[CascadingParameter]
3030
public ThemeManifest? Theme { get; set; }
3131

3232
/// <summary>
3333
/// Gets or sets the <see cref="SiteManifest"/> instance.
3434
/// </summary>
35-
[Parameter]
35+
[CascadingParameter]
3636
public SiteManifest? Site { get; set; }
3737
}

src/ScissorHands.Theme/PageViewBase.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@ public abstract class PageViewBase : ComponentBase
1313
/// <summary>
1414
/// Gets or sets the <see cref="ContentDocument"/> instance.
1515
/// </summary>
16-
[Parameter]
17-
public ContentDocument Document { get; set; } = new();
16+
[CascadingParameter]
17+
public ContentDocument? Document { get; set; }
1818

1919
/// <summary>
2020
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
2121
/// </summary>
22-
[Parameter]
22+
[CascadingParameter]
2323
public IEnumerable<PluginManifest>? Plugins { get; set; }
2424

2525
/// <summary>
2626
/// Gets or sets the <see cref="ThemeManifest"/> instance.
2727
/// </summary>
28-
[Parameter]
28+
[CascadingParameter]
2929
public ThemeManifest? Theme { get; set; }
3030

3131
/// <summary>
3232
/// Gets or sets the <see cref="SiteManifest"/> instance.
3333
/// </summary>
34-
[Parameter]
34+
[CascadingParameter]
3535
public SiteManifest? Site { get; set; }
3636
}

src/ScissorHands.Theme/PostViewBase.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,24 @@ public abstract class PostViewBase : ComponentBase
1313
/// <summary>
1414
/// Gets or sets the <see cref="ContentDocument"/> instance.
1515
/// </summary>
16-
[Parameter]
17-
public ContentDocument Document { get; set; } = new();
16+
[CascadingParameter]
17+
public ContentDocument? Document { get; set; }
1818

1919
/// <summary>
2020
/// Gets or sets the list of <see cref="PluginManifest"/> instances.
2121
/// </summary>
22-
[Parameter]
22+
[CascadingParameter]
2323
public IEnumerable<PluginManifest>? Plugins { get; set; }
2424

2525
/// <summary>
2626
/// Gets or sets the <see cref="ThemeManifest"/> instance.
2727
/// </summary>
28-
[Parameter]
28+
[CascadingParameter]
2929
public ThemeManifest? Theme { get; set; }
3030

3131
/// <summary>
3232
/// Gets or sets the <see cref="SiteManifest"/> instance.
3333
/// </summary>
34-
[Parameter]
34+
[CascadingParameter]
3535
public SiteManifest? Site { get; set; }
3636
}

src/ScissorHands.Web/Generators/StaticSiteGenerator.cs

Lines changed: 42 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public sealed class StaticSiteGenerator(
3030
IPluginRunner pluginRunner,
3131
IThemeService themeService,
3232
IComponentRenderer renderer,
33-
IAppPaths paths,
34-
IFileSystem fileSystem,
33+
IAppPaths paths,
34+
IFileSystem fileSystem,
3535
SiteManifest options,
3636
ILogger<StaticSiteGenerator> logger) : IStaticSiteGenerator
3737
{
@@ -69,39 +69,9 @@ public async Task BuildAsync<TMainLayout, TIndexView, TPostView, TPageView, TNot
6969
var notFoundDocument = documents.SingleOrDefault(d => d.Kind == ContentKind.Page && string.Equals(d.Metadata.Slug, PAGE_NOT_FOUND_SLUG, StringComparison.OrdinalIgnoreCase));
7070
await RenderNotFoundAsync<TNotFoundView>(notFoundDocument, plugins, theme, destination, layoutType, cancellationToken);
7171

72-
foreach (var document in documents)
72+
foreach (var document in documents.Where(d => IsNotFoundPage(d) == false))
7373
{
74-
cancellationToken.ThrowIfCancellationRequested();
75-
76-
if (document.Kind == ContentKind.Page && string.Equals(document.Metadata.Slug, PAGE_NOT_FOUND_SLUG, StringComparison.OrdinalIgnoreCase))
77-
{
78-
continue;
79-
}
80-
81-
var preProcessed = await _pluginRunner.RunPreMarkdownAsync(document, cancellationToken);
82-
var html = await _markdownService.ToHtmlAsync(preProcessed.Markdown, cancellationToken: cancellationToken);
83-
preProcessed.Html = html;
84-
var postMarkdown = await _pluginRunner.RunPostMarkdownAsync(preProcessed, cancellationToken);
85-
86-
var parameters = new Dictionary<string, object?>
87-
{
88-
["Document"] = postMarkdown,
89-
["Plugins"] = plugins,
90-
["Theme"] = theme,
91-
["Site"] = _options
92-
};
93-
94-
var rendered = postMarkdown.Kind switch
95-
{
96-
ContentKind.Page => await _renderer.RenderAsync<TPageView>(layoutType, parameters, cancellationToken),
97-
_ => await _renderer.RenderAsync<TPostView>(layoutType, parameters, cancellationToken)
98-
};
99-
100-
var finalHtml = await _pluginRunner.RunPostHtmlAsync(rendered, postMarkdown, cancellationToken);
101-
var outputPath = ResolveOutputPath(destination, postMarkdown.Metadata.Slug);
102-
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(outputPath)!);
103-
await _fileSystem.File.WriteAllTextAsync(outputPath, finalHtml, Encoding.UTF8, cancellationToken);
104-
_logger.LogInformation("Wrote {OutputPath}", outputPath);
74+
await RenderDocumentAsync<TPostView, TPageView>(document, plugins, theme, destination, layoutType, cancellationToken);
10575
}
10676

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

163+
private async Task RenderDocumentAsync<TPostView, TPageView>(ContentDocument document, IEnumerable<PluginManifest> plugins, ThemeManifest theme, string destination, Type layoutType, CancellationToken cancellationToken)
164+
where TPostView : ScissorHands.Theme.PostViewBase
165+
where TPageView : ScissorHands.Theme.PageViewBase
166+
{
167+
cancellationToken.ThrowIfCancellationRequested();
168+
169+
var preProcessed = await _pluginRunner.RunPreMarkdownAsync(document, cancellationToken);
170+
var html = await _markdownService.ToHtmlAsync(preProcessed.Markdown, cancellationToken: cancellationToken);
171+
preProcessed.Html = html;
172+
var postMarkdown = await _pluginRunner.RunPostMarkdownAsync(preProcessed, cancellationToken);
173+
174+
var parameters = new Dictionary<string, object?>
175+
{
176+
["Document"] = postMarkdown,
177+
["Plugins"] = plugins,
178+
["Theme"] = theme,
179+
["Site"] = _options
180+
};
181+
182+
var rendered = postMarkdown.Kind switch
183+
{
184+
ContentKind.Page => await _renderer.RenderAsync<TPageView>(layoutType, parameters, cancellationToken),
185+
_ => await _renderer.RenderAsync<TPostView>(layoutType, parameters, cancellationToken)
186+
};
187+
188+
var finalHtml = await _pluginRunner.RunPostHtmlAsync(rendered, postMarkdown, cancellationToken);
189+
var outputPath = ResolveOutputPath(destination, postMarkdown.Metadata.Slug);
190+
_fileSystem.Directory.CreateDirectory(_fileSystem.Path.GetDirectoryName(outputPath)!);
191+
await _fileSystem.File.WriteAllTextAsync(outputPath, finalHtml, Encoding.UTF8, cancellationToken);
192+
_logger.LogInformation("Wrote {OutputPath}", outputPath);
193+
}
194+
195+
private static bool IsNotFoundPage(ContentDocument document)
196+
{
197+
return document.Kind == ContentKind.Page &&
198+
string.Equals(document.Metadata.Slug, PAGE_NOT_FOUND_SLUG, StringComparison.OrdinalIgnoreCase) == true;
199+
}
200+
193201
private static string ResolveOutputPath(string root, string slug)
194202
{
195203
if (string.IsNullOrWhiteSpace(slug))

src/ScissorHands.Web/Renderers/ComponentRenderer.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
using System.Reflection;
2+
13
using Microsoft.AspNetCore.Components;
24
using Microsoft.AspNetCore.Components.Web;
35
using Microsoft.Extensions.DependencyInjection;
46
using Microsoft.Extensions.Logging;
57

8+
using ScissorHands.Theme;
9+
610
namespace ScissorHands.Web.Renderers;
711

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

22+
// Lazy initialization of cascading parameter names discovered via reflection
23+
private static readonly Lazy<HashSet<string>> _cascadingParameterNames = new(() =>
24+
{
25+
var parameterNames = new HashSet<string>(StringComparer.Ordinal);
26+
27+
// Discover all types in the ScissorHands.Theme assembly that have cascading parameters
28+
var themeAssembly = typeof(PageViewBase).Assembly;
29+
30+
try
31+
{
32+
var allTypes = themeAssembly.GetExportedTypes();
33+
34+
foreach (var type in allTypes)
35+
{
36+
// Find all properties with CascadingParameter attribute
37+
var cascadingProperties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
38+
.Where(p => p.GetCustomAttribute<CascadingParameterAttribute>() is not null);
39+
40+
foreach (var property in cascadingProperties)
41+
{
42+
parameterNames.Add(property.Name);
43+
}
44+
}
45+
}
46+
catch (ReflectionTypeLoadException)
47+
{
48+
// If type loading fails, fall back to empty set
49+
// This should not happen in normal operation, but provides safety
50+
}
51+
52+
return parameterNames;
53+
});
54+
1855
/// <inheritdoc />
1956
public async Task<string> RenderAsync<TComponent>(Type layoutType, IDictionary<string, object?> parameters, CancellationToken cancellationToken = default)
2057
where TComponent : IComponent
@@ -31,8 +68,14 @@ public async Task<string> RenderAsync<TComponent>(Type layoutType, IDictionary<s
3168
{
3269
builder.OpenComponent<TComponent>(0);
3370
var seq = 1;
71+
3472
foreach (var kvp in parameters)
3573
{
74+
if (_cascadingParameterNames.Value.Contains(kvp.Key))
75+
{
76+
continue;
77+
}
78+
3679
builder.AddAttribute(seq++, kvp.Key, kvp.Value);
3780
}
3881
builder.CloseComponent();

0 commit comments

Comments
 (0)