< Summary

Information
Class: Chronicis.Api.Services.ExternalLinks.BlobFilenameParser
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ExternalLinks/BlobFilenameParser.cs
Line coverage
100%
Covered lines: 33
Uncovered lines: 0
Coverable lines: 33
Total lines: 130
Line coverage: 100%
Branch coverage
100%
Covered branches: 22
Total branches: 22
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
DeriveSlug(...)100%1010100%
NormalizeSlug(...)100%22100%
PrettifySlug(...)100%1010100%

File(s)

/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    {
 1235        if (string.IsNullOrWhiteSpace(filename))
 36        {
 337            return string.Empty;
 38        }
 39
 40        // Remove .json extension if present
 941        var baseName = filename.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
 942            ? filename[..^5]
 943            : filename;
 44
 45        // If contains underscore, take substring after first underscore
 946        var underscoreIndex = baseName.IndexOf('_');
 947        var slug = underscoreIndex >= 0
 948            ? baseName[(underscoreIndex + 1)..]
 949            : baseName;
 50
 51        // Normalize slug: preserve word boundaries by replacing non-alphanumeric runs with hyphens
 952        slug = NormalizeSlug(slug);
 53
 54        // Guard: If normalization produced empty slug, try fallback with full base name
 955        if (string.IsNullOrWhiteSpace(slug))
 56        {
 257            slug = NormalizeSlug(baseName);
 58
 59            // If still empty after fallback, return empty (caller MUST skip and log warning)
 260            if (string.IsNullOrWhiteSpace(slug))
 61            {
 162                return string.Empty;
 63            }
 64        }
 65
 866        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    {
 1175        if (string.IsNullOrWhiteSpace(input))
 76        {
 377            return string.Empty;
 78        }
 79
 80        // Convert to lowercase
 881        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"
 885        normalized = NonSlugCharsPattern().Replace(normalized, "-");
 86
 87        // Trim leading and trailing hyphens
 888        normalized = normalized.Trim('-');
 89
 890        return normalized;
 91    }
 92
 93    /// <summary>
 94    /// Prettifies a slug for display as a title.
 95    /// Culture-invariant and deterministic.
 96    /// </summary>
 97    /// <param name="slug">Slug to prettify (e.g., "animated-armor").</param>
 98    /// <returns>Human-readable title (e.g., "Animated Armor").</returns>
 99    public static string PrettifySlug(string slug)
 100    {
 14101        if (string.IsNullOrWhiteSpace(slug))
 102        {
 3103            return string.Empty;
 104        }
 105
 106        // Replace hyphens with spaces and title case each word (culture-invariant)
 11107        var words = slug.Split('-', StringSplitOptions.RemoveEmptyEntries);
 11108        var sb = new StringBuilder();
 109
 56110        foreach (var word in words)
 111        {
 17112            if (sb.Length > 0)
 113            {
 6114                sb.Append(' ');
 115            }
 116
 117            // Title case: first letter uppercase (invariant), rest lowercase
 17118            if (word.Length > 0)
 119            {
 17120                sb.Append(char.ToUpperInvariant(word[0]));
 17121                if (word.Length > 1)
 122                {
 14123                    sb.Append(word[1..].ToLowerInvariant());
 124                }
 125            }
 126        }
 127
 11128        return sb.ToString();
 129    }
 130}