diff --git a/Lombiq.VueJs.Samples/Assets/Scripts/VueComponents/demo-sfc.vue b/Lombiq.VueJs.Samples/Assets/Scripts/VueComponents/demo-sfc.vue
index c63f550..29cfa34 100644
--- a/Lombiq.VueJs.Samples/Assets/Scripts/VueComponents/demo-sfc.vue
+++ b/Lombiq.VueJs.Samples/Assets/Scripts/VueComponents/demo-sfc.vue
@@ -18,6 +18,31 @@
[[ Hello! ]]
+
+
+
[[{ Does HTML localization escape HTML? YES! NO! }]]
+
+
+ [[{liquid}
+
+
{{ "Liquid example! (localized)" | t }}
+ The current time is: {{ "now" | utc | date: "%c" }}
+ ]]
+
+
+ [[{markdown}
+## Markdown Example
+
+Here is some _Markdown_ content. For more info, see [the docs](https://docs.orchardcore.net/en/main/docs/reference/modules/Markdown/).
+ ]]
+
+
diff --git a/Lombiq.VueJs.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs b/Lombiq.VueJs.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
index 28ceb14..2dd8a7d 100644
--- a/Lombiq.VueJs.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
+++ b/Lombiq.VueJs.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
@@ -51,6 +51,15 @@ public static async Task TestVueSfcASync(this UITestContext context)
// Verify localizer HTML escaping.
context.Missing(By.ClassName("not-html"));
+ context.Exists(By.ClassName("encoded-html"));
+
+ // Verify Liquid and Markdown working.
+ context.Get(By.CssSelector(".a-liquid-example h2")).Text.ShouldContain("Liquid example! (localized)");
+ context.Get(By.Id("markdown-example")).Text.ShouldContain("Markdown Example");
+ context
+ .Get(By.LinkText("the docs"))
+ .GetAttribute("href")
+ .ShouldBe("https://docs.orchardcore.net/en/main/docs/reference/modules/Markdown/");
}
public static async Task TestVueSfcEnhancedListAsync(this UITestContext context)
diff --git a/Lombiq.VueJs/Services/IVueTemplateExpressionConverter.cs b/Lombiq.VueJs/Services/IVueTemplateExpressionConverter.cs
new file mode 100644
index 0000000..c53afe8
--- /dev/null
+++ b/Lombiq.VueJs/Services/IVueTemplateExpressionConverter.cs
@@ -0,0 +1,22 @@
+using OrchardCore.DisplayManagement.Implementation;
+using System.Threading.Tasks;
+
+namespace Lombiq.VueJs.Services;
+
+///
+/// A service that handles [[{name} input ]] expressions in Vue SFC templates. Used by .
+///
+public interface IVueTemplateExpressionConverter
+{
+ ///
+ /// Returns a value indicating whether this converter should handle the provided , typically
+ /// based on the .
+ ///
+ bool IsApplicable(string name, string input, DisplayContext displayContext);
+
+ ///
+ /// Returns the output that should be substituted instead of the provided expression.
+ ///
+ ValueTask ConvertAsync(string name, string input, DisplayContext displayContext);
+}
diff --git a/Lombiq.VueJs/Services/LiquidVueTemplateExpressionConverter.cs b/Lombiq.VueJs/Services/LiquidVueTemplateExpressionConverter.cs
new file mode 100644
index 0000000..69f0f1d
--- /dev/null
+++ b/Lombiq.VueJs/Services/LiquidVueTemplateExpressionConverter.cs
@@ -0,0 +1,24 @@
+using Fluid;
+using OrchardCore.DisplayManagement.Implementation;
+using OrchardCore.Liquid;
+using System;
+using System.Threading.Tasks;
+
+namespace Lombiq.VueJs.Services;
+
+public class LiquidVueTemplateExpressionConverter : IVueTemplateExpressionConverter
+{
+ private readonly ILiquidTemplateManager _liquidTemplateManager;
+
+ public LiquidVueTemplateExpressionConverter(ILiquidTemplateManager liquidTemplateManager) =>
+ _liquidTemplateManager = liquidTemplateManager;
+
+ public bool IsApplicable(string name, string input, DisplayContext displayContext) =>
+ "liquid".EqualsOrdinalIgnoreCase(name);
+
+ public async ValueTask ConvertAsync(string name, string input, DisplayContext displayContext) =>
+ await _liquidTemplateManager.RenderStringAsync(
+ input,
+ NullEncoder.Default,
+ displayContext);
+}
diff --git a/Lombiq.VueJs/Services/MarkdownVueTemplateExpressionConverter.cs b/Lombiq.VueJs/Services/MarkdownVueTemplateExpressionConverter.cs
new file mode 100644
index 0000000..b7297f2
--- /dev/null
+++ b/Lombiq.VueJs/Services/MarkdownVueTemplateExpressionConverter.cs
@@ -0,0 +1,20 @@
+using OrchardCore.DisplayManagement.Implementation;
+using OrchardCore.Markdown.Services;
+using System;
+using System.Threading.Tasks;
+
+namespace Lombiq.VueJs.Services;
+
+public class MarkdownVueTemplateExpressionConverter : IVueTemplateExpressionConverter
+{
+ private readonly IMarkdownService _markdownService;
+
+ public MarkdownVueTemplateExpressionConverter(IMarkdownService markdownService) =>
+ _markdownService = markdownService;
+
+ public bool IsApplicable(string name, string input, DisplayContext displayContext) =>
+ "markdown".EqualsOrdinalIgnoreCase(name);
+
+ public ValueTask ConvertAsync(string name, string input, DisplayContext displayContext) =>
+ ValueTask.FromResult(_markdownService.ToHtml(input));
+}
diff --git a/Lombiq.VueJs/Services/VueSingleFileComponentShapeTemplateViewEngine.cs b/Lombiq.VueJs/Services/VueSingleFileComponentShapeTemplateViewEngine.cs
index 22c3a2d..90fc690 100644
--- a/Lombiq.VueJs/Services/VueSingleFileComponentShapeTemplateViewEngine.cs
+++ b/Lombiq.VueJs/Services/VueSingleFileComponentShapeTemplateViewEngine.cs
@@ -1,7 +1,8 @@
-using Lombiq.HelpfulLibraries.Common.Utilities;
using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
using OrchardCore.DisplayManagement.Descriptors.ShapeTemplateStrategy;
using OrchardCore.DisplayManagement.Implementation;
using System;
@@ -9,6 +10,7 @@
using System.IO;
using System.Linq;
using System.Net;
+using System.Text;
using System.Threading.Tasks;
namespace Lombiq.VueJs.Services;
@@ -20,7 +22,10 @@ public class VueSingleFileComponentShapeTemplateViewEngine : IShapeTemplateViewE
private readonly IShapeTemplateFileProviderAccessor _fileProviderAccessor;
private readonly IMemoryCache _memoryCache;
private readonly IStringLocalizerFactory _stringLocalizerFactory;
+ private readonly IHtmlLocalizerFactory _htmlLocalizerFactory;
+ private readonly ILogger _logger;
private readonly IEnumerable _amenders;
+ private readonly IEnumerable _converters;
public IEnumerable TemplateFileExtensions { get; } = new[] { ".vue" };
@@ -28,46 +33,117 @@ public VueSingleFileComponentShapeTemplateViewEngine(
IShapeTemplateFileProviderAccessor fileProviderAccessor,
IMemoryCache memoryCache,
IStringLocalizerFactory stringLocalizerFactory,
- IEnumerable amenders)
+ IHtmlLocalizerFactory htmlLocalizerFactory,
+ ILogger logger,
+ IEnumerable amenders,
+ IEnumerable converters)
{
_fileProviderAccessor = fileProviderAccessor;
_memoryCache = memoryCache;
_stringLocalizerFactory = stringLocalizerFactory;
+ _htmlLocalizerFactory = htmlLocalizerFactory;
+ _logger = logger;
_amenders = amenders;
+ _converters = converters;
}
public async Task RenderAsync(string relativePath, DisplayContext displayContext)
{
var template = await GetTemplateAsync(relativePath);
- var localizationRanges = template
- .AllIndexesOf("[[")
- .Where(index => template[(index + 2)..].Contains("]]"))
- .Select(index => new Range(
- index,
- template.IndexOfOrdinal(value: "]]", startIndex: index + 2) + 2))
- .WithoutOverlappingRanges(isSortedByStart: true);
+ // Remove all HTML comments. This is done first, because HTML comments take precedence over everything else.
+ // This way the contents of comments are guaranteed to not be evaluated.
+ template = template
+ .GetParenthesisRanges("")
+ .InvertRanges(template.Length)
+ .Join(template);
var shapeName = displayContext.Value.Metadata.Type;
- var stringLocalizer = _stringLocalizerFactory.Create("Vue.js SFC", shapeName);
+ var builder = new StringBuilder($"");
+ builder.Append("");
var entries = new List