Skip to content

Commit a940ff7

Browse files
committed
Rudimentary functionality of the blogging part is down.
1 parent 372edbe commit a940ff7

91 files changed

Lines changed: 61140 additions & 32 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Source/Dove.Blog.Abstractions/IDataProvider.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,12 @@ public interface IDataProvider
2828
/// <param name="encoding"></param>
2929
/// <returns></returns>
3030
Task WritePageContent(string pageName, string content, Encoding encoding = null!);
31+
32+
/// <summary>
33+
///
34+
/// </summary>
35+
/// <param name="path"></param>
36+
/// <param name="withExtension"></param>
37+
/// <returns></returns>
38+
Task<string[]> GetFileList(string path, bool withExtension = false);
3139
}

Source/Dove.Blog.Data/FileDataProvider.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,25 @@ public string RootPath
2323
}
2424
}
2525

26+
public Task<string[]> GetFileList(string path, bool withExtension = false)
27+
{
28+
var folderPath = Path.Combine(RootPath, path);
29+
if (Directory.Exists(folderPath))
30+
{
31+
var files = Directory.GetFiles(folderPath, "*.md", SearchOption.TopDirectoryOnly);
32+
return Task.FromResult(files.Select(x => withExtension ? Path.GetFileName(x) : Path.GetFileNameWithoutExtension(x)).ToArray());
33+
}
34+
return Task.FromResult(Array.Empty<string>());
35+
}
36+
2637
public Task<string> ReadPageContent(string pageName, Encoding encoding = null!)
2738
{
2839
var filePath = Path.Combine(RootPath, pageName + ".md");
40+
if(!File.Exists(filePath))
41+
{
42+
throw new FileNotFoundException($"No page named '{pageName}' was found.");
43+
}
44+
2945
var content = File.ReadAllTextAsync(filePath, encoding ?? Encoding.UTF8);
3046
return content;
3147
}

Source/Dove.Blog.Data/Models/Post.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ namespace Dove.Blog.Data;
22

