7

The goal

Create something that converts string to friendly string in Razor (I'm using C#/MVC4)

The problem

I have the following code:

<li>
   <img 
    src="~/Images/Products/@Model["CategoryName"]/@Model["ProductThumbnailUrl"]"
   />
</li>

The name of my categories are like: CDs, DVDs and Blurayand I want to concatenate as "friendly-string" like cds-dvds-and-blurays using something like this:

<li>
   <img 
    src="~/Images/Products
          /@HtmlHelper.ConvertToFriendlyString(@Model["CategoryName"])
          /@Model["ProductThumbnailUrl"]"
   />
</li>

Can you all understand?

So, someone have any idea?

Thanks in advance.

Guilherme Oderdenge
  • 4,935
  • 6
  • 61
  • 96
  • Take a look at [slug generation](http://stackoverflow.com/questions/2920744/url-slugify-alrogithm-in-c) and then you can create helper for it. – Zbigniew Jun 13 '13 at 16:25
  • See this answer on how Stack Overflow does it: http://stackoverflow.com/questions/25259/how-does-stackoverflow-generate-its-seo-friendly-urls/25486#25486 – Ryan Endacott Jun 13 '13 at 16:25

1 Answers1

7

I've done this before. While I dig up the code, here are things to keep in consideration:

  1. Make sure you store your generated URLs so you can do collision detection; converting strings to friendly URLs is almost certainly going to be lossy, so you need logic to resolve conflicted names.
  2. You ought to try to convert diacritical marks into more easily-typable characters.
  3. Consider making url-to-resource mappings a 1:many relationship; if the name of your resource changes, you may want to generate a new URL and have the old URL redirect to the new.

UPDATE: Here is my code for this. The Stack Overflow approach is OK, but I like mine better; instead of using a set of character substitutions, it uses the great .NET Unicode library to create friendlier characters:

public static string ConvertToFriendlyUrl(string text)
{
    var decomposed = text.Normalize(NormalizationForm.FormKD);
    var builder = new StringBuilder();
    foreach (var ch in decomposed)
    {
        var charInfo = CharUnicodeInfo.GetUnicodeCategory(ch);
        switch (charInfo)
        {
            // Keep these as they are
            case UnicodeCategory.DecimalDigitNumber:
            case UnicodeCategory.LetterNumber:
            case UnicodeCategory.LowercaseLetter:
            case UnicodeCategory.CurrencySymbol:
            case UnicodeCategory.OtherLetter:
            case UnicodeCategory.OtherNumber:
                builder.Append(ch);
                break;

            // Convert these to dashes
            case UnicodeCategory.DashPunctuation:
            case UnicodeCategory.MathSymbol:
            case UnicodeCategory.ModifierSymbol:
            case UnicodeCategory.OtherPunctuation:
            case UnicodeCategory.OtherSymbol:
            case UnicodeCategory.SpaceSeparator:
                builder.Append('-');
                break;

            // Convert to lower-case
            case UnicodeCategory.TitlecaseLetter:
            case UnicodeCategory.UppercaseLetter:
                builder.Append(char.ToLowerInvariant(ch));
                break;

            // Ignore certain types of characters
            case UnicodeCategory.OpenPunctuation:
            case UnicodeCategory.ClosePunctuation:
            case UnicodeCategory.ConnectorPunctuation:
            case UnicodeCategory.Control:
            case UnicodeCategory.EnclosingMark:
            case UnicodeCategory.FinalQuotePunctuation:
            case UnicodeCategory.Format:
            case UnicodeCategory.InitialQuotePunctuation:
            case UnicodeCategory.LineSeparator:
            case UnicodeCategory.ModifierLetter:
            case UnicodeCategory.NonSpacingMark:
            case UnicodeCategory.OtherNotAssigned:
            case UnicodeCategory.ParagraphSeparator:
            case UnicodeCategory.PrivateUse:
            case UnicodeCategory.SpacingCombiningMark:
            case UnicodeCategory.Surrogate:
                break;
        }
    }

    var built = builder.ToString();
    while (built.Contains("--")) 
        built = built.Replace("--", "-");
    while (built.EndsWith("-"))
    {
        built = built.Substring(0, built.Length - 1);
    }
    while (built.StartsWith("-"))
    {
        built = built.Substring(1, built.Length - 1);
    }
    return built;
}

public static string GetIncrementedUrl(string url)
{
    var parts = url.Split('-');
    var lastPortion = parts.LastOrDefault();
    int numToInc;
    bool incExisting;
    if (lastPortion == null)
    {
        numToInc = 1;
        incExisting = false;
    }
    else
    {
        if (int.TryParse(lastPortion, out numToInc))
        {
            incExisting = true;
        }
        else
        {
            incExisting = false;
            numToInc = 1;
        }
    }

    var fragToKeep = incExisting 
        ? string.Join("-", parts.Take(parts.Length - 1).ToArray()) 
        : url;
    return fragToKeep + "-" + (numToInc + 1).ToString();
}

public static string SeekUrl(
    string name, Func<string, bool> uniquenessCheck)
{
    var urlName = UrlUtils.ConvertToFriendlyUrl(name);
    while (!uniquenessCheck(urlName))
    {
        urlName = UrlUtils.GetIncrementedUrl(urlName);
    }

    return urlName;
}
Jacob
  • 77,566
  • 24
  • 149
  • 228
  • About the considerations: you are right! About your code: have some logic inside it to avoid name collision? I don't know if my understanding is correct. – Guilherme Oderdenge Jun 13 '13 at 16:41
  • I had used this to generate URLs for multiple things, so the name collision check was generalized; The `Func uniquenessCheck` parameter is what performs the name collision check. The caller passes its own implementation of this check depending on its needs. For example, `name => !yourDb.Things.Any(t => t.Name == name)`. – Jacob Jun 13 '13 at 16:44
  • Oh, ok! Got it! Wonderful explanation, thanks! But, how about the integration with Razor Engine? Can you clarify this for me? – Guilherme Oderdenge Jun 13 '13 at 16:45