using AngleSharp; using AngleSharp.Dom; namespace Nuuru.Server.Services; public enum HtmlEnrichmentTarget { ForumPost = 1 } public enum HtmlEnrichmentPhase { Immediate = 1, Deferred = 2 } public sealed record HtmlEnrichmentContext(HtmlEnrichmentTarget Target, int? ForumPostId = null) { public static HtmlEnrichmentContext ForForumPost(int? forumPostId = null) => new(HtmlEnrichmentTarget.ForumPost, forumPostId); } public sealed record HtmlEnrichmentResult(string Html, int Version); public interface IHtmlEnrichmentService { int ImmediateVersion { get; } int CurrentVersion { get; } bool NeedsEnrichment(string html, int contentHtmlVersion, HtmlEnrichmentContext context); Task EnrichImmediateAsync(string html, HtmlEnrichmentContext context, CancellationToken cancellationToken = default); Task EnrichAsync(string html, HtmlEnrichmentContext context, CancellationToken cancellationToken = default); } public interface IHtmlEnricher { HtmlEnrichmentPhase Phase { get; } Task EnrichAsync(IDocument document, HtmlEnrichmentContext context, CancellationToken cancellationToken = default); } public sealed class HtmlEnrichmentService : IHtmlEnrichmentService { private static readonly IBrowsingContext BrowsingContext = AngleSharp.BrowsingContext.New(Configuration.Default); public const int ForumPostImmediateContentHtmlVersion = 1; public const int ForumPostContentHtmlVersion = 3; private readonly IReadOnlyList _enrichers; public HtmlEnrichmentService(IEnumerable enrichers) { _enrichers = enrichers.ToList(); } public int ImmediateVersion => ForumPostImmediateContentHtmlVersion; public int CurrentVersion => ForumPostContentHtmlVersion; public bool NeedsEnrichment(string html, int contentHtmlVersion, HtmlEnrichmentContext context) { return !string.IsNullOrWhiteSpace(html) && contentHtmlVersion < GetTargetVersion(context, HtmlEnrichmentPhase.Deferred); } public Task EnrichImmediateAsync(string html, HtmlEnrichmentContext context, CancellationToken cancellationToken = default) { return EnrichInternalAsync(html, context, HtmlEnrichmentPhase.Immediate, cancellationToken); } public Task EnrichAsync(string html, HtmlEnrichmentContext context, CancellationToken cancellationToken = default) { return EnrichInternalAsync(html, context, HtmlEnrichmentPhase.Deferred, cancellationToken); } private async Task EnrichInternalAsync( string html, HtmlEnrichmentContext context, HtmlEnrichmentPhase maxPhase, CancellationToken cancellationToken) { var targetVersion = GetTargetVersion(context, maxPhase); if (string.IsNullOrWhiteSpace(html)) return new HtmlEnrichmentResult(html, targetVersion); var document = await BrowsingContext.OpenAsync(req => req.Content(html), cancellationToken); foreach (var enricher in _enrichers) { if (enricher.Phase > maxPhase) continue; await enricher.EnrichAsync(document, context, cancellationToken); } return new HtmlEnrichmentResult(document.Body?.InnerHtml ?? html, targetVersion); } private static int GetTargetVersion(HtmlEnrichmentContext context, HtmlEnrichmentPhase maxPhase) { return context.Target switch { HtmlEnrichmentTarget.ForumPost => maxPhase == HtmlEnrichmentPhase.Immediate ? ForumPostImmediateContentHtmlVersion : ForumPostContentHtmlVersion, _ => ForumPostContentHtmlVersion }; } }