using System.Diagnostics.CodeAnalysis; using Cysharp.Text; using Markdig; using Microsoft.EntityFrameworkCore; using OliverBooth.Data.Web; namespace OliverBooth.Services; internal sealed class TutorialService : ITutorialService { private readonly IDbContextFactory _dbContextFactory; private readonly MarkdownPipeline _markdownPipeline; /// /// Initializes a new instance of the class. /// /// The . /// The . public TutorialService(IDbContextFactory dbContextFactory, MarkdownPipeline markdownPipeline) { _dbContextFactory = dbContextFactory; _markdownPipeline = markdownPipeline; } /// public IReadOnlyCollection GetArticles(ITutorialFolder folder) { if (folder is null) throw new ArgumentNullException(nameof(folder)); using WebContext context = _dbContextFactory.CreateDbContext(); return context.TutorialArticles.Where(a => a.Folder == folder.Id).ToArray(); } /// public IReadOnlyCollection GetFolders(ITutorialFolder? parent = null) { using WebContext context = _dbContextFactory.CreateDbContext(); if (parent is null) return context.TutorialFolders.Where(f => f.Parent == null).ToArray(); return context.TutorialFolders.Where(a => a.Parent == parent.Id).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 string RenderArticle(ITutorialArticle article) { return Markdig.Markdown.ToHtml(article.Body, _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; } }