Skip to content

Commit c62c788

Browse files
committed
page file helper made more robust
1 parent 28cccee commit c62c788

5 files changed

Lines changed: 335 additions & 325 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ bld/
3232
[Bb]in/
3333
[Oo]bj/
3434
[Ll]og/
35+
[Pp]ublish/
3536

3637
# Visual Studio 2015/2017 cache/options directory
3738
.vs/

src/Ditto/src/PageCollection.cs

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
namespace Ditto;
2-
3-
public sealed record PageCollection(
4-
string Name,
5-
IReadOnlyList<Page> Pages);
6-
7-
public static class PageCollectionFactory {
8-
public static IEnumerable<PageCollection> Create(IEnumerable<Page> pages) {
9-
var pageDict = new Dictionary<string, List<Page>>();
10-
11-
// collections are derived from the first segment of the url
12-
// only pages with n > 1 subpages are included in collections
13-
foreach (var page in pages) {
14-
if (page.Path.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) is string[] segments
15-
&& segments.Length > 1) {
16-
var collectionName = segments[0];
17-
18-
if (pageDict.TryGetValue(collectionName, out var value)) {
19-
pageDict[collectionName].Add(page);
20-
}
21-
else {
22-
pageDict[collectionName] = [page];
23-
}
24-
}
25-
}
26-
27-
foreach (var kvp in pageDict) {
28-
yield return new(kvp.Key, kvp.Value.AsReadOnly());
29-
}
30-
}
31-
}
1+
namespace Ditto;
2+
3+
public sealed record PageCollection(
4+
string Name,
5+
IReadOnlyList<Page> Pages);
6+
7+
public static class PageCollectionFactory {
8+
public static IEnumerable<PageCollection> Create(IEnumerable<Page> pages) {
9+
var pageDict = new Dictionary<string, List<Page>>();
10+
11+
// collections are derived from the first segment of the url
12+
// only pages with n > 1 subpages are included in collections
13+
foreach (var page in pages) {
14+
if (page.Path.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) is string[] segments
15+
&& segments.Length > 1) {
16+
var collectionName = segments[0];
17+
18+
if (pageDict.TryGetValue(collectionName, out var value)) {
19+
pageDict[collectionName].Add(page);
20+
}
21+
else {
22+
pageDict[collectionName] = [page];
23+
}
24+
}
25+
}
26+
27+
foreach (var kvp in pageDict) {
28+
// sort the pages alpha by path (to support date-based filenames)
29+
yield return new(kvp.Key, [.. kvp.Value.OrderBy(x => x.Path)]);
30+
}
31+
}
32+
}

src/Ditto/src/PageFile.cs

