< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 36
Coverable lines: 36
Total lines: 253
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 24
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: NonSlugCharsPattern()100%210%
File 2: DeriveSlug(...)0%110100%
File 2: NormalizeSlug(...)0%2040%
File 2: PrettifySlug(...)0%110100%

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/ExternalLinks/BlobFilenameParser.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.RegularExpressions;
 3
 4namespace Chronicis.Api.Services.ExternalLinks;
 5
 6/// <summary>
 7/// Parses blob filenames to derive slugs and titles.
 8/// Handles SRD filename conventions (e.g., "srd-2024_animated-armor.json").
 9/// </summary>
 10public static partial class BlobFilenameParser
 11{
 12    // Matches any sequence of non-alphanumeric characters (to replace with single hyphen)
 13    [GeneratedRegex(@"[^a-z0-9]+", RegexOptions.Compiled)]
 14    private static partial Regex NonSlugCharsPattern();
 15
 16    /// <summary>
 17    /// Derives a slug from a blob filename.
 18    /// </summary>
 19    /// <param name="filename">Full filename including extension (e.g., "srd-2024_animated-armor.json").</param>
 20    /// <returns>Normalized slug (e.g., "animated-armor"), or empty string if normalization fails.</returns>
 21    /// <remarks>
 22    /// Rules:
 23    /// 1. Remove .json extension
 24    /// 2. If contains underscore, take substring after first underscore
 25    /// 3. Otherwise, use entire base filename
 26    /// 4. Normalize:
 27    ///    - Convert to lowercase
 28    ///    - Replace runs of non-alphanumeric chars with single hyphen (preserves word boundaries)
 29    ///    - Trim leading/trailing hyphens
 30    ///    - Collapse multiple consecutive hyphens to single hyphen
 31    /// 5. If result is empty, return empty string (caller should skip and log warning)
 32    /// </remarks>
 33    public static string DeriveSlug(string filename)
 34    {
 035        if (string.IsNullOrWhiteSpace(filename))
 36        {
 037            return string.Empty;
 38        }
 39
 40        // Remove .json extension if present
 041        var baseName = filename.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
 042            ? filename[..^5]
 043            : filename;
 44
 45        // If contains underscore, take substring after first underscore
 046        var underscoreIndex = baseName.IndexOf('_');
 047        var slug = underscoreIndex >= 0
 048            ? baseName[(underscoreIndex + 1)..]
 049            : baseName;
 50
 51        // Normalize slug: preserve word boundaries by replacing non-alphanumeric runs with hyphens
 052        slug = NormalizeSlug(slug);
 53
 54        // Guard: If normalization produced empty slug, try fallback with full base name
 055        if (string.IsNullOrWhiteSpace(slug))
 56        {
 057            slug = NormalizeSlug(baseName);
 58
 59            // If still empty after fallback, return empty (caller MUST skip and log warning)
 060            if (string.IsNullOrWhiteSpace(slug))
 61            {
 062                return string.Empty;
 63            }
 64        }
 65
 066        return slug;
 67    }
 68
 69    /// <summary>
 70    /// Normalizes a string to a valid slug format.
 71    /// Preserves word boundaries by replacing non-alphanumeric runs with single hyphens.
 72    /// </summary>
 73    private static string NormalizeSlug(string input)
 74    {
 075        if (string.IsNullOrWhiteSpace(input))
 76        {
 077            return string.Empty;
 78        }
 79
 80        // Convert to lowercase
 081        var normalized = input.ToLowerInvariant();
 82
 83        // Replace any run of non-alphanumeric characters with a single hyphen
 84        // This preserves word boundaries: "hello world!" -> "hello-world"
 085        normalized = NonSlugCharsPattern().Replace(normalized, "-");
 86
 87        // Trim leading and trailing hyphens
 088        normalized = normalized.Trim('-');
 89
 90        // Collapse multiple consecutive hyphens to single hyphen
 91        // (shouldn't happen with regex above, but defensive)
 092        while (normalized.Contains("--", StringComparison.Ordinal))
 93        {
 094            normalized = normalized.Replace("--", "-");
 95        }
 96
 097        return normalized;
 98    }
 99
 100    /// <summary>
 101    /// Prettifies a slug for display as a title.
 102    /// Culture-invariant and deterministic.
 103    /// </summary>
 104    /// <param name="slug">Slug to prettify (e.g., "animated-armor").</param>
 105    /// <returns>Human-readable title (e.g., "Animated Armor").</returns>
 106    public static string PrettifySlug(string slug)
 107    {
 0108        if (string.IsNullOrWhiteSpace(slug))
 109        {
 0110            return string.Empty;
 111        }
 112
 113        // Replace hyphens with spaces and title case each word (culture-invariant)
 0114        var words = slug.Split('-', StringSplitOptions.RemoveEmptyEntries);
 0115        var sb = new StringBuilder();
 116
 0117        foreach (var word in words)
 118        {
 0119            if (sb.Length > 0)
 120            {
 0121                sb.Append(' ');
 122            }
 123
 124            // Title case: first letter uppercase (invariant), rest lowercase
 0125            if (word.Length > 0)
 126            {
 0127                sb.Append(char.ToUpperInvariant(word[0]));
 0128                if (word.Length > 1)
 129                {
 0130                    sb.Append(word[1..].ToLowerInvariant());
 131                }
 132            }
 133        }
 134
 0135        return sb.ToString();
 136    }
 137}