Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions source/Handlebars.Test/Issues/Issue543Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Xunit;

namespace HandlebarsDotNet.Test
{
public class Issue543Tests
{
[Fact]
public void Subexpression_WriteSafeString_NotDoubleEncoded()
{
var h = Handlebars.Create();
h.RegisterHelper("func2", (writer, ctx, args) => writer.WriteSafeString("<b>bold</b>"));
h.RegisterHelper("func1", (writer, ctx, args) => writer.WriteSafeString($"<span>{args[0]}</span>"));
var result = h.Compile("{{func1 (func2)}}")(new { });
Assert.Equal("<span><b>bold</b></span>", result);
}

[Fact]
public void Standalone_WriteSafeString_NotEncoded()
{
var h = Handlebars.Create();
h.RegisterHelper("func2", (writer, ctx, args) => writer.WriteSafeString("<b>bold</b>"));
var result = h.Compile("{{func2}}")(new { });
Assert.Equal("<b>bold</b>", result);
}
}
}
20 changes: 14 additions & 6 deletions source/Handlebars/Compiler/Translation/Expression/PartialBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ internal class PartialBinder : HandlebarsExpressionVisitor
{
private static string SpecialPartialBlockName = "@partial-block";

private static string ToPartialName(object value)
{
if (value is SafeString safe) return safe.Value;
return (string) value;
}

private CompilationContext CompilationContext { get; }

public PartialBinder(CompilationContext compilationContext)
Expand Down Expand Up @@ -46,16 +52,17 @@ protected override Expression VisitPartialExpression(PartialExpression pex)
bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate));
}

var partialName = Cast<string>(pex.PartialName);
var partialNameObj = Arg<object>(pex.PartialName);
var partialName = Call(() => ToPartialName(partialNameObj));
var configuration = Arg(CompilationContext.Configuration);
var templateDelegate = FunctionBuilder.Compile(
new []
{
Call(() =>
InvokePartialWithFallback(partialName, bindingContext, writer, (ICompiledHandlebarsConfiguration) configuration)
).Expression
},
CompilationContext,
},
CompilationContext,
out _
);

Expand All @@ -67,18 +74,19 @@ out _
{
var bindingContext = CompilationContext.Args.BindingContext;
var writer = CompilationContext.Args.EncodedWriter;

if (pex.Argument != null || partialBlockTemplate != null)
{
var value = pex.Argument != null
? Arg<object>(FunctionBuilder.Reduce(pex.Argument, CompilationContext, out _))
: bindingContext.Property(o => o.Value);

var partialTemplate = Arg(partialBlockTemplate);
bindingContext = bindingContext.Call(o => o.CreateChildContext(value, partialTemplate));
}

var partialName = Cast<string>(pex.PartialName);
var partialNameObj = Arg<object>(pex.PartialName);
var partialName = Call(() => ToPartialName(partialNameObj));
var configuration = Arg(CompilationContext.Configuration);

return Call(() =>
Expand Down
6 changes: 6 additions & 0 deletions source/Handlebars/HandlebarsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public static void WriteSafeString(this in EncodedTextWriter writer, object valu
return;
}

if (value is SafeString safe)
{
writer.WriteSafeString(safe.Value);
return;
}

var current = writer.SuppressEncoding;
try
{
Expand Down
2 changes: 2 additions & 0 deletions source/Handlebars/HandlebarsUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public static bool IsFalsy(object value, bool includeZero)
return !b;
case string s:
return s == string.Empty;
case SafeString safe:
return safe.Value == string.Empty;
}

if (IsNumber(value) && !includeZero)
Expand Down
13 changes: 8 additions & 5 deletions source/Handlebars/Helpers/HelperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ public static class HelperExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object ReturnInvoke<THelperDescriptor, TOptions>(
this THelperDescriptor descriptor,
in TOptions options,
in Context context,
this THelperDescriptor descriptor,
in TOptions options,
in Context context,
in Arguments arguments
) where THelperDescriptor: class, IHelperDescriptor<TOptions>
) where THelperDescriptor: class, IHelperDescriptor<TOptions>
where TOptions : struct, IHelperOptions
{
var configuration = options.Frame.Configuration;
Expand All @@ -20,7 +20,10 @@ in Arguments arguments

descriptor.Invoke(output, options, context, arguments);

return output.ToString();
// Return a SafeString so the captured output — which already has the correct
// encoding applied by the EncodedTextWriter — is not encoded a second time
// when it is passed as an argument to an outer helper.
return new SafeString(output.ToString());
}
}
}
1 change: 1 addition & 0 deletions source/Handlebars/IO/EncodedTextWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public void Write<T>(T value)
case string v: Write(v, true); return;
case StringBuilder v: Write(v, true); return;
case Substring v: Write(v, true); return;
case SafeString safe: Write(safe.Value, false); return;

default:
WriteFormatted(value);
Expand Down
17 changes: 17 additions & 0 deletions source/Handlebars/SafeString.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace HandlebarsDotNet
{
/// <summary>
/// Wraps a string value that has already been HTML-encoded (or is intentionally unencoded HTML).
/// When written to an <see cref="EncodedTextWriter"/>, the content is passed through without
/// additional encoding. This is the return-value counterpart of
/// <see cref="HandlebarsExtensions.WriteSafeString(in EncodedTextWriter, string)"/>.
/// </summary>
internal sealed class SafeString
{
public readonly string Value;

public SafeString(string value) => Value = value;

public override string ToString() => Value;
}
}
Loading