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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Content engine library targeting .NET 11 / C# 15 with union types.
- Run docs site: `dotnet run --project docs/Pennington.Docs`

## Project Structure
- `src/Pennington/` — Core library (Markdig, YamlDotNet, AngleSharp, TextMateSharp)
- `src/Pennington/` — Core library (Markdig, SharpYaml, AngleSharp, TextMateSharp)
- `src/Pennington.UI/` — Razor component library (TableOfContentsNav, OutlineNav, Badge, Card, CodeBlock, etc.)
- `src/Pennington.MonorailCss/` — MonorailCSS integration (utility-first CSS generation)
- `src/Pennington.DocSite/` — Documentation site template (layout, pages, content resolver)
Expand Down
2 changes: 1 addition & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<PackageVersion Include="MinVer" Version="7.0.0" />
<PackageVersion Include="MonorailCss" Version="0.0.5-alpha.0.146" />
<PackageVersion Include="MonorailCss.Discovery" Version="0.0.5-alpha.0.146" />
<PackageVersion Include="SharpYaml" Version="3.7.0" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="Spectre.Console" Version="0.54.1-alpha.0.7" />
<PackageVersion Include="Spectre.Console.Cli" Version="1.0.0-alpha.0.7" />
Expand All @@ -37,7 +38,6 @@
<PackageVersion Include="XenoAtom.Terminal.UI" Version="3.4.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="16.3.0" />
<PackageVersion Include="ZiggyCreatures.FusionCache" Version="2.6.0" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ When a piece of site content is structured data — sponsors, navigation, schedu

## Register the file

`AddDataFile<T>(name, path)` deserializes `path` into `T` on first access and tracks the file for changes. Format is chosen from the extension: `.yml` and `.yaml` go through YamlDotNet, `.json` through `System.Text.Json`. Both deserializers use camelCase property naming with case-insensitive matching, mirroring how front matter is parsed.
`AddDataFile<T>(name, path)` deserializes `path` into `T` on first access and tracks the file for changes. Format is chosen from the extension: `.yml` and `.yaml` go through SharpYaml, `.json` through `System.Text.Json`. Both deserializers use camelCase property naming with case-insensitive matching, mirroring how front matter is parsed.

```csharp
builder.Services.AddDataFile<List<Sponsor>>("sponsors", "data/sponsors.yml");
Expand All @@ -21,7 +21,7 @@ builder.Services.AddDataFile<List<NavLink>>("nav", "data/nav.json");

The lookup key (`"sponsors"`) is case-insensitive and must be unique across registrations. Paths are resolved against the current working directory if relative.

The target type needs a parameterless constructor — use a record with init-only properties so YamlDotNet can populate it:
The target type needs a parameterless constructor — use a record with init-only properties so SharpYaml can populate it:

```csharp
public record Sponsor
Expand Down
15 changes: 15 additions & 0 deletions examples/MultipleSourcesExample/BlogFrontMatterYamlContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace MultipleSourcesExample;

using System.Text.Json.Serialization;
using SharpYaml.Serialization;

/// <summary>
/// Source-generated SharpYaml metadata for this host's custom <see cref="BlogFrontMatter"/>,
/// registered via <c>AddPenningtonYamlContext</c> so it deserializes without reflection. Types
/// no registered context covers (here, anything but <see cref="BlogFrontMatter"/>) fall back to reflection.
/// </summary>
[YamlSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true)]
[YamlSerializable(typeof(BlogFrontMatter))]
internal partial class BlogFrontMatterYamlContext : YamlSerializerContext
{
}
2 changes: 2 additions & 0 deletions examples/MultipleSourcesExample/MultipleSourcesExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<RootNamespace>MultipleSourcesExample</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!-- Direct reference so the SharpYaml source generator runs here (BlogFrontMatterYamlContext). -->
<PackageReference Include="SharpYaml" />
<ProjectReference Include="..\..\src\Pennington\Pennington.csproj" />
</ItemGroup>
<ItemGroup>
Expand Down
4 changes: 4 additions & 0 deletions examples/MultipleSourcesExample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
penn.AddMarkdownContent<BlogFrontMatter>(ServiceConfiguration.RegisterBlogSource);
});

// Opt the custom BlogFrontMatter into source-generated YAML metadata. DocFrontMatter is
// already covered by Pennington's built-in context; types with no context use reflection.
builder.Services.AddPenningtonYamlContext(BlogFrontMatterYamlContext.Default);

