< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 84
Coverable lines: 84
Total lines: 257
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 18
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: ExternalLinkRegex()100%210%
File 2: .ctor(...)100%210%
File 2: SyncExternalLinksAsync()0%2040%
File 2: GetExternalLinksForArticleAsync()100%210%
File 2: ExtractExternalLinksFromHtml(...)0%210140%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/obj/Release/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs

File '/home/runner/work/chronicis/chronicis/src/Chronicis.Api/obj/Release/net9.0/System.Text.RegularExpressions.Generator/System.Text.RegularExpressions.Generator.RegexGenerator/RegexGenerator.g.cs' does not exist (any more).

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/Articles/ArticleExternalLinkService.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using Chronicis.Api.Data;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.Models;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace Chronicis.Api.Services.Articles;
 8
 9/// <summary>
 10/// Service for managing external resource links embedded in article content.
 11/// </summary>
 12public partial class ArticleExternalLinkService : IArticleExternalLinkService
 13{
 14    private readonly ChronicisDbContext _context;
 15    private readonly ILogger<ArticleExternalLinkService> _logger;
 16
 017    public ArticleExternalLinkService(
 018        ChronicisDbContext context,
 019        ILogger<ArticleExternalLinkService> logger)
 20    {
 021        _context = context;
 022        _logger = logger;
 023    }
 24
 25    /// <summary>
 26    /// Regex pattern to extract external links from article HTML.
 27    /// Matches: &lt;span data-type="external-link" ... data-source="..." data-id="..." data-title="..."&gt;
 28    /// </summary>
 29    [GeneratedRegex(
 30        @"<span[^>]*data-type=""external-link""[^>]*data-source=""([^""]*)""[^>]*data-id=""([^""]*)""[^>]*data-title=""(
 31        RegexOptions.IgnoreCase | RegexOptions.Compiled)]
 32    private static partial Regex ExternalLinkRegex();
 33
 34    public async Task SyncExternalLinksAsync(Guid articleId, string? htmlContent)
 35    {
 36        try
 37        {
 38            // Extract external links from HTML
 039            var extractedLinks = ExtractExternalLinksFromHtml(htmlContent);
 40
 041            _logger.LogDebug(
 042                "Extracted {Count} external links from article {ArticleId}",
 043                extractedLinks.Count,
 044                articleId);
 45
 46            // Delete all existing external links for this article
 047            var existingLinks = await _context.ArticleExternalLinks
 048                .Where(ael => ael.ArticleId == articleId)
 049                .ToListAsync();
 50
 051            if (existingLinks.Any())
 52            {
 053                _context.ArticleExternalLinks.RemoveRange(existingLinks);
 054                _logger.LogDebug(
 055                    "Removed {Count} existing external links for article {ArticleId}",
 056                    existingLinks.Count,
 057                    articleId);
 58            }
 59
 60            // Insert new external links
 061            if (extractedLinks.Any())
 62            {
 063                var newLinks = extractedLinks.Select(link => new ArticleExternalLink
 064                {
 065                    Id = Guid.NewGuid(),
 066                    ArticleId = articleId,
 067                    Source = link.Source,
 068                    ExternalId = link.ExternalId,
 069                    DisplayTitle = link.DisplayTitle
 070                }).ToList();
 71
 072                await _context.ArticleExternalLinks.AddRangeAsync(newLinks);
 73
 074                _logger.LogDebug(
 075                    "Added {Count} new external links for article {ArticleId}",
 076                    newLinks.Count,
 077                    articleId);
 078            }
 79
 080            await _context.SaveChangesAsync();
 081        }
 082        catch (Exception ex)
 83        {
 084            _logger.LogError(
 085                ex,
 086                "Error syncing external links for article {ArticleId}",
 087                articleId);
 088            throw;
 89        }
 090    }
 91
 92    public async Task<List<ArticleExternalLinkDto>> GetExternalLinksForArticleAsync(Guid articleId)
 93    {
 94        try
 95        {
 096            var links = await _context.ArticleExternalLinks
 097                .Where(ael => ael.ArticleId == articleId)
 098                .OrderBy(ael => ael.Source)
 099                .ThenBy(ael => ael.DisplayTitle)
 0100                .Select(ael => new ArticleExternalLinkDto
 0101                {
 0102                    Id = ael.Id,
 0103                    ArticleId = ael.ArticleId,
 0104                    Source = ael.Source,
 0105                    ExternalId = ael.ExternalId,
 0106                    DisplayTitle = ael.DisplayTitle
 0107                })
 0108                .ToListAsync();
 109
 0110            _logger.LogDebug(
 0111                "Retrieved {Count} external links for article {ArticleId}",
 0112                links.Count,
 0113                articleId);
 114
 0115            return links;
 116        }
 0117        catch (Exception ex)
 118        {
 0119            _logger.LogError(
 0120                ex,
 0121                "Error retrieving external links for article {ArticleId}",
 0122                articleId);
 0123            throw;
 124        }
 0125    }
 126
 127    /// <summary>
 128    /// Extracts external link information from HTML content.
 129    /// </summary>
 130    private List<(string Source, string ExternalId, string DisplayTitle)> ExtractExternalLinksFromHtml(string? htmlConte
 131    {
 0132        if (string.IsNullOrWhiteSpace(htmlContent))
 133        {
 0134            return new List<(string, string, string)>();
 135        }
 136
 0137        var links = new List<(string Source, string ExternalId, string DisplayTitle)>();
 0138        var matches = ExternalLinkRegex().Matches(htmlContent);
 139
 0140        foreach (Match match in matches)
 141        {
 0142            if (match.Success && match.Groups.Count >= 4)
 143            {
 0144                var source = match.Groups[1].Value;
 0145                var externalId = match.Groups[2].Value;
 0146                var displayTitle = match.Groups[3].Value;
 147
 148                // Validate that we have all required fields
 0149                if (!string.IsNullOrWhiteSpace(source) &&
 0150                    !string.IsNullOrWhiteSpace(externalId) &&
 0151                    !string.IsNullOrWhiteSpace(displayTitle))
 152                {
 0153                    links.Add((source, externalId, displayTitle));
 154                }
 155            }
 156        }
 157
 0158        return links;
 159    }
 160}