Skip to content

Commit ca8947d

Browse files
committed
feat: Implement asset-aware structures for markdown processing with link detection and resolution
1 parent 3c765e2 commit ca8947d

8 files changed

+881
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using OwlCore.Storage;
2+
3+
namespace WindowsAppCommunity.Blog.Assets;
4+
5+
/// <summary>
6+
/// Provides decision logic for asset inclusion via path rewriting.
7+
/// Strategy returns rewritten path - path structure determines Include vs Reference behavior.
8+
/// </summary>
9+
public interface IAssetInclusionStrategy
10+
{
11+
/// <summary>
12+
/// Decides asset inclusion strategy by returning rewritten path.
13+
/// </summary>
14+
/// <param name="referencingMarkdown">The markdown file that references the asset.</param>
15+
/// <param name="referencedAsset">The asset file being referenced.</param>
16+
/// <param name="originalPath">The original relative path from markdown.</param>
17+
/// <param name="ct">Cancellation token.</param>
18+
/// <returns>
19+
/// Rewritten path string. Path structure determines behavior:
20+
/// <list type="bullet">
21+
/// <item><description>Child path (no ../ prefix): Asset included in page folder (self-contained)</description></item>
22+
/// <item><description>Parent path (../ prefix): Asset referenced externally (link rewritten to account for folderization)</description></item>
23+
/// </list>
24+
/// </returns>
25+
Task<string> DecideAsync(
26+
IFile referencingMarkdown,
27+
IFile referencedAsset,
28+
string originalPath,
29+
CancellationToken ct = default);
30+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using OwlCore.Storage;
2+
3+
namespace WindowsAppCommunity.Blog.Assets;
4+
5+
/// <summary>
6+
/// Detects relative asset links in rendered HTML output.
7+
/// </summary>
8+
public interface IAssetLinkDetector
9+
{
10+
/// <summary>
11+
/// Detects relative asset link strings in rendered HTML output.
12+
/// </summary>
13+
/// <param name="htmlSource">Virtual IFile representing rendered HTML output (in-memory representation).</param>
14+
/// <param name="ct">Cancellation token.</param>
15+
/// <returns>Async enumerable of relative path strings.</returns>
16+
IAsyncEnumerable<string> DetectAsync(IFile htmlSource, CancellationToken ct = default);
17+
}

src/Blog/Assets/IAssetResolver.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using OwlCore.Storage;
2+
3+
namespace WindowsAppCommunity.Blog.Assets;
4+
5+
/// <summary>
6+
/// Resolves relative path strings to IFile instances.
7+
/// </summary>
8+
public interface IAssetResolver
9+
{
10+
/// <summary>
11+
/// Root folder for relative path resolution.
12+
/// </summary>
13+
IFolder SourceFolder { get; init; }
14+
15+
/// <summary>
16+
/// Markdown file for relative path context.
17+
/// </summary>
18+
IFile MarkdownSource { get; init; }
19+
20+
/// <summary>
21+
/// Resolves a relative path string to an IFile instance.
22+
/// </summary>
23+
/// <param name="relativePath">The relative path to resolve.</param>
24+
/// <param name="ct">Cancellation token.</param>
25+
/// <returns>The resolved IFile, or null if not found.</returns>
26+
Task<IFile?> ResolveAsync(string relativePath, CancellationToken ct = default);
27+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using OwlCore.Storage;
6+
using WindowsAppCommunity.Blog.Assets;
7+
using WindowsAppCommunity.Blog.PostPage;
8+
9+
namespace WindowsAppCommunity.Blog.Page
10+
{
11+
/// <summary>
12+
/// Asset-aware virtual HTML file - extends base with link detection, asset resolution, and inclusion decisions.
13+
/// Sealed - this is the final asset-aware implementation.
14+
/// Implements link rewriting and asset tracking during post-processing.
15+
/// </summary>
16+
public sealed class AssetAwareHtmlTemplatedMarkdownFile : HtmlTemplatedMarkdownFile
17+
{
18+
private readonly List<IFile> _includedAssets = new();
19+
20+
/// <summary>
21+
/// Creates asset-aware virtual HTML file with lazy markdown→HTML generation and asset management.
22+
/// </summary>
23+
/// <param name="id">Unique identifier for this file (parent-derived)</param>
24+
/// <param name="markdownSource">Source markdown file to transform</param>
25+
/// <param name="templateSource">Template as IFile or IFolder</param>
26+
/// <param name="templateFileName">Template file name when source is IFolder (defaults to "template.html")</param>
27+
/// <param name="parent">Parent folder in virtual hierarchy (optional)</param>
28+
public AssetAwareHtmlTemplatedMarkdownFile(
29+
string id,
30+
IFile markdownSource,
31+
IStorable templateSource,
32+
string? templateFileName = null,
33+
IFolder? parent = null)
34+
: base(id, markdownSource, templateSource, templateFileName, parent)
35+
{
36+
}
37+
38+
/// <summary>
39+
/// Asset link detector for finding relative links in rendered HTML output.
40+
/// </summary>
41+
public required IAssetLinkDetector LinkDetector { get; init; }
42+
43+
/// <summary>
44+
/// Asset resolver for converting paths to IFile instances.
45+
/// </summary>
46+
public required IAssetResolver Resolver { get; init; }
47+
48+
/// <summary>
49+
/// Inclusion strategy for deciding include vs reference via path rewriting.
50+
/// </summary>
51+
public required IAssetInclusionStrategy InclusionStrategy { get; init; }
52+
53+
/// <summary>
54+
/// Assets that were decided for inclusion (self-contained in page folder).
55+
/// Exposed to containing folder for yielding in virtual structure.
56+
/// </summary>
57+
public IReadOnlyCollection<IFile> IncludedAssets => _includedAssets.AsReadOnly();
58+
59+
/// <summary>
60+
/// Post-process HTML with asset management pipeline.
61+
/// Detects links → Resolves to files → Decides include/reference via path rewriting → Tracks included assets.
62+
/// </summary>
63+
/// <param name="html">Rendered HTML from template</param>
64+
/// <param name="model">Data model used for rendering</param>
65+
/// <param name="ct">Cancellation token</param>
66+
/// <returns>Post-processed HTML with rewritten links</returns>
67+
protected override async Task<string> PostProcessHtmlAsync(string html, PostPageDataModel model, CancellationToken ct)
68+
{
69+
// Clear included assets from any previous generation
70+
_includedAssets.Clear();
71+
72+
// Detect asset links in rendered HTML output (pass self as IFile)
73+
await foreach (var originalPath in LinkDetector.DetectAsync(this, ct))
74+
{
75+
// Resolve path to IFile
76+
var resolvedAsset = await Resolver.ResolveAsync(originalPath, ct);
77+
78+
// Null resolver policy: Skip if not found (preserve broken link)
79+
if (resolvedAsset == null)
80+
{
81+
continue;
82+
}
83+
84+
// Strategy decides include vs reference by returning rewritten path
85+
// Path structure determines behavior:
86+
// - Child path (no ../ prefix): Include
87+
// - Parent path (../ prefix): Reference
88+
var rewrittenPath = await InclusionStrategy.DecideAsync(
89+
MarkdownSource, // Pass original markdown source (not virtual HTML)
90+
resolvedAsset,
91+
originalPath,
92+
ct);
93+
94+
// Implicit decision based on path structure
95+
if (!rewrittenPath.StartsWith("../"))
96+
{
97+
// Include: Add to tracked assets (will be yielded by containing folder)
98+
_includedAssets.Add(resolvedAsset);
99+
}
100+
// Reference: Asset not added to included list (stays external)
101+
102+
// Rewrite link in HTML (applies to both Include and Reference)
103+
html = html.Replace(originalPath, rewrittenPath);
104+
}
105+
106+
return html;
107+
}
108+
}
109+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.CompilerServices;
4+
using System.Threading;
5+
using OwlCore.Storage;
6+
using WindowsAppCommunity.Blog.Assets;
7+
8+
namespace WindowsAppCommunity.Blog.Page
9+
{
10+
/// <summary>
11+
/// Asset-aware virtual folder - extends base with markdown-referenced asset inclusion.
12+
/// Creates asset-aware file variant and yields included assets in virtual structure.
13+
/// Implements lazy generation - no file system operations during construction.
14+
/// </summary>
15+
public class AssetAwareHtmlTemplatedMarkdownPageFolder : HtmlTemplatedMarkdownPageFolder
16+
{
17+
/// <summary>
18+
/// Creates asset-aware virtual folder representing single-page output structure with asset management.
19+
/// No file system operations occur during construction (lazy generation).
20+
/// </summary>
21+
/// <param name="markdownSource">Source markdown file to transform</param>
22+
/// <param name="templateSource">Template as IFile or IFolder</param>
23+
/// <param name="templateFileName">Template file name when source is IFolder (defaults to "template.html")</param>
24+
public AssetAwareHtmlTemplatedMarkdownPageFolder(
25+
IFile markdownSource,
26+
IStorable templateSource,
27+
string? templateFileName = null)
28+
: base(markdownSource, templateSource, templateFileName)
29+
{
30+
}
31+
32+
/// <summary>
33+
/// Asset link detector for finding relative links in markdown.
34+
/// </summary>
35+
public required IAssetLinkDetector LinkDetector { get; init; }
36+
37+
/// <summary>
38+
/// Asset resolver for converting paths to IFile instances.
39+
/// </summary>
40+
public required IAssetResolver Resolver { get; init; }
41+
42+
/// <summary>
43+
/// Inclusion strategy for deciding include vs reference per asset.
44+
/// </summary>
45+
public required IAssetInclusionStrategy InclusionStrategy { get; init; }
46+
47+
/// <inheritdoc />
48+
public override async IAsyncEnumerable<IStorableChild> GetItemsAsync(
49+
StorableType type = StorableType.All,
50+
[EnumeratorCancellation] CancellationToken cancellationToken = default)
51+
{
52+
AssetAwareHtmlTemplatedMarkdownFile? assetAwareFile = null;
53+
54+
// Yield base items (HTML file + template assets), capturing asset-aware file reference
55+
await foreach (var item in base.GetItemsAsync(type, cancellationToken))
56+
{
57+
// Intercept HTML file creation to replace with asset-aware variant
58+
if (item is HtmlTemplatedMarkdownFile htmlFile && assetAwareFile == null)
59+
{
60+
// Create asset-aware variant with required properties set
61+
assetAwareFile = new AssetAwareHtmlTemplatedMarkdownFile(
62+
htmlFile.Id,
63+
MarkdownSource,
64+
TemplateSource,
65+
TemplateFileName,
66+
this)
67+
{
68+
Name = htmlFile.Name,
69+
Created = htmlFile.Created,
70+
Modified = htmlFile.Modified,
71+
LinkDetector = LinkDetector,
72+
Resolver = Resolver,
73+
InclusionStrategy = InclusionStrategy
74+
};
75+
76+
yield return assetAwareFile;
77+
continue;
78+
}
79+
80+
// Pass through other items (template assets)
81+
yield return item;
82+
}
83+
84+
// Yield markdown-referenced assets that were decided for inclusion
85+
if (assetAwareFile != null && (type == StorableType.All || type == StorableType.File))
86+
{
87+
foreach (var includedAsset in assetAwareFile.IncludedAssets)
88+
{
89+
yield return (IStorableChild)includedAsset;
90+
}
91+
}
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)