var app = builder.Build();

app.UsePennington();
Expand Down
1 change: 1 addition & 0 deletions examples/MultipleSourcesExample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Bare `AddPennington` host with two `AddMarkdownContent<T>` calls pointing at dif
- Multiple markdown sources in one host
- Per-source `ContentPath` / `BasePageUrl` / `ExcludePaths`
- Per-source front-matter types
- Opt a custom front-matter type into source-generated YAML metadata via `AddPenningtonYamlContext` (`BlogFrontMatterYamlContext`); reflection remains the fallback for unregistered types
- Overlap demonstration toggled by the `MULTIPLE_SOURCES_OVERLAP=1` env var

## Referenced from
Expand Down
3 changes: 3 additions & 0 deletions src/Pennington.BlogSite/BlogSiteServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public static IServiceCollection AddBlogSite(this IServiceCollection services,
: [blogSiteAssembly, .. routingAssemblies];
});

// Source-generated YAML metadata for BlogSite's front-matter type (reflection fallback otherwise).
services.AddPenningtonYamlContext(BlogSiteYamlContext.Default);

// Make Pennington.UI components available inline in markdown via Mdazor.
// <CodeBlock> is intentionally excluded: markdown authors should use fenced
// code blocks, not a component round-trip through Mdazor+Markdig.
Expand Down
15 changes: 15 additions & 0 deletions src/Pennington.BlogSite/BlogSiteYamlContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Pennington.BlogSite;

using System.Text.Json.Serialization;
using SharpYaml.Serialization;

/// <summary>
/// Source-generated SharpYaml metadata for BlogSite's front-matter record, registered by
/// <see cref="BlogSiteServiceExtensions.AddBlogSite"/> so it deserializes without reflection
/// (NativeAOT/trim-friendly).
/// </summary>
[YamlSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true)]
[YamlSerializable(typeof(BlogSiteFrontMatter))]
internal partial class BlogSiteYamlContext : YamlSerializerContext
{
}
2 changes: 2 additions & 0 deletions src/Pennington.BlogSite/Pennington.BlogSite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<!-- Direct reference so the SharpYaml source generator runs in this project (BlogSiteYamlContext). -->
<PackageReference Include="SharpYaml" />
<ProjectReference Include="..\Pennington\Pennington.csproj" />
<ProjectReference Include="..\Pennington.UI\Pennington.UI.csproj" />
<ProjectReference Include="..\Pennington.MonorailCss\Pennington.MonorailCss.csproj" />
Expand Down
3 changes: 3 additions & 0 deletions src/Pennington.DocSite/DocSiteServiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ public static IServiceCollection AddDocSite(this IServiceCollection services,
options.ConfigurePennington?.Invoke(penn);
});

// Source-generated YAML metadata for DocSite's front-matter types (reflection fallback otherwise).
services.AddPenningtonYamlContext(DocSiteYamlContext.Default);

// Make Pennington.UI components available inline in markdown via Mdazor.
// <CodeBlock> is intentionally excluded: markdown authors should use fenced
// code blocks, not a component round-trip through Mdazor+Markdig.
Expand Down
16 changes: 16 additions & 0 deletions src/Pennington.DocSite/DocSiteYamlContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Pennington.DocSite;

using System.Text.Json.Serialization;
using SharpYaml.Serialization;

/// <summary>
/// Source-generated SharpYaml metadata for DocSite's front-matter records, registered by
/// <see cref="DocSiteServiceExtensions.AddDocSite"/> so they deserialize without reflection
/// (NativeAOT/trim-friendly).
/// </summary>
[YamlSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true)]
[YamlSerializable(typeof(DocSiteFrontMatter))]
[YamlSerializable(typeof(BlogPostFrontMatter))]
internal partial class DocSiteYamlContext : YamlSerializerContext
{
}
2 changes: 2 additions & 0 deletions src/Pennington.DocSite/Pennington.DocSite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<!-- Direct reference so the SharpYaml source generator runs in this project (DocSiteYamlContext). -->
<PackageReference Include="SharpYaml" />
<ProjectReference Include="..\Pennington\Pennington.csproj" />
<ProjectReference Include="..\Pennington.UI\Pennington.UI.csproj" />
<ProjectReference Include="..\Pennington.MonorailCss\Pennington.MonorailCss.csproj" />
Expand Down
10 changes: 2 additions & 8 deletions src/Pennington/Content/MarkdownContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ namespace Pennington.Content;
using LlmsTxt;
using Pipeline;
using Routing;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using SharpYaml;

