diff --git a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilderExtensions.cs b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilderExtensions.cs
index 439b746a55..f7474ca3f2 100644
--- a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlContentBuilderExtensions.cs
@@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Html
throw new ArgumentNullException(nameof(args));
}
- builder.AppendHtml(new HtmlFormatString(format, args));
+ builder.AppendHtml(new HtmlFormattableString(format, args));
return builder;
}
@@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Html
throw new ArgumentNullException(nameof(args));
}
- builder.AppendHtml(new HtmlFormatString(formatProvider, format, args));
+ builder.AppendHtml(new HtmlFormattableString(formatProvider, format, args));
return builder;
}
@@ -219,147 +219,5 @@ namespace Microsoft.AspNetCore.Html
builder.AppendHtml(encoded);
return builder;
}
-
- [DebuggerDisplay("{DebuggerToString()}")]
- private class HtmlFormatString : IHtmlContent
- {
- private readonly IFormatProvider _formatProvider;
- private readonly string _format;
- private readonly object[] _args;
-
- public HtmlFormatString(string format, object[] args)
- : this(null, format, args)
- {
- }
-
- public HtmlFormatString(IFormatProvider formatProvider, string format, object[] args)
- {
- Debug.Assert(format != null);
- Debug.Assert(args != null);
-
- _formatProvider = formatProvider ?? CultureInfo.CurrentCulture;
- _format = format;
- _args = args;
- }
-
- public void WriteTo(TextWriter writer, HtmlEncoder encoder)
- {
- if (writer == null)
- {
- throw new ArgumentNullException(nameof(writer));
- }
-
- if (encoder == null)
- {
- throw new ArgumentNullException(nameof(encoder));
- }
-
- var formatProvider = new EncodingFormatProvider(_formatProvider, encoder);
- writer.Write(string.Format(formatProvider, _format, _args));
- }
-
- private string DebuggerToString()
- {
- using (var writer = new StringWriter())
- {
- WriteTo(writer, HtmlEncoder.Default);
- return writer.ToString();
- }
- }
- }
-
- // This class implements Html encoding via an ICustomFormatter. Passing an instance of this
- // class into a string.Format method or anything similar will evaluate arguments implementing
- // IHtmlContent without HTML encoding them, and will give other arguments the standard
- // composite format string treatment, and then HTML encode the result.
- //
- // Plenty of examples of ICustomFormatter and the interactions with string.Format here:
- // https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx#Format6_Example
- private class EncodingFormatProvider : IFormatProvider, ICustomFormatter
- {
- private readonly HtmlEncoder _encoder;
- private readonly IFormatProvider _formatProvider;
-
- public EncodingFormatProvider(IFormatProvider formatProvider, HtmlEncoder encoder)
- {
- Debug.Assert(formatProvider != null);
- Debug.Assert(encoder != null);
-
- _formatProvider = formatProvider;
- _encoder = encoder;
- }
-
- public string Format(string format, object arg, IFormatProvider formatProvider)
- {
- // These are the cases we need to special case. We trust the HtmlEncodedString or IHtmlContent instance
- // to do the right thing with encoding.
- var htmlString = arg as HtmlEncodedString;
- if (htmlString != null)
- {
- return htmlString.ToString();
- }
-
- var htmlContent = arg as IHtmlContent;
- if (htmlContent != null)
- {
- using (var writer = new StringWriter())
- {
- htmlContent.WriteTo(writer, _encoder);
- return writer.ToString();
- }
- }
-
- // If we get here then 'arg' is not an IHtmlContent, and we want to handle it the way a normal
- // string.Format would work, but then HTML encode the result.
- //
- // First check for an ICustomFormatter - if the IFormatProvider is a CultureInfo, then it's likely
- // that ICustomFormatter will be null.
- var customFormatter = (ICustomFormatter)_formatProvider.GetFormat(typeof(ICustomFormatter));
- if (customFormatter != null)
- {
- var result = customFormatter.Format(format, arg, _formatProvider);
- if (result != null)
- {
- return _encoder.Encode(result);
- }
- }
-
- // Next check if 'arg' is an IFormattable (DateTime is an example).
- //
- // An IFormattable will likely call back into the IFormatterProvider and ask for more information
- // about how to format itself. This is the typical case when IFormatterProvider is a CultureInfo.
- var formattable = arg as IFormattable;
- if (formattable != null)
- {
- var result = formattable.ToString(format, _formatProvider);
- if (result != null)
- {
- return _encoder.Encode(result);
- }
- }
-
- // If we get here then there's nothing really smart left to try.
- if (arg != null)
- {
- var result = arg.ToString();
- if (result != null)
- {
- return _encoder.Encode(result);
- }
- }
-
- return string.Empty;
- }
-
- public object GetFormat(Type formatType)
- {
- if (formatType == typeof(ICustomFormatter))
- {
- return this;
- }
-
- return null;
- }
- }
}
}
diff --git a/src/Microsoft.AspNetCore.Html.Abstractions/HtmlFormattableString.cs b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlFormattableString.cs
new file mode 100644
index 0000000000..82e9b69d98
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Html.Abstractions/HtmlFormattableString.cs
@@ -0,0 +1,184 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Text.Encodings.Web;
+
+namespace Microsoft.AspNetCore.Html
+{
+ ///
+ /// An implementation of composite string formatting
+ /// (see https://msdn.microsoft.com/en-us/library/txafckwd(v=vs.110).aspx) which HTML encodes
+ /// formatted arguments.
+ ///
+ [DebuggerDisplay("{DebuggerToString()}")]
+ public class HtmlFormattableString : IHtmlContent
+ {
+ private readonly IFormatProvider _formatProvider;
+ private readonly string _format;
+ private readonly object[] _args;
+
+ ///
+ /// Creates a new with the given and
+ /// .
+ ///
+ /// A composite format string.
+ /// An array that contains objects to format.
+ public HtmlFormattableString(string format, params object[] args)
+ : this(formatProvider: null, format: format, args: args)
+ {
+ }
+
+ ///
+ /// Creates a new with the given ,
+ /// and .
+ ///
+ /// An object that provides culture-specific formatting information.
+ /// A composite format string.
+ /// An array that contains objects to format.
+ public HtmlFormattableString(IFormatProvider formatProvider, string format, params object[] args)
+ {
+ if (format == null)
+ {
+ throw new ArgumentNullException(nameof(format));
+ }
+
+ if (args == null)
+ {
+ throw new ArgumentNullException(nameof(args));
+ }
+
+ _formatProvider = formatProvider ?? CultureInfo.CurrentCulture;
+ _format = format;
+ _args = args;
+ }
+
+ ///
+ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
+ {
+ if (writer == null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ if (encoder == null)
+ {
+ throw new ArgumentNullException(nameof(encoder));
+ }
+
+ var formatProvider = new EncodingFormatProvider(_formatProvider, encoder);
+ writer.Write(string.Format(formatProvider, _format, _args));
+ }
+
+ private string DebuggerToString()
+ {
+ using (var writer = new StringWriter())
+ {
+ WriteTo(writer, HtmlEncoder.Default);
+ return writer.ToString();
+ }
+ }
+
+ // This class implements Html encoding via an ICustomFormatter. Passing an instance of this
+ // class into a string.Format method or anything similar will evaluate arguments implementing
+ // IHtmlContent without HTML encoding them, and will give other arguments the standard
+ // composite format string treatment, and then HTML encode the result.
+ //
+ // Plenty of examples of ICustomFormatter and the interactions with string.Format here:
+ // https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx#Format6_Example
+ private class EncodingFormatProvider : IFormatProvider, ICustomFormatter
+ {
+ private readonly HtmlEncoder _encoder;
+ private readonly IFormatProvider _formatProvider;
+
+ private StringWriter _writer;
+
+ public EncodingFormatProvider(IFormatProvider formatProvider, HtmlEncoder encoder)
+ {
+ Debug.Assert(formatProvider != null);
+ Debug.Assert(encoder != null);
+
+ _formatProvider = formatProvider;
+ _encoder = encoder;
+ }
+
+ public string Format(string format, object arg, IFormatProvider formatProvider)
+ {
+ // These are the cases we need to special case. We trust the HtmlEncodedString or IHtmlContent instance
+ // to do the right thing with encoding.
+ var htmlString = arg as HtmlEncodedString;
+ if (htmlString != null)
+ {
+ return htmlString.ToString();
+ }
+
+ var htmlContent = arg as IHtmlContent;
+ if (htmlContent != null)
+ {
+ _writer = _writer ?? new StringWriter();
+
+ htmlContent.WriteTo(_writer, _encoder);
+
+ var result = _writer.ToString();
+ _writer.GetStringBuilder().Clear();
+
+ return result;
+ }
+
+ // If we get here then 'arg' is not an IHtmlContent, and we want to handle it the way a normal
+ // string.Format would work, but then HTML encode the result.
+ //
+ // First check for an ICustomFormatter - if the IFormatProvider is a CultureInfo, then it's likely
+ // that ICustomFormatter will be null.
+ var customFormatter = (ICustomFormatter)_formatProvider.GetFormat(typeof(ICustomFormatter));
+ if (customFormatter != null)
+ {
+ var result = customFormatter.Format(format, arg, _formatProvider);
+ if (result != null)
+ {
+ return _encoder.Encode(result);
+ }
+ }
+
+ // Next check if 'arg' is an IFormattable (DateTime is an example).
+ //
+ // An IFormattable will likely call back into the IFormatterProvider and ask for more information
+ // about how to format itself. This is the typical case when IFormatterProvider is a CultureInfo.
+ var formattable = arg as IFormattable;
+ if (formattable != null)
+ {
+ var result = formattable.ToString(format, _formatProvider);
+ if (result != null)
+ {
+ return _encoder.Encode(result);
+ }
+ }
+
+ // If we get here then there's nothing really smart left to try.
+ if (arg != null)
+ {
+ var result = arg.ToString();
+ if (result != null)
+ {
+ return _encoder.Encode(result);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ public object GetFormat(Type formatType)
+ {
+ if (formatType == typeof(ICustomFormatter))
+ {
+ return this;
+ }
+
+ return null;
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlFormattableStringTest.cs b/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlFormattableStringTest.cs
new file mode 100644
index 0000000000..6f466e6046
--- /dev/null
+++ b/test/Microsoft.AspNetCore.Html.Abstractions.Test/HtmlFormattableStringTest.cs
@@ -0,0 +1,217 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Globalization;
+using System.IO;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.WebEncoders.Testing;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Html
+{
+ public class HtmlFormattableStringTest
+ {
+ [Fact]
+ public void HtmlFormattableString_EmptyArgs()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("Hello, World!");
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("Hello, World!", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_EmptyArgsAndCulture()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString(CultureInfo.CurrentCulture, "Hello, World!");
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("Hello, World!", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_MultipleArguments()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("{0} {1} {2} {3}!", "First", "Second", "Third", "Fourth");
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal(
+ "HtmlEncode[[First]] HtmlEncode[[Second]] HtmlEncode[[Third]] HtmlEncode[[Fourth]]!",
+ result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_WithHtmlEncodedString()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("{0}!", new HtmlEncodedString("First"));
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("First!", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_WithOtherIHtmlContent()
+ {
+ // Arrange
+ var builder = new HtmlContentBuilder();
+ builder.Append("First");
+
+ var formattableString = new HtmlFormattableString("{0}!", builder);
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("HtmlEncode[[First]]!", result);
+ }
+
+ // This test is needed to ensure the shared StringWriter gets cleared.
+ [Fact]
+ public void HtmlFormattableString_WithMultipleHtmlContentArguments()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString(
+ "Happy {0}, {1}!",
+ new HtmlEncodedString("Birthday"),
+ new HtmlContentBuilder().Append("Billy"));
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("Happy Birthday, HtmlEncode[[Billy]]!", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_WithHtmlEncodedString_AndOffset()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("{0, 20}!", new HtmlEncodedString("First"));
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal(" First!", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_With3Arguments()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("0x{0:X} - {1} equivalent for {2}.", 50, "hex", 50);
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal(
+ "0xHtmlEncode[[32]] - HtmlEncode[[hex]] equivalent for HtmlEncode[[50]].",
+ result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_WithAlignmentComponent()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("{0, -25} World!", "Hello");
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal(
+ "HtmlEncode[[Hello]] World!", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_WithFormatStringComponent()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("0x{0:X}", 50);
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("0xHtmlEncode[[32]]", result);
+ }
+
+ [Fact]
+ public void HtmlFormattableString_WithCulture()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString(
+ CultureInfo.InvariantCulture,
+ "Numbers in InvariantCulture - {0, -5:N} {1} {2} {3}!",
+ 1.1,
+ 2.98,
+ 145.82,
+ 32.86);
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal(
+ "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]] " +
+ "HtmlEncode[[145.82]] HtmlEncode[[32.86]]!",
+ result);
+ }
+
+ [Fact]
+ [ReplaceCulture("en-US", "en-US")]
+ public void HtmlFormattableString_UsesPassedInCulture()
+ {
+ // Arrange
+ var culture = new CultureInfo("fr-FR");
+ var formattableString = new HtmlFormattableString(culture, "{0} in french!", 1.21);
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("HtmlEncode[[1,21]] in french!", result);
+ }
+
+ [Fact]
+ [ReplaceCulture("de-DE", "de-DE")]
+ public void HtmlFormattableString_UsesCurrentCulture()
+ {
+ // Arrange
+ var formattableString = new HtmlFormattableString("{0:D}", DateTime.Parse("01/02/2015"));
+
+ // Act
+ var result = HtmlContentToString(formattableString);
+
+ // Assert
+ Assert.Equal("HtmlEncode[[Sonntag, 1. Februar 2015]]", result);
+ }
+
+ private static string HtmlContentToString(IHtmlContent content)
+ {
+ using (var writer = new StringWriter())
+ {
+ content.WriteTo(writer, new HtmlTestEncoder());
+ return writer.ToString();
+ }
+ }
+ }
+}