diff --git a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs
index 5140451e..0ce75535 100644
--- a/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs
+++ b/Lombiq.HelpfulLibraries.AspNetCore/Localization/LocalizedHtmlStringExtensions.cs
@@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Html;
using Newtonsoft.Json;
using System.IO;
+using System.Linq;
+using System.Text;
using System.Text.Encodings.Web;
namespace Microsoft.AspNetCore.Mvc.Localization;
@@ -29,4 +31,29 @@ public static string Html(this IHtmlContent htmlContent)
htmlContent.WriteTo(stringWriter, HtmlEncoder.Default);
return stringWriter.ToString();
}
+
+ ///
+ /// Concatenates the localized HTML string with the provided HTML
+ /// content. This is suitable for joining individually localizable HTML strings.
+ ///
+ public static LocalizedHtmlString Concat(this LocalizedHtmlString first, params IHtmlContent[] other)
+ {
+ if (other.Length == 0) return first;
+
+ var builder = new StringBuilder(first.Html());
+ foreach (var content in other) builder.Append(content.Html());
+ var html = builder.ToString();
+
+ return new LocalizedHtmlString(html, html);
+ }
+
+ public static LocalizedHtmlString Join(this IHtmlContent separator, params LocalizedHtmlString[] items)
+ {
+ if (items.Length == 0) return null;
+
+ var first = items[0];
+ var other = items.Skip(1).SelectMany(item => new[] { separator, item }).ToArray();
+
+ return first.Concat(other);
+ }
}
diff --git a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs
index 1038e497..a3f081e4 100644
--- a/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs
+++ b/Lombiq.HelpfulLibraries.Common/Extensions/DictionaryExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
@@ -165,4 +166,101 @@ public static void AddToList(
///
public static IReadOnlyDictionary ToReadOnly(this IDictionary dictionary) =>
dictionary as IReadOnlyDictionary ?? new Dictionary(dictionary);
+
+ ///
+ /// Creates a new dictionary based on by taking each entry's value, and if it's not
+ /// and has at least one not item then that item is added under the
+ /// same key.
+ ///
+ [SuppressMessage(
+ "Design",
+ "MA0016:Prefer returning collection abstraction instead of implementation",
+ Justification = "Better compatibility.")]
+ public static Dictionary WithFirstValues(
+ this IEnumerable> dictionary,
+ Func select = null)
+ where TValues : IEnumerable
+ {
+ var result = new Dictionary();
+
+ foreach (var (key, values) in dictionary)
+ {
+ if (select == null)
+ {
+ if (values is not null && values.FirstOrDefault() is { } value)
+ {
+ result[key] = value;
+ }
+ }
+ else
+ {
+ if (select(values) is { } value)
+ {
+ result[key] = value;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Creates a new dictionary based on where each value is a new instance of
+ /// which is an of . If the value
+ /// is not then it's added to the list.
+ ///
+ [SuppressMessage(
+ "Design",
+ "MA0016:Prefer returning collection abstraction instead of implementation",
+ Justification = "Better compatibility.")]
+ public static Dictionary ToListValuedDictionary(
+ this IEnumerable> dictionary,
+ Func select = null)
+ where TValues : IList, new()
+ {
+ var result = new Dictionary();
+
+ foreach (var (key, value) in dictionary)
+ {
+ if (select != null)
+ {
+ result[key] = select(value);
+ continue;
+ }
+
+ var list = new TValues();
+ if (value is not null) list.Add(value);
+ result[key] = list;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner.
+ ///
+ public static IDictionary ToDictionaryIgnoreCase(
+ this IEnumerable> source) =>
+ new Dictionary(source, StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner.
+ ///
+ public static IDictionary ToDictionaryIgnoreCase(
+ this IEnumerable source,
+ Func keySelector) =>
+ new Dictionary(
+ source.Select(item => new KeyValuePair(keySelector(item), item)),
+ StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Creates a new string keyed dictionary where the key is compared in a case-insensitive manner.
+ ///
+ public static IDictionary ToDictionaryIgnoreCase(
+ this IEnumerable source,
+ Func keySelector,
+ Func valueSelector) =>
+ new Dictionary(
+ source.Select(item => new KeyValuePair(keySelector(item), valueSelector(item))),
+ StringComparer.OrdinalIgnoreCase);
}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs
index c9c08ce6..10f3bea2 100644
--- a/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Contents/CommonContentDisplayTypes.cs
@@ -11,4 +11,5 @@ public static class CommonContentDisplayTypes
public const string Detail = nameof(Detail);
public const string Summary = nameof(Summary);
public const string SummaryAdmin = nameof(SummaryAdmin);
+ public const string Thumbnail = nameof(Thumbnail);
}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md
new file mode 100644
index 00000000..494a017b
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Docs/Validation.md
@@ -0,0 +1,5 @@
+# Lombiq Helpful Libraries - Orchard Core Libraries - Validation for Orchard Core
+
+## `IEmailAndPasswordValidator`
+
+A service for returning validation errors for email of password. Use its `ValidateEmailAsync` method to get email format validation errors, and its `ValidatePasswordAsync` to get validation errors from the registered `IPasswordValidator` instances.
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs
new file mode 100644
index 00000000..d9d308eb
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/ILiquidContentDisplayService.cs
@@ -0,0 +1,33 @@
+using Fluid.Values;
+using OrchardCore.DisplayManagement;
+using OrchardCore.DisplayManagement.Implementation;
+using System;
+using System.Threading.Tasks;
+
+namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid;
+
+///
+/// Service that displays shapes as HTML in a .
+///
+public interface ILiquidContentDisplayService
+{
+ ///
+ /// Creates new instances of a dynamic shape object and renders it to HTML in a .
+ ///
+ ValueTask DisplayShapeAsync(IShape shape);
+
+ ///
+ /// Creates new instances of a typed shape object and renders it to HTML in a .
+ ///
+ /// The type to instantiate.
+ ValueTask DisplayNewAsync(string shapeType, Action initialize);
+
+ ///
+ /// Displays an already instantiated as a .
+ ///
+ ValueTask DisplayNewAsync(
+ string shapeType,
+ Func> shapeFactory,
+ Action creating = null,
+ Action created = null);
+}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs
new file mode 100644
index 00000000..5cefd7ac
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Liquid/LiquidContentDisplayService.cs
@@ -0,0 +1,41 @@
+using Fluid.Values;
+using OrchardCore.DisplayManagement;
+using OrchardCore.DisplayManagement.Implementation;
+using OrchardCore.DisplayManagement.Liquid;
+using System;
+using System.Threading.Tasks;
+
+namespace Lombiq.HelpfulLibraries.OrchardCore.Liquid;
+
+public class LiquidContentDisplayService : ILiquidContentDisplayService
+{
+ private readonly IDisplayHelper _displayHelper;
+ private readonly IShapeFactory _shapeFactory;
+ public LiquidContentDisplayService(IDisplayHelper displayHelper, IShapeFactory shapeFactory)
+ {
+ _displayHelper = displayHelper;
+ _shapeFactory = shapeFactory;
+ }
+
+ public async ValueTask DisplayNewAsync(
+ string shapeType,
+ Func> shapeFactory,
+ Action creating = null,
+ Action created = null)
+ {
+ var shape = await _shapeFactory.CreateAsync(shapeType, shapeFactory, creating, created);
+ return await DisplayShapeAsync(shape);
+ }
+
+ public async ValueTask DisplayNewAsync(string shapeType, Action initialize)
+ {
+ var shape = await _shapeFactory.CreateAsync(shapeType, initialize);
+ return await DisplayShapeAsync(shape);
+ }
+
+ public async ValueTask DisplayShapeAsync(IShape shape)
+ {
+ var content = await _displayHelper.ShapeExecuteAsync(shape);
+ return new HtmlContentValue(content);
+ }
+}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs
index 1d83f587..5b71a0a2 100644
--- a/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Navigation/NavigationItemBuilderExtensions.cs
@@ -60,4 +60,10 @@ public static NavigationBuilder AddSeparator(this NavigationBuilder builder, ISt
///
public static NavigationBuilder AddLabel(this NavigationBuilder builder, LocalizedString label) =>
builder.Add(label, subMenu => subMenu.Url("#").AddClass("disabled menuWidget__link_label"));
+
+ ///
+ /// Adds a link to the menu item pointing to the site settings page identified by the .
+ ///
+ public static NavigationItemBuilder SiteSettings(this NavigationItemBuilder builder, string groupId) =>
+ builder.Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId });
}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs b/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs
new file mode 100644
index 00000000..cc5d9663
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Validation/EmailAndPasswordValidator.cs
@@ -0,0 +1,46 @@
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Localization;
+using OrchardCore.Email;
+using OrchardCore.Users;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Lombiq.HelpfulLibraries.OrchardCore.Validation;
+
+public class EmailAndPasswordValidator : IEmailAndPasswordValidator
+{
+ private readonly IEmailAddressValidator _emailAddressValidator;
+ private readonly UserManager _userManager;
+ private readonly IStringLocalizer T;
+ public EmailAndPasswordValidator(
+ IEmailAddressValidator emailAddressValidator,
+ UserManager userManager,
+ IStringLocalizer stringLocalizer)
+ {
+ _emailAddressValidator = emailAddressValidator;
+ _userManager = userManager;
+ T = stringLocalizer;
+ }
+
+ public Task> ValidateEmailAsync(string email) =>
+ Task.FromResult(_emailAddressValidator.Validate(email)
+ ? Enumerable.Empty()
+ : new[] { T["Invalid email address."] });
+
+ public async Task> ValidatePasswordAsync(string password)
+ {
+ var errors = new List();
+ if (password == null) return errors;
+
+ foreach (var passwordValidator in _userManager.PasswordValidators)
+ {
+ var result = await passwordValidator.ValidateAsync(_userManager, user: null, password);
+
+ if (result.Succeeded) continue;
+ errors.AddRange(result.Errors.Select(error => T[error.Description]));
+ }
+
+ return errors;
+ }
+}
diff --git a/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs b/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs
new file mode 100644
index 00000000..bc6bd600
--- /dev/null
+++ b/Lombiq.HelpfulLibraries.OrchardCore/Validation/IEmailAndPasswordValidator.cs
@@ -0,0 +1,21 @@
+using Microsoft.Extensions.Localization;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Lombiq.HelpfulLibraries.OrchardCore.Validation;
+
+///
+/// A service for returning validation errors for email of password.
+///
+public interface IEmailAndPasswordValidator
+{
+ ///
+ /// Validates the provided and returns any validation errors as a localized string.
+ ///
+ Task> ValidateEmailAsync(string email);
+
+ ///
+ /// Validates the provided and returns any validation errors as a localized string.
+ ///
+ Task> ValidatePasswordAsync(string password);
+}