using System.Diagnostics.CodeAnalysis; using Cysharp.Text; using Humanizer; using Markdig; using Microsoft.EntityFrameworkCore; using OliverBooth.Common.Data; using OliverBooth.Common.Data.Blog; using OliverBooth.Common.Data.Web; using OliverBooth.Common.Services; using OliverBooth.Data.Blog; using OliverBooth.Data.Web; namespace OliverBooth.Services; internal sealed class TutorialService : ITutorialService { private readonly IDbContextFactory _blogContextFactory; private readonly IDbContextFactory _dbContextFactory; private readonly MarkdownPipeline _markdownPipeline; /// /// Initializes a new instance of the class. /// /// The factory. /// The factory. /// The . public TutorialService(IDbContextFactory dbContextFactory, IDbContextFactory blogContextFactory, MarkdownPipeline markdownPipeline) { _dbContextFactory = dbContextFactory; _markdownPipeline = markdownPipeline; _blogContextFactory = blogContextFactory; } /// public IReadOnlyCollection GetArticles(ITutorialFolder folder, Visibility visibility = Visibility.None) { if (folder is null) throw new ArgumentNullException(nameof(folder)); using WebContext context = _dbContextFactory.CreateDbContext(); IQueryable articles = context.TutorialArticles.Where(a => a.Folder == folder.Id); if (visibility != Visibility.None) articles = articles.Where(a => a.Visibility == visibility); return articles.ToArray(); } /// public IReadOnlyCollection GetFolders(ITutorialFolder? parent = null, Visibility visibility = Visibility.None) { using WebContext context = _dbContextFactory.CreateDbContext(); IQueryable 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(); } /// public ITutorialFolder? GetFolder(int id) { using WebContext context = _dbContextFactory.CreateDbContext(); return context.TutorialFolders.FirstOrDefault(f => f.Id == id); } /// [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); } /// public string GetFullSlug(ITutorialFolder folder) { if (folder is null) throw new ArgumentNullException(nameof(folder)); var folderStack = new Stack(); 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(); } /// 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}"; } /// 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); } /// public IReadOnlyList GetLegacyComments(ITutorialArticle article) { if (article.RedirectFrom is not { } postId) { return ArraySegment.Empty; } using BlogContext context = _blogContextFactory.CreateDbContext(); return context.LegacyComments.Where(c => c.PostId == postId && c.ParentComment == null).ToArray(); } /// public IReadOnlyList GetLegacyReplies(ILegacyComment comment) { using BlogContext context = _blogContextFactory.CreateDbContext(); return context.LegacyComments.Where(c => c.ParentComment == comment.Id).ToArray(); } /// public string RenderArticle(ITutorialArticle article) { return Markdig.Markdown.ToHtml(article.Body, _markdownPipeline); } /// 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("", 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); } /// 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; } /// 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; } }