2024-02-20 20:36:23 +00:00
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
using Cysharp.Text;
|
2024-04-27 15:41:19 +01:00
|
|
|
using Humanizer;
|
2024-02-20 20:36:23 +00:00
|
|
|
using Markdig;
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
2024-02-23 17:50:40 +00:00
|
|
|
using OliverBooth.Data;
|
2024-05-01 16:47:31 +01:00
|
|
|
using OliverBooth.Data.Blog;
|
2024-02-20 20:36:23 +00:00
|
|
|
using OliverBooth.Data.Web;
|
|
|
|
|
|
|
|
namespace OliverBooth.Services;
|
|
|
|
|
|
|
|
internal sealed class TutorialService : ITutorialService
|
|
|
|
{
|
2024-05-01 16:47:31 +01:00
|
|
|
private readonly IDbContextFactory<BlogContext> _blogContextFactory;
|
2024-02-20 20:36:23 +00:00
|
|
|
private readonly IDbContextFactory<WebContext> _dbContextFactory;
|
|
|
|
private readonly MarkdownPipeline _markdownPipeline;
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the <see cref="TutorialService" /> class.
|
|
|
|
/// </summary>
|
2024-05-01 16:47:31 +01:00
|
|
|
/// <param name="dbContextFactory">The <see cref="WebContext" /> factory.</param>
|
|
|
|
/// <param name="blogContextFactory">The <see cref="BlogContext" /> factory.</param>
|
2024-02-20 20:36:23 +00:00
|
|
|
/// <param name="markdownPipeline">The <see cref="MarkdownPipeline" />.</param>
|
2024-05-01 16:47:31 +01:00
|
|
|
public TutorialService(IDbContextFactory<WebContext> dbContextFactory,
|
|
|
|
IDbContextFactory<BlogContext> blogContextFactory,
|
|
|
|
MarkdownPipeline markdownPipeline)
|
2024-02-20 20:36:23 +00:00
|
|
|
{
|
|
|
|
_dbContextFactory = dbContextFactory;
|
|
|
|
_markdownPipeline = markdownPipeline;
|
2024-05-01 16:47:31 +01:00
|
|
|
_blogContextFactory = blogContextFactory;
|
2024-02-20 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2024-02-23 17:50:40 +00:00
|
|
|
public IReadOnlyCollection<ITutorialArticle> GetArticles(ITutorialFolder folder,
|
|
|
|
Visibility visibility = Visibility.None)
|
2024-02-20 20:36:23 +00:00
|
|
|
{
|
|
|
|
if (folder is null) throw new ArgumentNullException(nameof(folder));
|
|
|
|
|
|
|
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
2024-02-23 17:50:40 +00:00
|
|
|
IQueryable<TutorialArticle> articles = context.TutorialArticles.Where(a => a.Folder == folder.Id);
|
|
|
|
|
|
|
|
if (visibility != Visibility.None) articles = articles.Where(a => a.Visibility == visibility);
|
|
|
|
return articles.ToArray();
|
2024-02-20 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
2024-02-23 17:50:40 +00:00
|
|
|
public IReadOnlyCollection<ITutorialFolder> GetFolders(ITutorialFolder? parent = null,
|
|
|
|
Visibility visibility = Visibility.None)
|
2024-02-20 20:36:23 +00:00
|
|
|
{
|
|
|
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
2024-02-23 17:50:40 +00:00
|
|
|
IQueryable<TutorialFolder> folders = context.TutorialFolders;
|
|
|
|
|
|
|
|
folders = parent is null ? folders.Where(f => f.Parent == null) : folders.Where(f => f.Parent == parent.Id);
|
|
|
|
if (visibility != Visibility.None) folders = folders.Where(a => a.Visibility == visibility);
|
|
|
|
return folders.ToArray();
|
2024-02-20 20:36:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public ITutorialFolder? GetFolder(int id)
|
|
|
|
{
|
|
|
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return context.TutorialFolders.FirstOrDefault(f => f.Id == id);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
[return: NotNullIfNotNull(nameof(slug))]
|
|
|
|
public ITutorialFolder? GetFolder(string? slug, ITutorialFolder? parent = null)
|
|
|
|
{
|
|
|
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
return parent is null
|
|
|
|
? context.TutorialFolders.FirstOrDefault(a => a.Slug == slug)
|
|
|
|
: context.TutorialFolders.FirstOrDefault(a => a.Slug == slug && a.Parent == parent.Id);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public string GetFullSlug(ITutorialFolder folder)
|
|
|
|
{
|
|
|
|
if (folder is null) throw new ArgumentNullException(nameof(folder));
|
|
|
|
|
|
|
|
var folderStack = new Stack<ITutorialFolder>();
|
|
|
|
folderStack.Push(folder);
|
|
|
|
|
|
|
|
while (folder.Parent is { } parentId)
|
|
|
|
{
|
|
|
|
ITutorialFolder? current = GetFolder(parentId);
|
|
|
|
if (current is null) break;
|
|
|
|
folderStack.Push(current);
|
|
|
|
}
|
|
|
|
|
|
|
|
using var builder = ZString.CreateUtf8StringBuilder();
|
|
|
|
|
|
|
|
while (folderStack.Count > 0)
|
|
|
|
{
|
|
|
|
builder.Append(folderStack.Pop().Slug);
|
|
|
|
|
|
|
|
if (folderStack.Count > 0)
|
|
|
|
builder.Append('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
return builder.ToString();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public string GetFullSlug(ITutorialArticle article)
|
|
|
|
{
|
|
|
|
if (article is null) throw new ArgumentNullException(nameof(article));
|
|
|
|
ITutorialFolder? folder = GetFolder(article.Folder);
|
|
|
|
if (folder is null) return article.Slug;
|
|
|
|
return $"{GetFullSlug(folder)}/{article.Slug}";
|
|
|
|
}
|
|
|
|
|
2024-05-01 16:47:31 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public int GetLegacyCommentCount(ITutorialArticle article)
|
|
|
|
{
|
|
|
|
if (article.RedirectFrom is not { } postId)
|
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
using BlogContext context = _blogContextFactory.CreateDbContext();
|
|
|
|
return context.LegacyComments.Count(c => c.PostId == postId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public IReadOnlyList<ILegacyComment> GetLegacyComments(ITutorialArticle article)
|
|
|
|
{
|
|
|
|
if (article.RedirectFrom is not { } postId)
|
|
|
|
{
|
|
|
|
return ArraySegment<ILegacyComment>.Empty;
|
|
|
|
}
|
|
|
|
|
|
|
|
using BlogContext context = _blogContextFactory.CreateDbContext();
|
|
|
|
return context.LegacyComments.Where(c => c.PostId == postId && c.ParentComment == null).ToArray();
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public IReadOnlyList<ILegacyComment> GetLegacyReplies(ILegacyComment comment)
|
|
|
|
{
|
|
|
|
using BlogContext context = _blogContextFactory.CreateDbContext();
|
|
|
|
return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray();
|
|
|
|
}
|
|
|
|
|
2024-02-20 20:36:23 +00:00
|
|
|
/// <inheritdoc />
|
|
|
|
public string RenderArticle(ITutorialArticle article)
|
|
|
|
{
|
|
|
|
return Markdig.Markdown.ToHtml(article.Body, _markdownPipeline);
|
|
|
|
}
|
|
|
|
|
2024-04-27 15:41:19 +01:00
|
|
|
/// <inheritdoc />
|
|
|
|
public string RenderExcerpt(ITutorialArticle article, out bool wasTrimmed)
|
|
|
|
{
|
|
|
|
if (!string.IsNullOrWhiteSpace(article.Excerpt))
|
|
|
|
{
|
|
|
|
wasTrimmed = false;
|
|
|
|
return Markdig.Markdown.ToHtml(article.Excerpt, _markdownPipeline);
|
|
|
|
}
|
|
|
|
|
|
|
|
string body = article.Body;
|
|
|
|
int moreIndex = body.IndexOf("<!--more-->", StringComparison.Ordinal);
|
|
|
|
|
|
|
|
if (moreIndex == -1)
|
|
|
|
{
|
|
|
|
string excerpt = body.Truncate(255, "...");
|
|
|
|
wasTrimmed = body.Length > 255;
|
|
|
|
return Markdig.Markdown.ToHtml(excerpt, _markdownPipeline);
|
|
|
|
}
|
|
|
|
|
|
|
|
wasTrimmed = true;
|
|
|
|
return Markdig.Markdown.ToHtml(body[..moreIndex], _markdownPipeline);
|
|
|
|
}
|
|
|
|
|
2024-02-20 20:36:23 +00:00
|
|
|
/// <inheritdoc />
|
|
|
|
public bool TryGetArticle(int id, [NotNullWhen(true)] out ITutorialArticle? article)
|
|
|
|
{
|
|
|
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
article = context.TutorialArticles.FirstOrDefault(a => a.Id == id);
|
|
|
|
return article is not null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
public bool TryGetArticle(string slug, [NotNullWhen(true)] out ITutorialArticle? article)
|
|
|
|
{
|
|
|
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
|
|
if (slug is null)
|
|
|
|
{
|
|
|
|
article = null;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
string[] tokens = slug.Split('/');
|
|
|
|
ITutorialFolder? folder = null;
|
|
|
|
|
|
|
|
for (var index = 0; index < tokens.Length - 1; index++)
|
|
|
|
{
|
|
|
|
folder = GetFolder(tokens[index], folder);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (folder is null)
|
|
|
|
{
|
|
|
|
article = null;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
using WebContext context = _dbContextFactory.CreateDbContext();
|
|
|
|
slug = tokens[^1];
|
|
|
|
article = context.TutorialArticles.FirstOrDefault(a => a.Slug == slug && a.Folder == folder.Id);
|
|
|
|
return article is not null;
|
|
|
|
}
|
|
|
|
}
|