33
public class Post
44
{
5-
public Guid Id { get; set; }
65
public required string Title { get; set; }
7-
public string? Description { get; set; }
8-
public string? Author { get; set; }
9-
public string? Category { get; set; }
10-
public string? UpdatedBy { get; set; }
11-
public required string FileName { get; set; }
12-
public required DateTimeOffset Created { get; set; }
13-
public required DateTimeOffset Updated { get; set; }
14-
}
6+
public required string Slug { get; set; }
7+
public string? Summary { get; set; }
8+
public string? Content { get; set; }
9+
public required string Author { get; set; }
10+
public string[]? Categories { get; set; }
11+
public string[]? Tags { get; set; }
12+
public required DateTimeOffset Posted { get; set; }
13+
public DateTimeOffset? Updated { get; set; }
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Dove.Blog.Data;
2+
3+
public class PostSummary
4+
{
5+
public required string Title { get; set; }
6+
public required string Slug { get; set; }
7+
public string? Summary { get; set; }
8+
public required string Author { get; set; }
9+
public required DateTimeOffset Posted { get; set; }
10+
public DateTimeOffset? Updated { get; set; }
11+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using Dove.Blog.Abstractions;
2+
using Markdig.Extensions.Yaml;
3+
using Markdig;
4+
using YamlDotNet.Serialization.NamingConventions;
5+
using YamlDotNet.Serialization;
6+
using Dove.Blog.Data;
7+
using Microsoft.Extensions.Caching.Memory;
8+
using Markdig.Syntax;
9+
using Microsoft.Extensions.Logging;
10+
using System.Text;
11+
using System.Text.RegularExpressions;
12+
13+
namespace Dove.Blog.Logic;
14+
15+
public interface IBlogProvider
16+
{
17+
Task<IEnumerable<(string category, int posts)>> GetCategories();
18+
Task<Post> GetPost(string? postTitle);
19+
Task<IEnumerable<string>> GetTags();
20+
}
21+
22+
public class BlogProvider(IDataProvider dataProvider, IMemoryCache memoryCache, ILogger<BlogProvider> logger) : IBlogProvider
23+
{
24+
protected readonly IDataProvider _provider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider));
25+
protected readonly IMemoryCache _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
26+
private readonly ILogger<BlogProvider> _logger = logger ?? throw new ArgumentNullException(nameof(logger));
27+
28+
private static Regex _htmlRegex = new Regex("<.*?>", RegexOptions.Compiled);
29+
30+
public async Task<IEnumerable<string>> GetTags()
31+
{
32+
try
33+
{
34+
var files = await _provider.GetFileList("Posts");
35+
var posts = files.Select(x => GetPost(x).Result)
36+
.ToList();
37+
38+
var tags = posts.Where(p => p.Tags != null)
39+
.SelectMany(p => p.Tags!)
40+
.Distinct()
41+
.ToArray();
42+
return tags;
43+
44+
}
45+
catch (Exception ex)
46+
{
47+
_logger.LogError(ex, "Error loading tags");
48+
throw;
49+
}
50+
}
51+
52+
public async Task<IEnumerable<(string category, int posts)>> GetCategories()
53+
{
54+
try
55+
{
56+
var result = new Dictionary<string, int>();
57+
58+
var files = await _provider.GetFileList("Posts");
59+
foreach (var fileName in files)
60+
{
61+
var post = await GetPost(fileName);
62+
if (post.Categories != null)
63+
{
64+
foreach (var category in post.Categories)
65+
{
66+
if (result.ContainsKey(category))
67+
{
68+
result[category]++;
69+
}
70+
else
71+
{
72+
result.Add(category, 1);
73+
}
74+
}
75+
}
76+
}
77+
78+
return result.Select(x => (x.Key, x.Value));
79+
}
80+
catch (Exception ex)
81+
{
82+
_logger.LogError(ex, "Error loading categories");
83+
throw;
84+
}
85+
}
86+
87+
public async Task<IEnumerable<PostSummary>> GetPosts(string? category = null, params string[] tags)
88+
{
89+
try
90+
{
91+
var files = await _provider.GetFileList("Posts");
92+
var posts = files.Select(x => GetPost(x).Result)
93+
.ToList();
94+
95+
if (!string.IsNullOrEmpty(category))
96+
{
97+
posts = posts.Where(p => p.Categories != null && p.Categories.Contains(category))
98+
.ToList();
99+
}
100+
if (tags.Length > 0)
101+
{
102+
posts = posts.Where(p => p.Tags != null && p.Tags.Intersect(tags).Any())
103+
.ToList();
104+
}
105+
106+
107+
108+
return posts.Select(p =>
109+
{
110+
return new PostSummary
111+
{
112+
Slug = p.Slug,
113+
Title = p.Title,
114+
Summary = p.Summary ?? _htmlRegex.Replace(p.Content?.Substring(0, 200) + " ...", string.Empty),
115+
Author = p.Author,
116+
Posted = p.Posted,
117+
Updated = p.Updated
118+
};
119+
});
120+
121+
}
122+
catch (Exception ex)
123+
{
124+
_logger.LogError(ex, "Error loading posts");
125+
throw;
126+
}
127+
}
128+
129+
public async Task<Post> GetPost(string? postTitle)
130+
{
131+
ArgumentNullException.ThrowIfNull(postTitle, nameof(postTitle));
132+
try
133+
{
134+
Post? postObject = (_cache.TryGetValue("Post/" + postTitle!, out var page) ? page : null) as Post;
135+
if (postObject != null)
136+
{
137+
return postObject;
138+
}
139+
140+
var markdown = await _provider.ReadPageContent($"Posts/{postTitle}");
141+
var pipeline = new MarkdownPipelineBuilder()
142+
.UseYamlFrontMatter()
143+
.UseEmojiAndSmiley()
144+
.Build();
145+
146+
var document = Markdown.Parse(markdown, pipeline);
147+
var yamlBlock = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
148+
if (yamlBlock != null)
149+
{
150+
var yaml = markdown.Substring(yamlBlock.Span.Start + 3, yamlBlock.Span.Length - 6);
151+
markdown = markdown.Substring(yamlBlock.Span.Length + 1);
152+
document = Markdown.Parse(markdown, pipeline);
153+
154+
var deserializer = new DeserializerBuilder()
155+
.WithNamingConvention(UnderscoredNamingConvention.Instance)
156+
.Build();
157+
158+
postObject = deserializer.Deserialize<Post>(yaml);
159+
}
160+
161+
postObject ??= new Post
162+
{
163+
Slug = Slugify(postTitle),
164+
Author = string.Empty,
165+
Posted = DateTimeOffset.Now,
166+
Title = postTitle,
167+
};
168+
169+
postObject.Content = document.ToHtml();
170+
171+
_cache.Set("Post/" + postTitle!, postObject);
172+
return postObject;
173+
174+
}
175+
catch (Exception ex)
176+
{
177+
_logger.LogError(ex, "Error loading post {postTitle}", postTitle);
178+
throw;
179+
}
180+
}
181+
182+
private string Slugify(string text)
183+
{
184+
var cleaned = Regex.Replace(text, @"\s+", " ");
185+
var trimmed = cleaned.Replace(" ", "-");
186+
var normalized = trimmed.Normalize(NormalizationForm.FormD);
187+
var finalized = Regex.Replace(normalized, @"[^a-zA-Z0-9\-\._]", string.Empty);
188+
189+
return finalized.ToLower();
190+
}
191+
192+
private static string RemoveHTMLTagsCompiled(string html)
193+
{
194+
return _htmlRegex.Replace(html, string.Empty);
195+
}
196+
197+
}