Lines changed: 91 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,91 @@
1-
namespace Ditto;
2-
3-
public sealed record PageFile(
4-
string InputPath,
5-
string OutputPath,
6-
string Path,
7-
string PageName,
8-
ViewType ViewType);
9-
10-
public interface IPageFileLoader {
11-
IReadOnlyList<PageFile> LoadFiles();
12-
}
13-
14-
public sealed class PageFileLoader(string basePath, string outputPath) : IPageFileLoader {
15-
private readonly string _indexFileName = string.Concat("index", Website.TemplateExtension);
16-
public IReadOnlyList<PageFile> LoadFiles() {
17-
var pages = new List<PageFile>();
18-
var filePaths =
19-
Directory.GetFiles(basePath, string.Concat("*", Website.TemplateExtension), SearchOption.AllDirectories)
20-
.Union(Directory.GetFiles(basePath, "*.md", SearchOption.AllDirectories));
21-
22-
foreach (var filePath in filePaths) {
23-
var relativeFilePath = Path.GetRelativePath(basePath, filePath);
24-
25-
// skip files in the layouts and partials directory
26-
if (relativeFilePath.StartsWith(Website.LayoutsDirectory, StringComparison.OrdinalIgnoreCase)
27-
|| relativeFilePath.StartsWith(Website.PartialsDirectory, StringComparison.OrdinalIgnoreCase)) {
28-
continue;
29-
}
30-
31-
// If the file is named index.html, it should map to the output path
32-
// directly. Otherwise, a file named index.html should be placed in
33-
// a subdirectory with the same name as the original file.
34-
var outputFilePath = Equals(relativeFilePath, _indexFileName)
35-
? Path.Join(outputPath, relativeFilePath)
36-
: Path.Join(
37-
outputPath,
38-
Path.GetDirectoryName(relativeFilePath),
39-
Path.GetFileNameWithoutExtension(relativeFilePath),
40-
_indexFileName);
41-
42-
pages.Add(new(
43-
InputPath: filePath,
44-
OutputPath: outputFilePath,
45-
Path: PageFileHelper.GetPath(basePath, filePath),
46-
PageName: PageFileHelper.GetPageName(basePath, filePath),
47-
ViewType: PageFileHelper.GetViewType(filePath)));
48-
}
49-
50-
return pages;
51-
}
52-
}
53-
54-
internal static class PageFileHelper {
55-
internal static string GetPageName(string basePath, string filePath) {
56-
var pageName = "index";
57-
if (Path.GetRelativePath(basePath, filePath) is string relativePath
58-
&& !Equals(relativePath, "index.html")
59-
&& !Equals(relativePath, filePath)) {
60-
// strip extension, replace backslashes with forward slashes and
61-
// prepend + append forward slashes
62-
pageName = relativePath[..^Path.GetExtension(relativePath).Length].Replace("\\", "/");
63-
}
64-
return pageName;
65-
}
66-
67-
internal static string GetPath(string basePath, string filePath) {
68-
var path = "/";
69-
if (GetPageName(basePath, filePath) is string pageName
70-
&& !Equals("index", pageName)) {
71-
// strip extension, replace backslashes with forward slashes and
72-
// prepend + append forward slashes
73-
path = string.Concat("/", pageName, "/");
74-
}
75-
return path;
76-
}
77-
78-
internal static ViewType GetViewType(string filePath) =>
79-
string.Equals(Path.GetExtension(filePath), ".md", StringComparison.OrdinalIgnoreCase)
80-
? ViewType.Markdown
81-
: ViewType.Html;
82-
83-
}
1+
namespace Ditto;
2+
3+
public sealed record PageFile(
4+
string InputPath,
5+
string OutputPath,
6+
string Path,
7+
string PageName,
8+
ViewType ViewType);
9+
10+
public interface IPageFileLoader {
11+
IReadOnlyList<PageFile> LoadFiles();
12+
}
13+
14+
public sealed class PageFileLoader(string basePath, string outputPath) : IPageFileLoader {
15+
private readonly string _indexFileName = string.Concat("index", Website.TemplateExtension);
16+
public IReadOnlyList<PageFile> LoadFiles() {
17+
var pages = new List<PageFile>();
18+
var filePaths =
19+
Directory.GetFiles(basePath, string.Concat("*", Website.TemplateExtension), SearchOption.AllDirectories)
20+
.Union(Directory.GetFiles(basePath, "*.md", SearchOption.AllDirectories));
21+
22+
foreach (var filePath in filePaths) {
23+
var relativeFilePath = Path.GetRelativePath(basePath, filePath);
24+
25+
// skip files in the layouts and partials directory
26+
if (relativeFilePath.StartsWith(Website.LayoutsDirectory, StringComparison.OrdinalIgnoreCase)
27+
|| relativeFilePath.StartsWith(Website.PartialsDirectory, StringComparison.OrdinalIgnoreCase)) {
28+
continue;
29+
}
30+
31+
// If the file is named index.html, it should map to the output path
32+
// directly. Otherwise, a file named index.html should be placed in
33+
// a subdirectory with the same name as the original file.
34+
var outputFilePath = Equals(relativeFilePath, _indexFileName)
35+
? Path.Join(outputPath, relativeFilePath)
36+
: Path.Join(
37+
outputPath,
38+
Path.GetDirectoryName(relativeFilePath),
39+
Path.GetFileNameWithoutExtension(relativeFilePath),
40+
_indexFileName);
41+
42+
pages.Add(new(
43+
InputPath: filePath,
44+
OutputPath: outputFilePath,
45+
Path: PageFileHelper.GetPath(basePath, filePath),
46+
PageName: PageFileHelper.GetPageName(basePath, filePath),
47+
ViewType: PageFileHelper.GetViewType(filePath)));
48+
}
49+
50+
return pages;
51+
}
52+
}
53+
54+
internal static class PageFileHelper {
55+
internal static string GetPageName(string basePath, string filePath) {
56+
var pageName = "index";
57+
if (GetRelativePath(basePath, filePath) is string relativePath
58+
&& !Equals(relativePath, "index.html")
59+
&& !Equals(relativePath, filePath)) {
60+
// strip extension, replace backslashes with forward slashes and
61+
// prepend + append forward slashes
62+
pageName = relativePath[..^Path.GetExtension(relativePath).Length].Replace("\\", "/");
63+
}
64+
return pageName;
65+
}
66+
67+
internal static string GetPath(string basePath, string filePath) {
68+
var path = "/";
69+
if (GetPageName(basePath, filePath) is string pageName
70+
&& !Equals("index", pageName)) {
71+
// strip extension, replace backslashes with forward slashes and
72+
// prepend + append forward slashes
73+
path = string.Concat("/", pageName, "/");
74+
}
75+
return path;
76+
}
77+
78+
internal static ViewType GetViewType(string filePath) =>
79+
string.Equals(Path.GetExtension(filePath), ".md", StringComparison.OrdinalIgnoreCase)
80+
? ViewType.Markdown
81+
: ViewType.Html;
82+
83+
private static string? GetRelativePath(string basePath, string filePath) {
84+
// we define our own method here to avoid issues with Path.GetRelativePath
85+
// on different platforms
86+
if (filePath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase)) {
87+
return filePath[basePath.Length..].TrimStart('/', '\\');
88+
}
89+
return default;
90+
}
91+
}

