Skip to content

Commit e40c1c0

Browse files
committed
fix: materialize blog page assets
1 parent ebbd8cf commit e40c1c0

5 files changed

Lines changed: 178 additions & 24 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using OwlCore.Storage;
2+
using WindowsAppCommunity.Blog.Assets;
3+
4+
namespace WindowsAppCommunity.CommandLine.Blog.PostPage;
5+
6+
internal static class PageAssetMaterializer
7+
{
8+
public static async Task<HashSet<string>> GetFileIdsAsync(IStorable source)
9+
{
10+
if (source is IFile file)
11+
return [file.Id];
12+
13+
if (source is IFolder folder)
14+
return [.. await new DepthFirstRecursiveFolder(folder).GetFilesAsync().Select(x => x.Id).ToListAsync()];
15+
16+
return [];
17+
}
18+
19+
public static async Task CopyAssetsAsync(IModifiableFolder pageOutputFolder, IEnumerable<PageAsset> assets)
20+
{
21+
foreach (var asset in assets)
22+
{
23+
if (Path.GetExtension(asset.ResolvedFile.Name).Equals(".md", StringComparison.OrdinalIgnoreCase))
24+
continue;
25+
26+
var rewrittenPath = NormalizePath(asset.RewrittenPath);
27+
var directoryPath = NormalizePath(Path.GetDirectoryName(rewrittenPath));
28+
var assetOutputFolder = pageOutputFolder;
29+
30+
if (!string.IsNullOrWhiteSpace(directoryPath) && directoryPath != ".")
31+
{
32+
assetOutputFolder = (IModifiableFolder)await pageOutputFolder
33+
.CreateFoldersAlongRelativePathAsync(directoryPath, overwrite: false)
34+
.LastAsync();
35+
}
36+
37+
await assetOutputFolder.CreateCopyOfAsync(asset.ResolvedFile, overwrite: true);
38+
}
39+
}
40+
41+
private static string NormalizePath(string? path)
42+
{
43+
return path?.Replace('\\', '/') ?? string.Empty;
44+
}
45+
}

