diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/Filters/SlugifyFilter.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/Filters/SlugifyFilter.cs index 7453c9e1f4b..73ff824dfd4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/Filters/SlugifyFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/Filters/SlugifyFilter.cs @@ -14,9 +14,8 @@ public SlugifyFilter(ISlugService slugService) } public ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext ctx) { - var transliterateArg = arguments["transliterate"]; - var transliterate = transliterateArg.IsNil() || transliterateArg.ToBooleanValue(); + var slug = _slugService.Slugify(input.ToStringValue()); - return new StringValue(_slugService.Slugify(input.ToStringValue(), transliterate)); + return new StringValue(slug); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj b/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj index 3a013f28474..d527e334252 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/OrchardCore.Liquid.csproj @@ -24,6 +24,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs index 5aaa43ebdf5..7043b3991c2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Liquid/Startup.cs @@ -72,7 +72,6 @@ public override void ConfigureServices(IServiceCollection services) .AddLiquidFilter("local") .AddLiquidFilter("utc") .AddLiquidFilter("slugify") - .AddLiquidFilter("transliterate") .AddLiquidFilter("liquid") .AddLiquidFilter("href") .AddLiquidFilter("absolute_url") diff --git a/src/OrchardCore.Modules/OrchardCore.Liquid/Filters/TransliterateFilter.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Liquid/TransliterateFilter.cs similarity index 85% rename from src/OrchardCore.Modules/OrchardCore.Liquid/Filters/TransliterateFilter.cs rename to src/OrchardCore.Modules/OrchardCore.Localization/Liquid/TransliterateFilter.cs index 1e480bbd892..e87a4a13c5d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Liquid/Filters/TransliterateFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.Localization/Liquid/TransliterateFilter.cs @@ -1,8 +1,8 @@ -using AnyAscii; using Fluid; using Fluid.Values; +using OrchardCore.Liquid; -namespace OrchardCore.Liquid.Filters; +namespace OrchardCore.Localization.Liquid.Filters; public class TransliterateFilter : ILiquidFilter { diff --git a/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs index f9d2ea3639a..150142de423 100644 --- a/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Localization/Startup.cs @@ -5,7 +5,9 @@ using Microsoft.Extensions.Options; using OrchardCore.Admin.Models; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Liquid; using OrchardCore.Localization.Drivers; +using OrchardCore.Localization.Liquid.Filters; using OrchardCore.Localization.Models; using OrchardCore.Localization.Services; using OrchardCore.Modules; @@ -34,6 +36,8 @@ public override void ConfigureServices(IServiceCollection services) AddDataAnnotationsPortableObjectLocalization(); services.Replace(ServiceDescriptor.Singleton()); + + services.AddLiquidFilter("transliterate"); } /// diff --git a/src/OrchardCore.Modules/OrchardCore.Media/OrchardCore.Media.csproj b/src/OrchardCore.Modules/OrchardCore.Media/OrchardCore.Media.csproj index a06d986a8ed..a55f8840861 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/OrchardCore.Media.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Media/OrchardCore.Media.csproj @@ -39,6 +39,7 @@ + diff --git a/src/OrchardCore.Modules/OrchardCore.Media/Services/SlugifyMediaNameNormalizerService.cs b/src/OrchardCore.Modules/OrchardCore.Media/Services/SlugifyMediaNameNormalizerService.cs index 8a74ca14712..7a4bcbf7a41 100644 --- a/src/OrchardCore.Modules/OrchardCore.Media/Services/SlugifyMediaNameNormalizerService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Media/Services/SlugifyMediaNameNormalizerService.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Options; +using OrchardCore.Localization; using OrchardCore.Modules.Services; namespace OrchardCore.Media.Services; @@ -18,11 +19,15 @@ public SlugifyMediaNameNormalizerService( public string NormalizeFolderName(string folderName) { - return _slugService.Slugify(folderName, _options.Transliterate); + return _options.Transliterate + ? _slugService.SlugifyAndTransliterate(folderName) + : _slugService.Slugify(folderName); } public string NormalizeFileName(string fileName) { - return _slugService.Slugify(Path.GetFileNameWithoutExtension(fileName), _options.Transliterate) + Path.GetExtension(fileName); + return _options.Transliterate + ? _slugService.SlugifyAndTransliterate(Path.GetFileNameWithoutExtension(fileName)) + : _slugService.Slugify(Path.GetFileNameWithoutExtension(fileName)) + Path.GetExtension(fileName); } } diff --git a/src/OrchardCore/OrchardCore.Abstractions/Localization/Extensions/StringExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Localization/Extensions/StringExtensions.cs new file mode 100644 index 00000000000..140d505b7d1 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Localization/Extensions/StringExtensions.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Localization; + +/// +/// +/// +public static class StringExtensions +{ + /// + /// Converts the non-Latin characters to their ASCII equivalents. + /// + public static string Transliterate(this string text) => AnyAscii.Transliteration.Transliterate(text); +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/Localization/Extensions/TransliterationSlugServiceExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Localization/Extensions/TransliterationSlugServiceExtensions.cs new file mode 100644 index 00000000000..e18c01ba9f9 --- /dev/null +++ b/src/OrchardCore/OrchardCore.Abstractions/Localization/Extensions/TransliterationSlugServiceExtensions.cs @@ -0,0 +1,19 @@ +using OrchardCore.Modules.Services; + +namespace OrchardCore.Localization; + +public static class TransliterationSlugServiceExtensions +{ + /// + /// Converts the specified text to a URL-friendly slug after applying transliteration. + /// + /// The slug service used to generate the slug from the transliterated text. + /// The input text to transliterate and convert to a slug. Cannot be null or empty. + /// A slugified string representing the transliterated input text. + public static string SlugifyAndTransliterate(this ISlugService slugService, string text) + { + ArgumentException.ThrowIfNullOrEmpty(text); + + return slugService.Slugify(text.Transliterate()); + } +} diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Services/ISlugService.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Services/ISlugService.cs index be70f8064fa..21b9c8f0b56 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Modules/Services/ISlugService.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Services/ISlugService.cs @@ -9,15 +9,6 @@ public interface ISlugService /// The slug created from the input text. string Slugify(string text); - /// - /// Transforms specified text to the form suitable for URL slugs, - /// optionally transliterating non-Latin characters to their ASCII equivalents first. - /// - /// The text to transform. - /// Whether to transliterate non-Latin characters before slugifying. - /// The slug created from the input text. - string Slugify(string text, bool transliterate); - /// /// Transforms specified text to a custom form generally not suitable for URL slugs. /// Allows you to use a specified separator char. diff --git a/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj b/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj index 025ed9c0599..df33cb1778e 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj +++ b/src/OrchardCore/OrchardCore.Abstractions/OrchardCore.Abstractions.csproj @@ -16,6 +16,7 @@ + diff --git a/src/OrchardCore/OrchardCore/Modules/Services/SlugService.cs b/src/OrchardCore/OrchardCore/Modules/Services/SlugService.cs index d6c40aa2f9e..5f02de22b27 100644 --- a/src/OrchardCore/OrchardCore/Modules/Services/SlugService.cs +++ b/src/OrchardCore/OrchardCore/Modules/Services/SlugService.cs @@ -1,6 +1,5 @@ using System.Globalization; using System.Text; -using AnyAscii; using Cysharp.Text; namespace OrchardCore.Modules.Services; @@ -10,16 +9,6 @@ public class SlugService : ISlugService private const char Hyphen = '-'; private const int MaxLength = 1000; - public string Slugify(string text, bool transliterate) - { - if (transliterate && !string.IsNullOrEmpty(text)) - { - text = text.Transliterate(); - } - - return Slugify(text); - } - public string Slugify(string text, char separator) { throw new NotImplementedException(); diff --git a/src/OrchardCore/OrchardCore/OrchardCore.csproj b/src/OrchardCore/OrchardCore/OrchardCore.csproj index 03fd9517016..94c5acc04df 100644 --- a/src/OrchardCore/OrchardCore/OrchardCore.csproj +++ b/src/OrchardCore/OrchardCore/OrchardCore.csproj @@ -1,4 +1,4 @@ - + @@ -14,7 +14,6 @@ - diff --git a/test/OrchardCore.Tests/Apis/Context/SiteStartup.cs b/test/OrchardCore.Tests/Apis/Context/SiteStartup.cs index 78c0dc5f03d..0b4224e4472 100644 --- a/test/OrchardCore.Tests/Apis/Context/SiteStartup.cs +++ b/test/OrchardCore.Tests/Apis/Context/SiteStartup.cs @@ -23,6 +23,7 @@ public void ConfigureServices(IServiceCollection services) "OrchardCore.Tenants" ) .AddTenantFeatures( + "OrchardCore.Localization", "OrchardCore.Apis.GraphQL" ) .ConfigureServices(collection => diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Liquid/SlugifyFilterTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Liquid/SlugifyFilterTests.cs new file mode 100644 index 00000000000..711be569f4b --- /dev/null +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Liquid/SlugifyFilterTests.cs @@ -0,0 +1,54 @@ +using Fluid; +using OrchardCore.Liquid; +using OrchardCore.Tests.Apis.Context; + +namespace OrchardCore.Tests.Modules.OrchardCore.Liquid; + +public class SlugifyFilterTests +{ + [Theory] + [InlineData("Æneid Æneid", "æneid-æneid")] + [InlineData("Aeneid Aeneid", "aeneid-aeneid")] + public async Task SlugifyFilterShouldReturnSlugifiedString(string text, string expected) + { + // Arrange + var context = new SiteContext(); + + await context.InitializeAsync(); + + // Act & Assert + await context.UsingTenantScopeAsync(async scope => + { + var template = $$$"""{{ "{{{text}}}" | slugify }}"""; + + var liquidTemplateManager = scope.ServiceProvider.GetRequiredService(); + + var result = await liquidTemplateManager.RenderStringAsync(template, NullEncoder.Default, null); + + Assert.NotEmpty(result); + Assert.Equal(expected, result); + }); + } + + [Fact] + public async Task SlugifyThenTransliterateFilterShouldReturnSlugifiedTransliteratedString() + { + // Arrange + var context = new SiteContext(); + + await context.InitializeAsync(); + + // Act & Assert + await context.UsingTenantScopeAsync(async scope => + { + var template = """{{ "Æneid Æneid" | transliterate | slugify }}"""; + + var liquidTemplateManager = scope.ServiceProvider.GetRequiredService(); + + var result = await liquidTemplateManager.RenderStringAsync(template, NullEncoder.Default, null); + + Assert.NotEmpty(result); + Assert.Equal("aeneid-aeneid", result); + }); + } +} diff --git a/test/OrchardCore.Tests/Modules/OrchardCore.Liquid/TransliterateFilterTests.cs b/test/OrchardCore.Tests/Modules/OrchardCore.Liquid/TransliterateFilterTests.cs new file mode 100644 index 00000000000..2de87d92e21 --- /dev/null +++ b/test/OrchardCore.Tests/Modules/OrchardCore.Liquid/TransliterateFilterTests.cs @@ -0,0 +1,36 @@ +using Fluid; +using OrchardCore.Liquid; +using OrchardCore.Tests.Apis.Context; + +namespace OrchardCore.Tests.Modules.OrchardCore.Liquid; + +public class TransliterateFilterTests +{ + [Theory] + [InlineData("Æneid", "Aeneid")] + [InlineData("æneid", "aeneid")] + [InlineData("Aeneid", "Aeneid")] + [InlineData("aeneid", "aeneid")] + [InlineData("Ελληνικά", "Ellinika")] + [InlineData("джинсы_клеш", "dzhinsy_klesh")] + public async Task TransliterateFilterShouldReturnTransliteratedString(string text, string expected) + { + // Arrange + var context = new SiteContext(); + + await context.InitializeAsync(); + + // Act & Assert + await context.UsingTenantScopeAsync(async scope => + { + var template = $$$"""{{ "{{{text}}}" | transliterate }}"""; + + var liquidTemplateManager = scope.ServiceProvider.GetRequiredService(); + + var result = await liquidTemplateManager.RenderStringAsync(template, NullEncoder.Default, null); + + Assert.NotEmpty(result); + Assert.Equal(expected, result); + }); + } +} diff --git a/test/OrchardCore.Tests/Tokens.Content/SlugServiceTests.cs b/test/OrchardCore.Tests/Tokens.Content/SlugServiceTests.cs index c293f3e2069..7218569b0d9 100644 --- a/test/OrchardCore.Tests/Tokens.Content/SlugServiceTests.cs +++ b/test/OrchardCore.Tests/Tokens.Content/SlugServiceTests.cs @@ -1,3 +1,4 @@ +using OrchardCore.Localization; using OrchardCore.Modules.Services; namespace OrchardCore.Tests.Tokens.Content; @@ -81,14 +82,7 @@ public void ShouldPreserveNonLatinCharacters(string input, string expected) [Fact] public void ShouldTransliterateWhenRequested() { - var slug = _slugService.Slugify("Æneid", transliterate: true); + var slug = _slugService.SlugifyAndTransliterate("Æneid"); Assert.Equal("aeneid", slug); } - - [Fact] - public void ShouldNotTransliterateWhenDisabled() - { - var slug = _slugService.Slugify("Æneid", transliterate: false); - Assert.Equal("æneid", slug); - } }