Skip to content

Commit f7663a3

Browse files
committed
feat: Add asset inclusion strategy and link detection for markdown processing
1 parent ca8947d commit f7663a3

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using OwlCore.Storage;
2+
3+
namespace WindowsAppCommunity.Blog.Assets;
4+
5+
/// <summary>
6+
/// Reference-only inclusion strategy: always rewrites paths to add one level of parent navigation,
7+
/// treating all assets as externally referenced (not included in page folder).
8+
/// </summary>
9+
public sealed class ReferenceOnlyInclusionStrategy : IAssetInclusionStrategy
10+
{
11+
/// <inheritdoc/>
12+
public Task<string> DecideAsync(
13+
IFile referencingMarkdown,
14+
IFile referencedAsset,
15+
string originalPath,
16+
CancellationToken ct = default)
17+
{
18+
// Reference-only behavior: add one parent directory prefix to account for folderization
19+
// Markdown file becomes folder/index.html, so links need one extra "../" to reach original location
20+
if (string.IsNullOrWhiteSpace(originalPath))
21+
return Task.FromResult(originalPath);
22+
23+
var rewrittenPath = "../" + originalPath;
24+
return Task.FromResult(rewrittenPath);
25+
}
26+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.Runtime.CompilerServices;
2+
using System.Text.RegularExpressions;
3+
using OwlCore.Storage;
4+
5+
namespace WindowsAppCommunity.Blog.Assets;
6+
7+
/// <summary>
8+
/// Detects relative asset links in rendered HTML using path-pattern regex (no element parsing).
9+
/// </summary>
10+
public sealed partial class RegexAssetLinkDetector : IAssetLinkDetector
11+
{
12+
/// <summary>
13+
/// Regex pattern for relative path segments: alphanumerics, underscore, hyphen, dot.
14+
/// Matches paths with optional ./ or ../ prefixes and / or \ separators.
15+
/// </summary>
16+
[GeneratedRegex(@"(?<![A-Za-z0-9_\-\.])(?:(?:\./)|(?:\.\./)+)?[A-Za-z0-9_\-\.]+(?:[\/\\][A-Za-z0-9_\-\.]+)*(?![A-Za-z0-9_\-\.])", RegexOptions.Compiled)]
17+
private static partial Regex RelativePathPattern();
18+
19+
/// <inheritdoc/>
20+
public async IAsyncEnumerable<string> DetectAsync(IFile htmlSource, [EnumeratorCancellation] CancellationToken ct = default)
21+
{
22+
// Read HTML content
23+
using var stream = await htmlSource.OpenStreamAsync(FileAccess.Read, ct);
24+
using var reader = new StreamReader(stream);
25+
var html = await reader.ReadToEndAsync(ct);
26+
27+
// Find all matches
28+
var matches = RelativePathPattern().Matches(html);
29+
30+
foreach (Match match in matches)
31+
{
32+
if (ct.IsCancellationRequested)
33+
yield break;
34+
35+
var path = match.Value;
36+
37+
// Filter out non-relative patterns
38+
if (string.IsNullOrWhiteSpace(path))
39+
continue;
40+
41+
// Exclude absolute schemes
42+
if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
43+
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
44+
path.StartsWith("data:", StringComparison.OrdinalIgnoreCase) ||
45+
path.StartsWith("//", StringComparison.Ordinal))
46+
continue;
47+
48+
// Exclude absolute root paths (optional - treating these as non-relative)
49+
if (path.StartsWith('/') || path.StartsWith('\\'))
50+
continue;
51+
52+
yield return path;
53+
}
54+
}
55+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using OwlCore.Storage;
2+
3+
namespace WindowsAppCommunity.Blog.Assets;
4+
5+
/// <summary>
6+
/// Resolves relative paths to IFile instances using source folder and markdown file context.
7+
/// Paths are resolved relative to the markdown file's location (pre-folderization).
8+
/// </summary>
9+
public sealed class RelativePathAssetResolver : IAssetResolver
10+
{
11+
/// <inheritdoc/>
12+
public required IFolder SourceFolder { get; init; }
13+
14+
/// <inheritdoc/>
15+
public required IFile MarkdownSource { get; init; }
16+
17+
/// <inheritdoc/>
18+
public async Task<IFile?> ResolveAsync(string relativePath, CancellationToken ct = default)
19+
{
20+
if (string.IsNullOrWhiteSpace(relativePath))
21+
return null;
22+
23+
try
24+
{
25+
// Normalize path separators to forward slash
26+
var normalizedPath = relativePath.Replace('\\', '/');
27+
28+
// Resolve relative to markdown file's location (pre-folderization)
29+
// The markdown file itself is the base for relative path resolution
30+
var item = await MarkdownSource.GetItemByRelativePathAsync(normalizedPath, ct);
31+
32+
// Return only if it's a file
33+
return item as IFile;
34+
}
35+
catch
36+
{
37+
// Path resolution failed (invalid path, not found, etc.)
38+
return null;
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)