src/Ditto/test/PageFileTests.cs

Lines changed: 60 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,60 @@
1-
namespace Ditto.Tests;
2-
3-
public sealed class PageFileLoaderTests {
4-
[Fact]
5-
public void LoadFiles_ReturnsPageFiles_WhenValidInput() {
6-
var pageFileLoader = new PageFileLoader(Shared.BasePath, Shared.OutputPath);
7-
var pageFiles = pageFileLoader.LoadFiles();
8-
9-
Assert.NotNull(pageFiles);
10-
Assert.NotEmpty(pageFiles);
11-
Assert.Equal(5, pageFiles.Count);
12-
Assert.All(pageFiles, pageFile => {
13-
Assert.NotNull(pageFile.InputPath);
14-
Assert.NotNull(pageFile.OutputPath);
15-
Assert.NotNull(pageFile.Path);
16-
Assert.NotNull(pageFile.PageName);
17-
18-
Assert.True(File.Exists(pageFile.InputPath));
19-
Assert.Equal("index.html", Path.GetFileName(pageFile.OutputPath)); // every page file should be named index.html
20-
// Assert.Contains(pageFile.PageName, pageFile.OutputPath);
21-
22-
if (Path.GetRelativePath(Shared.BasePath, pageFile.InputPath) == "index.html") {
23-
// the index page should map to the output path directly
24-
Assert.Equal("index.html", Path.GetRelativePath(Shared.OutputPath, pageFile.OutputPath));
25-
}
26-
});
27-
}
28-
}
29-
30-
public sealed class PageFileHelperTests {
31-
[Theory]
32-
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\index.html", "/")]
33-
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\about.html", "/about/")]
34-
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\blog\post1.html", "/blog/post1/")]
35-
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/index.html", "/")]
36-
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/about.html", "/about/")]
37-
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/blog/post1.html", "/blog/post1/")]
38-
public void GetPath_ReturnsExpectedPath(string basePath, string filePath, string expectedPath) {
39-
Assert.Equal(expectedPath, PageFileHelper.GetPath(basePath, filePath));
40-
}
41-
42-
[Theory]
43-
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\index.html", "index")]
44-
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\about.html", "about")]
45-
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\blog\post1.html", "blog/post1")]
46-
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/index.html", "index")]
47-
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/about.html", "about")]
48-
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/blog/post1.html", "blog/post1")]
49-
public void GetPageName_ReturnsExpected(string basePath, string filePath, string expected) {
50-
Assert.Equal(expected, PageFileHelper.GetPageName(basePath, filePath));
51-
}
52-
53-
[Theory]
54-
[InlineData("about.md", ViewType.Markdown)]
55-
[InlineData("contact.html", ViewType.Html)]
56-
public void GetViewType_ReturnsExpectedViewType(string fileName, ViewType expectedViewType) {
57-
var viewType = PageFileHelper.GetViewType(fileName);
58-
Assert.Equal(expectedViewType, viewType);
59-
}
60-
}
1+
namespace Ditto.Tests;
2+
3+
public sealed class PageFileLoaderTests {
4+
[Fact]
5+
public void LoadFiles_ReturnsPageFiles_WhenValidInput() {
6+
var pageFileLoader = new PageFileLoader(Shared.BasePath, Shared.OutputPath);
7+
var pageFiles = pageFileLoader.LoadFiles();
8+
9+
Assert.NotNull(pageFiles);
10+
Assert.NotEmpty(pageFiles);
11+
Assert.Equal(5, pageFiles.Count);
12+
Assert.All(pageFiles, pageFile => {
13+
Assert.NotNull(pageFile.InputPath);
14+
Assert.NotNull(pageFile.OutputPath);
15+
Assert.NotNull(pageFile.Path);
16+
Assert.NotNull(pageFile.PageName);
17+
18+
Assert.True(File.Exists(pageFile.InputPath));
19+
Assert.Equal("index.html", Path.GetFileName(pageFile.OutputPath)); // every page file should be named index.html
20+
// Assert.Contains(pageFile.PageName, pageFile.OutputPath);
21+
22+
if (Path.GetRelativePath(Shared.BasePath, pageFile.InputPath) == "index.html") {
23+
// the index page should map to the output path directly
24+
Assert.Equal("index.html", Path.GetRelativePath(Shared.OutputPath, pageFile.OutputPath));
25+
}
26+
});
27+
}
28+
}
29+
30+
public sealed class PageFileHelperTests {
31+
[Theory]
32+
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\index.html", "/")]
33+
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\about.html", "/about/")]
34+
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\blog\post1.html", "/blog/post1/")]
35+
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/index.html", "/")]
36+
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/about.html", "/about/")]
37+
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/blog/post1.html", "/blog/post1/")]
38+
public void GetPath_ReturnsExpectedPath(string basePath, string filePath, string expectedPath) {
39+
Assert.Equal(expectedPath, PageFileHelper.GetPath(basePath, filePath));
40+
}
41+
42+
[Theory]
43+
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\index.html", "index")]
44+
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\about.html", "about")]
45+
[InlineData(@"C:\users\ditto\website\src", @"C:\users\ditto\website\src\blog\post1.html", "blog/post1")]
46+
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/index.html", "index")]
47+
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/about.html", "about")]
48+
[InlineData(@"/users/ditto/website/src", @"/users/ditto/website/src/blog/post1.html", "blog/post1")]
49+
public void GetPageName_ReturnsExpected(string basePath, string filePath, string expected) {
50+
Assert.Equal(expected, PageFileHelper.GetPageName(basePath, filePath));
51+
}
52+
53+
[Theory]
54+
[InlineData("about.md", ViewType.Markdown)]
55+
[InlineData("contact.html", ViewType.Html)]
56+
[InlineData("post", ViewType.Html)]
57+
public void GetViewType_ReturnsExpectedViewType(string fileName, ViewType expectedViewType) {
58+
Assert.Equal(expectedViewType, PageFileHelper.GetViewType(fileName));
59+
}
60+
}

0 commit comments

Comments
 (0)