src/Commands/Blog/PostPage/PageCommand.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,24 +88,31 @@ private async Task<int> ExecuteAsync(string markdownPath, string templatePath, s
8888
// Resolve output folder (SystemFolder throws if doesn't exist)
8989
IModifiableFolder outputFolder = new SystemFolder(outputPath);
9090

91+
var templateFileIds = await PageAssetMaterializer.GetFileIdsAsync(templateSource);
92+
9193
// Create virtual PostPageFolder (lazy generation - no I/O during construction)
9294
var postPageFolder = new AssetAwareHtmlTemplatedMarkdownPageFolder(markdownFile, templateSource, templateFileName)
9395
{
9496
Id = markdownFile.Id.HashMD5Fast(),
95-
// Single-file page output includes assets by default
96-
// Unlike multi-page output which references assets by default
97-
AssetStrategy = new KnownAssetStrategy(),
97+
AssetStrategy = new KnownAssetStrategy
98+
{
99+
IncludedAssetFileIds = templateFileIds,
100+
ReferencedAssetFileIds = [markdownFile.Id],
101+
UnknownAssetFallbackStrategy = AssetFallbackBehavior.Reference,
102+
UnknownAssetFaultStrategy = FaultStrategy.None,
103+
},
98104
Resolver = new RelativePathAssetResolver(),
99105
LinkDetector = new RegexAssetLinkDetector(),
100106
};
101107

102108
// Create output folder for this page
103-
var pageOutputFolder = await outputFolder.CreateFolderAsync(postPageFolder.Name, overwrite: true);
109+
var pageOutputFolder = (IModifiableFolder)await outputFolder.CreateFolderAsync(postPageFolder.Name, overwrite: true);
104110

105111
// Materialize virtual structure by recursively copying all files
106-
await foreach (AssetAwareHtmlTemplatedMarkdownFile file in new DepthFirstRecursiveFolder(postPageFolder).GetFilesAsync())
112+
await foreach (AssetAwareHtmlTemplatedMarkdownFile file in postPageFolder.GetItemsAsync(StorableType.File))
107113
{
108-
// TODO, see https://discord.com/channels/372137812037730304/1396673230013464636/1441902694196449505
114+
await pageOutputFolder.CreateCopyOfAsync(file, overwrite: true);
115+
await PageAssetMaterializer.CopyAssetsAsync(pageOutputFolder, file.Assets);
109116
}
110117

111118
var outputFolderName = Path.GetFileNameWithoutExtension(markdownFile.Name);

src/Commands/Blog/PostPage/PagesCommand.cs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ private async Task<int> ExecuteAsync(
8686

8787
// Create recursive markdown-to-webpage folder (lazy generation - no I/O during construction)
8888
// Turns `.md` files into folders with an `index.html` holding asset metadata for output copy
89-
HashSet<string> templateFileIds = [.. (templateSource is IFile file) ? [file.Id] : await new DepthFirstRecursiveFolder((IFolder)templateSource).GetFilesAsync().Select(x => x.Id).ToListAsync()];
90-
HashSet<string> markdownSourceFileIds = [.. await new DepthFirstRecursiveFolder(markdownSourceFolder).GetFilesAsync().Select(x => x.Id).ToListAsync()];
89+
var templateFileIds = await PageAssetMaterializer.GetFileIdsAsync(templateSource);
90+
var markdownSourceFileIds = await PageAssetMaterializer.GetFileIdsAsync(markdownSourceFolder);
9191
var pagesFolder = new AssetAwareHtmlTemplatedMarkdownPagesFolder(markdownSourceFolder, templateSource, templateFileName)
9292
{
9393
LinkDetector = new RegexAssetLinkDetector(),
@@ -112,20 +112,8 @@ private async Task<int> ExecuteAsync(
112112
await foreach (AssetAwareHtmlTemplatedMarkdownFile indexFile in pageFolder.GetItemsAsync(StorableType.File))
113113
{
114114
// Create folders relative to THIS page's output folder, then copy
115-
var copiedIndexFile = await pageOutputFolder.CreateCopyOfAsync(indexFile, overwrite: true);
116-
117-
// Copy all assets referenced in index.html to the rewritten asset path
118-
// Logger.LogInformation($"Included: {indexFile.Assets.Count}");
119-
foreach (var asset in indexFile.Assets)
120-
{
121-
if (Path.GetExtension(asset.ResolvedFile.Name) == ".md")
122-
{
123-
//
124-
}
125-
126-
var assetOutputFolder = (IModifiableFolder)await pageOutputFolder.CreateFoldersAlongRelativePathAsync(asset.RewrittenPath, overwrite: false).LastAsync();
127-
await assetOutputFolder.CreateCopyOfAsync(asset.ResolvedFile, overwrite: true);
128-
}
115+
await pageOutputFolder.CreateCopyOfAsync(indexFile, overwrite: true);
116+
await PageAssetMaterializer.CopyAssetsAsync(pageOutputFolder, indexFile.Assets);
129117
}
130118
}
131119

tests/Blog/AssetAwareHtmlTemplatedMarkdownPagesFolderTests.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public class AssetAwareHtmlTemplatedMarkdownPagesFolderTests
2424
private IFile _page2File = null!;
2525
private IFile _logoFile = null!;
2626

27+
private static KnownAssetStrategy CreateReferenceOnlyStrategy()
28+
{
29+
return new KnownAssetStrategy
30+
{
31+
UnknownAssetFallbackStrategy = AssetFallbackBehavior.Reference,
32+
UnknownAssetFaultStrategy = FaultStrategy.None,
33+
};
34+
}
35+
2736
[TestInitialize]
2837
public async Task Setup()
2938
{
@@ -94,7 +103,7 @@ await templateWriter.WriteAsync(@"<!DOCTYPE html>
94103
{
95104
LinkDetector = new RegexAssetLinkDetector(),
96105
Resolver = new RelativePathAssetResolver(),
97-
AssetStrategy = new ReferenceOnlyAssetStrategy()
106+
AssetStrategy = CreateReferenceOnlyStrategy()
98107
};
99108
}
100109

@@ -176,12 +185,13 @@ public async Task AssetPathResolution_ResolvesValidPaths()
176185
[TestMethod]
177186
public async Task AssetStrategy_AppliesReferenceDecisions()
178187
{
179-
var strategy = new ReferenceOnlyAssetStrategy();
188+
var strategy = CreateReferenceOnlyStrategy();
180189
Assert.IsNotNull(_page1File, "page1.md should exist");
181190
Assert.IsNotNull(_logoFile, "logo.png should exist");
182191

183192
var rewrittenPath = await strategy.DecideAsync(_page1File, _logoFile, "../images/logo.png");
184193

194+
Assert.IsNotNull(rewrittenPath, "Reference-only strategy should return a rewritten path");
185195
Assert.IsTrue(rewrittenPath.StartsWith("../"), "Reference-only strategy should return path with ../ prefix");
186196
Assert.IsTrue(rewrittenPath.Contains("images/logo.png"), "Rewritten path should preserve original structure");
187197
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using Microsoft.VisualStudio.TestTools.UnitTesting;
2+
using System.CommandLine;
3+
using WindowsAppCommunity.CommandLine.Blog.PostPage;
4+
5+
namespace WindowsAppCommunity.CommandLine.Tests.Blog;
6+
7+
[TestClass]
8+
public class BlogCommandMaterializationTests
9+
{
10+
[TestMethod]
11+
public async Task PageCommand_CopiesGeneratedIndexAndTemplateAssets()
12+
{
13+
var tempRoot = CreateTempRoot();
14+
15+
try
16+
{
17+
var markdownPath = Path.Combine(tempRoot, "post.md");
18+
var templateFolder = Path.Combine(tempRoot, "template");
19+
var outputFolder = Path.Combine(tempRoot, "output");
20+
21+
Directory.CreateDirectory(Path.Combine(templateFolder, "images"));
22+
Directory.CreateDirectory(outputFolder);
23+
24+
await File.WriteAllTextAsync(markdownPath, "---\ntitle: Test Post\n---\n\n# Hello");
25+
await File.WriteAllTextAsync(Path.Combine(templateFolder, "template.html"), "<html><head><link rel=\"stylesheet\" href=\"styles.css\"></head><body><img src=\"images/logo.png\">{{ body }}</body></html>");
26+
await File.WriteAllTextAsync(Path.Combine(templateFolder, "styles.css"), "body { color: black; }");
27+
await File.WriteAllTextAsync(Path.Combine(templateFolder, "images", "logo.png"), "logo");
28+
29+
var exitCode = await new PageCommand().InvokeAsync([
30+
"--markdown", markdownPath,
31+
"--template", templateFolder,
32+
"--output", outputFolder]);
33+
34+
var pageOutputFolder = Path.Combine(outputFolder, "post");
35+
36+
Assert.AreEqual(0, exitCode);
37+
Assert.IsTrue(File.Exists(Path.Combine(pageOutputFolder, "index.html")), "index.html should be generated.");
38+
Assert.IsTrue(File.Exists(Path.Combine(pageOutputFolder, "styles.css")), "styles.css should be copied as a file.");
39+
Assert.IsFalse(Directory.Exists(Path.Combine(pageOutputFolder, "styles.css")), "styles.css must not be materialized as a folder.");
40+
Assert.IsTrue(File.Exists(Path.Combine(pageOutputFolder, "images", "logo.png")), "Nested template image should be copied.");
41+
Assert.IsTrue(File.Exists(Path.Combine(templateFolder, "styles.css")), "Template source asset should remain in place.");
42+
Assert.AreEqual(0, Directory.GetFiles(pageOutputFolder, "*.md", SearchOption.AllDirectories).Length, "Markdown source should not be copied into page output.");
43+
}
44+
finally
45+
{
46+
DeleteTempRoot(tempRoot);
47+
}
48+
}
49+
50+
[TestMethod]
51+
public async Task PagesCommand_CopiesTemplateAssetsAndReferencedSourceAssetsToRewrittenPaths()
52+
{
53+
var tempRoot = CreateTempRoot();
54+
55+
try
56+
{
57+
var sourceFolder = Path.Combine(tempRoot, "source");
58+
var templateFolder = Path.Combine(tempRoot, "template");
59+
var outputFolder = Path.Combine(tempRoot, "output");
60+
61+
Directory.CreateDirectory(Path.Combine(sourceFolder, "images"));
62+
Directory.CreateDirectory(Path.Combine(templateFolder, "images"));
63+
Directory.CreateDirectory(outputFolder);
64+
65+
await File.WriteAllTextAsync(Path.Combine(sourceFolder, "page1.md"), "---\ntitle: Page 1\n---\n\n# Page 1\n\n![Content](images/content.png)");
66+
await File.WriteAllTextAsync(Path.Combine(sourceFolder, "images", "content.png"), "content");
67+
await File.WriteAllTextAsync(Path.Combine(templateFolder, "template.html"), "<html><head><link rel=\"stylesheet\" href=\"styles.css\"></head><body><img src=\"images/template-logo.png\">{{ body }}</body></html>");
68+
await File.WriteAllTextAsync(Path.Combine(templateFolder, "styles.css"), "body { color: black; }");
69+
await File.WriteAllTextAsync(Path.Combine(templateFolder, "images", "template-logo.png"), "logo");
70+
71+
var exitCode = await new PagesCommand().InvokeAsync([
72+
"--markdown-folder", sourceFolder,
73+
"--template", templateFolder,
74+
"--output", outputFolder]);
75+
76+
var pageOutputFolder = Path.Combine(outputFolder, "page1");
77+
78+
Assert.AreEqual(0, exitCode);
79+
Assert.IsTrue(File.Exists(Path.Combine(pageOutputFolder, "index.html")), "Page index.html should be generated.");
80+
Assert.IsTrue(File.Exists(Path.Combine(pageOutputFolder, "styles.css")), "Template stylesheet should be copied into each page folder.");
81+
Assert.IsFalse(Directory.Exists(Path.Combine(pageOutputFolder, "styles.css")), "styles.css must not be materialized as a folder.");
82+
Assert.IsTrue(File.Exists(Path.Combine(pageOutputFolder, "images", "template-logo.png")), "Template image should be copied into each page folder.");
83+
Assert.IsTrue(File.Exists(Path.Combine(outputFolder, "images", "content.png")), "Referenced source image should be copied to the rewritten parent-relative path.");
84+
Assert.AreEqual(0, Directory.GetFiles(outputFolder, "*.md", SearchOption.AllDirectories).Length, "Markdown source should not be copied into multi-page output.");
85+
}
86+
finally
87+
{
88+
DeleteTempRoot(tempRoot);
89+
}
90+
}
91+
92+
private static string CreateTempRoot()
93+
{
94+
var tempRoot = Path.Combine(Path.GetTempPath(), "wac-blog-tests", Guid.NewGuid().ToString("N"));
95+
Directory.CreateDirectory(tempRoot);
96+
return tempRoot;
97+
}
98+
99+
private static void DeleteTempRoot(string tempRoot)
100+
{
101+
if (Directory.Exists(tempRoot))
102+
Directory.Delete(tempRoot, recursive: true);
103+
}
104+
}

0 commit comments

Comments
 (0)