/// <summary>
/// Discovers and provides markdown content from a directory.
Expand Down Expand Up @@ -573,11 +572,6 @@ private async Task<ImmutableList<LlmsSubtree>> LoadSubtreesAsync()
}

var basePrefix = NormalizeBasePageUrl(_options.BasePageUrl.Value);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();

var builder = ImmutableList.CreateBuilder<LlmsSubtree>();

foreach (var file in _fileSystem.Directory.EnumerateFiles(
Expand All @@ -596,7 +590,7 @@ private async Task<ImmutableList<LlmsSubtree>> LoadSubtreesAsync()
LlmsSubtreeSidecar? sidecar;
try
{
sidecar = deserializer.Deserialize<LlmsSubtreeSidecar?>(content);
sidecar = YamlSerializer.Deserialize<LlmsSubtreeSidecar>(content, PenningtonYaml.ReflectionOptions);
}
catch
{
Expand Down
11 changes: 2 additions & 9 deletions src/Pennington/Content/RedirectContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ namespace Pennington.Content;
using Microsoft.Extensions.DependencyInjection;
using Pipeline;
using Routing;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using SharpYaml;

/// <summary>
/// Holds the unified redirect map used by <see cref="PenningtonRedirectMiddleware"/>
Expand Down Expand Up @@ -164,12 +162,7 @@ private async Task<ImmutableDictionary<string, string>> LoadYamlAsync()

try
{
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.Build();

var config = deserializer.Deserialize<RedirectsConfig>(yaml);
var config = YamlSerializer.Deserialize<RedirectsConfig>(yaml, PenningtonYaml.ReflectionOptions);
if (config?.Redirects is null || config.Redirects.Count == 0)
{
return ImmutableDictionary<string, string>.Empty;
Expand Down
36 changes: 21 additions & 15 deletions src/Pennington/Data/DataFileLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,19 @@ namespace Pennington.Data;

using System.IO.Abstractions;
using System.Text.Json;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
using FrontMatter;
using SharpYaml;
using SharpYaml.Events;

/// <summary>
/// Deserializes a data file's bytes into a typed value. Format is chosen from the
/// extension (<c>.yml</c> / <c>.yaml</c> use YamlDotNet, <c>.json</c> uses System.Text.Json),
/// extension (<c>.yml</c> / <c>.yaml</c> use SharpYaml, <c>.json</c> uses System.Text.Json),
/// both configured with camelCase naming and case-insensitive property matching to mirror
/// <see cref="FrontMatter.FrontMatterParser"/>'s behavior.
/// <see cref="FrontMatter.FrontMatterParser"/>'s behavior. Arbitrary data types deserialize
/// via reflection (no source-gen context covers them).
/// </summary>
public static class DataFileLoader
{
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.WithCaseInsensitivePropertyMatching()
.IgnoreUnmatchedProperties()
.Build();

private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Expand Down Expand Up @@ -93,9 +88,20 @@ private static bool RootIsYamlSequence(string content, string path)
{
try
{
var stream = new YamlStream();
stream.Load(new StringReader(content));
return stream.Documents.Count > 0 && stream.Documents[0].RootNode is YamlSequenceNode;
// The first node-start event after the stream/document preamble is the root node.
var parser = Parser.CreateParser(new StringReader(content));
while (parser.MoveNext())
{
switch (parser.Current)
{
case SequenceStart:
return true;
case MappingStart or Scalar:
return false;
}
}

return false;
}
catch (Exception ex)
{
Expand Down Expand Up @@ -129,7 +135,7 @@ private static T DeserializeYaml<T>(string content, string path)
{
try
{
return YamlDeserializer.Deserialize<T>(content)
return YamlSerializer.Deserialize<T>(content, PenningtonYaml.ReflectionOptions)
?? throw new InvalidDataException($"YAML in {path} deserialized to null");
}
catch (Exception ex) when (ex is not InvalidDataException)
Expand Down
24 changes: 0 additions & 24 deletions src/Pennington/FrontMatter/BufferedYamlParser.cs

This file was deleted.

Loading
Loading