Source/Dove.Blog.Logic/PageDataProvider.cs

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,51 @@
1-
using System;
21
using Dove.Blog.Abstractions;
32
using Dove.Blog.Data;
43
using Markdig;
54
using Markdig.Extensions.Yaml;
65
using Markdig.Syntax;
7-
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.Extensions.Caching.Memory;
87
using YamlDotNet.Serialization;
98
using YamlDotNet.Serialization.NamingConventions;
109

1110
namespace Dove.Blog.Logic;
1211

13-
public class PageDataProvider(IDataProvider provider)
12+
public class PageDataProvider(IDataProvider provider, IMemoryCache memoryCache)
1413
{
1514
private readonly IDataProvider _provider = provider ?? throw new ArgumentNullException(nameof(provider));
15+
private readonly IMemoryCache _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
16+
17+
public async Task<IEnumerable<Page>> GetPages()
18+
{
19+
var pages = new List<Page>();
20+
var pageNames = await _provider.GetFileList("Pages");
21+
foreach (var pageName in pageNames)
22+
{
23+
var page = await GetPageFrontMatter(pageName);
24+
if (page != null)
25+
{
26+
pages.Add(page);
27+
}
28+
}
29+
return pages;
30+
}
1631

1732
public async Task<Page> GetPage(string? pageName)
1833
{
19-
Page? pageObject = null;
34+
ArgumentNullException.ThrowIfNull(pageName, nameof(pageName));
35+
36+
Page? pageObject = (_cache.TryGetValue("Page/" + pageName!, out var page) ? page : null) as Page;
37+
if(pageObject != null)
38+
{
39+
return pageObject;
40+
}
2041

2142
var markdown = await _provider.ReadPageContent($"Pages/{pageName}");
22-
var pipeline = new MarkdownPipelineBuilder().UseYamlFrontMatter().UseEmojiAndSmiley().Build();
23-
var document = Markdown.Parse(markdown, pipeline);
43+
var pipeline = new MarkdownPipelineBuilder()
44+
.UseYamlFrontMatter()
45+
.UseEmojiAndSmiley()
46+
.Build();
2447

48+
var document = Markdown.Parse(markdown, pipeline);
2549
var yamlBlock = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
2650
if (yamlBlock != null)
2751
{
@@ -32,6 +56,7 @@ public async Task<Page> GetPage(string? pageName)
3256
var deserializer = new DeserializerBuilder()
3357
.WithNamingConvention(UnderscoredNamingConvention.Instance)
3458
.Build();
59+
3560
pageObject = deserializer.Deserialize<Page>(yaml);
3661
}
3762

@@ -43,6 +68,35 @@ public async Task<Page> GetPage(string? pageName)
4368
};
4469
pageObject.Content = document.ToHtml();
4570

71+
_cache.Set("Page/" + pageName!, pageObject);
4672
return pageObject;
4773
}
74+
75+
private async Task<Page?> GetPageFrontMatter(string pageName)
76+
{
77+
ArgumentNullException.ThrowIfNull(pageName, nameof(pageName));
78+
79+
var markdown = await _provider.ReadPageContent($"Pages/{pageName}");
80+
var pipeline = new MarkdownPipelineBuilder()
81+
.UseYamlFrontMatter()
82+
.Build();
83+
84+
var document = Markdown.Parse(markdown, pipeline);
85+
var yamlBlock = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
86+
if (yamlBlock != null)
87+
{
88+
var yaml = markdown.Substring(yamlBlock.Span.Start + 3, yamlBlock.Span.Length - 6);
89+
markdown = markdown.Substring(yamlBlock.Span.Length + 1);
90+
document = Markdown.Parse(markdown, pipeline);
91+
92+
var deserializer = new DeserializerBuilder()
93+
.WithNamingConvention(UnderscoredNamingConvention.Instance)
94+
.Build();
95+
96+
var pageObject = deserializer.Deserialize<Page>(yaml);
97+
return pageObject;
98+
}
99+
100+
return null;
101+
}
48102
}

0 commit comments

Comments
 (0)