diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs index 79e1a067d..b5ddeaa57 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringJintExtension.cs @@ -6,12 +6,8 @@ // ========================================================================== using System; -using System.IO; -using System.Text; -using HtmlAgilityPack; using Jint; using Jint.Native; -using Markdig; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.Scripting.Extensions @@ -60,13 +56,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions { try { - var document = LoadHtml(text); - - var sb = new StringBuilder(); - - WriteTextTo(document.DocumentNode, sb); - - return sb.ToString().Trim(' ', '\n', '\r'); + return TextHelpers.Html2Text(text); } catch { @@ -74,76 +64,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions } }; - private static HtmlDocument LoadHtml(string text) - { - var document = new HtmlDocument(); - - document.LoadHtml(text); - - return document; - } - - private static void WriteTextTo(HtmlNode node, StringBuilder sb) - { - switch (node.NodeType) - { - case HtmlNodeType.Comment: - break; - case HtmlNodeType.Document: - WriteChildrenTextTo(node, sb); - break; - case HtmlNodeType.Text: - var html = ((HtmlTextNode)node).Text; - - if (HtmlNode.IsOverlappedClosingElement(html)) - { - break; - } - - if (!string.IsNullOrWhiteSpace(html)) - { - sb.Append(HtmlEntity.DeEntitize(html)); - } - - break; - - case HtmlNodeType.Element: - switch (node.Name) - { - case "p": - sb.AppendLine(); - break; - case "br": - sb.AppendLine(); - break; - case "style": - return; - case "script": - return; - } - - if (node.HasChildNodes) - { - WriteChildrenTextTo(node, sb); - } - - break; - } - } - - private static void WriteChildrenTextTo(HtmlNode node, StringBuilder sb) - { - foreach (var child in node.ChildNodes) - { - WriteTextTo(child, sb); - } - } - private readonly Func markdown2Text = text => { try { - return Markdown.ToPlainText(text).Trim(' ', '\n', '\r'); + return TextHelpers.Markdown2Text(text); } catch { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs index 2de45b6eb..c355d88c2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/Extensions/StringWordsJintExtension.cs @@ -17,27 +17,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions { try { - var numWords = 0; - - for (int i = 1; i < text.Length; i++) - { - if (char.IsWhiteSpace(text[i - 1])) - { - var character = text[i]; - - if (char.IsLetterOrDigit(character) || char.IsPunctuation(character)) - { - numWords++; - } - } - } - - if (text.Length > 2) - { - numWords++; - } - - return numWords; + return TextHelpers.WordCount(text); } catch { @@ -49,17 +29,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.Extensions { try { - var characterCount = 0; - - for (int i = 0; i < text.Length; i++) - { - if (char.IsLetter(text[i])) - { - characterCount++; - } - } - - return characterCount; + return TextHelpers.CharacterCount(text); } catch { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs index ffed40632..585f8fe34 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringFluidExtension.cs @@ -14,14 +14,7 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions { public sealed class StringFluidExtension : IFluidExtension { - public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) - { - TemplateContext.GlobalFilters.AddFilter("escape", Escape); - TemplateContext.GlobalFilters.AddFilter("slugify", Slugify); - TemplateContext.GlobalFilters.AddFilter("trim", Trim); - } - - public static FluidValue Slugify(FluidValue input, FilterArguments arguments, TemplateContext context) + private static readonly FilterDelegate Slugify = (input, arguments, context) => { if (input is StringValue value) { @@ -31,9 +24,9 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions } return input; - } + }; - public static FluidValue Escape(FluidValue input, FilterArguments arguments, TemplateContext context) + private static readonly FilterDelegate Escape = (input, arguments, context) => { var result = input.ToStringValue(); @@ -41,11 +34,30 @@ namespace Squidex.Domain.Apps.Core.Templates.Extensions result = result[1..^1]; return FluidValue.Create(result); - } + }; - public static FluidValue Trim(FluidValue input, FilterArguments arguments, TemplateContext context) + private static readonly FilterDelegate Markdown2Text = (input, arguments, context) => + { + return FluidValue.Create(TextHelpers.Markdown2Text(input.ToStringValue())); + }; + + private static readonly FilterDelegate Html2Text = (input, arguments, context) => + { + return FluidValue.Create(TextHelpers.Html2Text(input.ToStringValue())); + }; + + private static readonly FilterDelegate Trim = (input, arguments, context) => { return FluidValue.Create(input.ToStringValue().Trim()); + }; + + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + TemplateContext.GlobalFilters.AddFilter("html2text", Html2Text); + TemplateContext.GlobalFilters.AddFilter("markdown2text", Markdown2Text); + TemplateContext.GlobalFilters.AddFilter("escape", Escape); + TemplateContext.GlobalFilters.AddFilter("slugify", Slugify); + TemplateContext.GlobalFilters.AddFilter("trim", Trim); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs new file mode 100644 index 000000000..589008b56 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Templates/Extensions/StringWordsFluidExtension.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Fluid; +using Fluid.Values; + +namespace Squidex.Domain.Apps.Core.Templates.Extensions +{ + public class StringWordsFluidExtension : IFluidExtension + { + private static readonly FilterDelegate WordCount = (input, arguments, context) => + { + return FluidValue.Create(TextHelpers.WordCount(input.ToStringValue())); + }; + + private static readonly FilterDelegate CharacterCount = (input, arguments, context) => + { + return FluidValue.Create(TextHelpers.CharacterCount(input.ToStringValue())); + }; + + public void RegisterGlobalTypes(IMemberAccessStrategy memberAccessStrategy) + { + TemplateContext.GlobalFilters.AddFilter("word_count", WordCount); + TemplateContext.GlobalFilters.AddFilter("character_count", CharacterCount); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/TextHelpers.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/TextHelpers.cs new file mode 100644 index 000000000..0c8171b46 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/TextHelpers.cs @@ -0,0 +1,137 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text; +using HtmlAgilityPack; +using Markdig; + +namespace Squidex.Domain.Apps.Core +{ + public static class TextHelpers + { + public static string Markdown2Text(string markdown) + { + return Markdown.ToPlainText(markdown).Trim(' ', '\n', '\r'); + } + + public static string Html2Text(string html) + { + var document = LoadHtml(html); + + var sb = new StringBuilder(); + + WriteTextTo(document.DocumentNode, sb); + + return sb.ToString().Trim(' ', '\n', '\r'); + } + + private static HtmlDocument LoadHtml(string text) + { + var document = new HtmlDocument(); + + document.LoadHtml(text); + + return document; + } + + private static void WriteTextTo(HtmlNode node, StringBuilder sb) + { + switch (node.NodeType) + { + case HtmlNodeType.Comment: + break; + case HtmlNodeType.Document: + WriteChildrenTextTo(node, sb); + break; + case HtmlNodeType.Text: + var html = ((HtmlTextNode)node).Text; + + if (HtmlNode.IsOverlappedClosingElement(html)) + { + break; + } + + if (!string.IsNullOrWhiteSpace(html)) + { + sb.Append(HtmlEntity.DeEntitize(html)); + } + + break; + + case HtmlNodeType.Element: + switch (node.Name) + { + case "p": + sb.AppendLine(); + break; + case "br": + sb.AppendLine(); + break; + case "style": + return; + case "script": + return; + } + + if (node.HasChildNodes) + { + WriteChildrenTextTo(node, sb); + } + + break; + } + } + + private static void WriteChildrenTextTo(HtmlNode node, StringBuilder sb) + { + foreach (var child in node.ChildNodes) + { + WriteTextTo(child, sb); + } + } + + public static int CharacterCount(string text) + { + var characterCount = 0; + + for (int i = 0; i < text.Length; i++) + { + if (char.IsLetter(text[i])) + { + characterCount++; + } + } + + return characterCount; + } + + public static int WordCount(string text) + { + var numWords = 0; + + for (int i = 1; i < text.Length; i++) + { + if (char.IsWhiteSpace(text[i - 1])) + { + var character = text[i]; + + if (char.IsLetterOrDigit(character) || char.IsPunctuation(character)) + { + numWords++; + } + } + } + + if (text.Length > 2) + { + numWords++; + } + + return numWords; + } + } +} diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 62702c299..e2c90bfb4 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -86,9 +86,16 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); + services.AddSingleton>(DomainObjectGrainFormatter.Format); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index a1dfae6b5..9348c140c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -97,6 +97,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules new DateTimeFluidExtension(), new EventFluidExtensions(urlGenerator), new StringFluidExtension(), + new StringWordsFluidExtension(), new UserFluidExtension() }; diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs index 72c817403..cbd5961a1 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Templates/FluidTemplateEngineTests.cs @@ -24,7 +24,9 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates { var extensions = new IFluidExtension[] { - new DateTimeFluidExtension() + new DateTimeFluidExtension(), + new StringFluidExtension(), + new StringWordsFluidExtension() }; sut = new FluidTemplateEngine(extensions); @@ -113,6 +115,66 @@ namespace Squidex.Domain.Apps.Core.Operations.Templates Assert.Equal("Hello", result); } + [Fact] + public async Task Should_format_html_to_text() + { + var template = "{{ e.text | html2text }}"; + + var value = new + { + Text = "

Hello World

" + }; + + var result = await RenderAync(template, value); + + Assert.Equal("Hello World", result); + } + + [Fact] + public async Task Should_convert_markdown_to_text() + { + var template = "{{ e.text | markdown2text }}"; + + var value = new + { + Text = "## Hello World" + }; + + var result = await RenderAync(template, value); + + Assert.Equal("Hello World", result); + } + + [Fact] + public async Task Should_format_word_count() + { + var template = "{{ e.text | word_count }}"; + + var value = new + { + Text = "Hello World" + }; + + var result = await RenderAync(template, value); + + Assert.Equal("2", result); + } + + [Fact] + public async Task Should_format_character_count() + { + var template = "{{ e.text | character_count }}"; + + var value = new + { + text = "Hello World" + }; + + var result = await RenderAync(template, value); + + Assert.Equal("10", result); + } + [Fact] public async Task Should_throw_exception_when_template_invalid() {