diff --git a/src/Microsoft.AspNet.Html.Abstractions/HtmlContentBuilderExtensions.cs b/src/Microsoft.AspNet.Html.Abstractions/HtmlContentBuilderExtensions.cs
index bfd5e3ef03..0e15077764 100644
--- a/src/Microsoft.AspNet.Html.Abstractions/HtmlContentBuilderExtensions.cs
+++ b/src/Microsoft.AspNet.Html.Abstractions/HtmlContentBuilderExtensions.cs
@@ -3,7 +3,9 @@
using System;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
+using System.Text;
using Microsoft.Framework.WebEncoders;
namespace Microsoft.AspNet.Html.Abstractions
@@ -13,6 +15,82 @@ namespace Microsoft.AspNet.Html.Abstractions
///
public static class HtmlContentBuilderExtensions
{
+ ///
+ /// Appends the specified to the existing content after replacing each format
+ /// item with the HTML encoded representation of the corresponding item in the
+ /// array.
+ ///
+ ///
+ /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx).
+ /// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed.
+ ///
+ ///
+ /// The object array to format. Each element in the array will be formatted and then HTML encoded.
+ ///
+ /// A reference to this instance after the append operation has completed.
+ public static IHtmlContentBuilder AppendFormat(
+ this IHtmlContentBuilder builder,
+ string format,
+ params object[] args)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (format == null)
+ {
+ throw new ArgumentNullException(nameof(format));
+ }
+
+ if (args == null)
+ {
+ throw new ArgumentNullException(nameof(args));
+ }
+
+ builder.Append(new HtmlFormatString(format, args));
+ return builder;
+ }
+
+ ///
+ /// Appends the specified to the existing content with information from the
+ /// after replacing each format item with the HTML encoded
+ /// representation of the corresponding item in the array.
+ ///
+ /// An object that supplies culture-specific formatting information.
+ ///
+ /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx).
+ /// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed.
+ ///
+ ///
+ /// The object array to format. Each element in the array will be formatted and then HTML encoded.
+ ///
+ /// A reference to this instance after the append operation has completed.
+ public static IHtmlContentBuilder AppendFormat(
+ this IHtmlContentBuilder builder,
+ IFormatProvider formatProvider,
+ string format,
+ params object[] args)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (format == null)
+ {
+ throw new ArgumentNullException(nameof(format));
+ }
+
+ if (args == null)
+ {
+ throw new ArgumentNullException(nameof(args));
+ }
+
+ builder.Append(new HtmlFormatString(formatProvider, format, args));
+ return builder;
+ }
+
///
/// Appends an .
///
@@ -132,5 +210,141 @@ namespace Microsoft.AspNet.Html.Abstractions
}
}
}
+
+ [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, IHtmlEncoder 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 IHtmlEncoder _encoder;
+ private readonly IFormatProvider _formatProvider;
+
+ public EncodingFormatProvider(IFormatProvider formatProvider, IHtmlEncoder encoder)
+ {
+ Debug.Assert(formatProvider != null);
+ Debug.Assert(encoder != null);
+
+ _formatProvider = formatProvider;
+ _encoder = encoder;
+ }
+
+ public string Format(string format, object arg, IFormatProvider formatProvider)
+ {
+ // This is the case we need to special case. We trust the IHtmlContent instance to do the
+ // right thing with encoding.
+ 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.HtmlEncode(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.HtmlEncode(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.HtmlEncode(result);
+ }
+ }
+
+ return string.Empty;
+ }
+
+ public object GetFormat(Type formatType)
+ {
+ if (formatType == typeof(ICustomFormatter))
+ {
+ return this;
+ }
+
+ return null;
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNet.Html.Abstractions/project.json b/src/Microsoft.AspNet.Html.Abstractions/project.json
index eee0e0ab1b..e4f6452bf1 100644
--- a/src/Microsoft.AspNet.Html.Abstractions/project.json
+++ b/src/Microsoft.AspNet.Html.Abstractions/project.json
@@ -16,7 +16,7 @@
"dnx451": { },
"dnxcore50": {
"dependencies": {
- "System.Resources.ResourceManager": "4.0.1-beta-"
+ "System.Resources.ResourceManager": "4.0.1-beta-*"
}
}
}
diff --git a/src/Microsoft.Framework.WebEncoders.Core/project.json b/src/Microsoft.Framework.WebEncoders.Core/project.json
index 54dbd1c28e..0da3e70874 100644
--- a/src/Microsoft.Framework.WebEncoders.Core/project.json
+++ b/src/Microsoft.Framework.WebEncoders.Core/project.json
@@ -18,7 +18,7 @@
"System.Diagnostics.Debug": "4.0.11-beta-*",
"System.IO": "4.0.11-beta-*",
"System.Reflection": "4.0.10-*",
- "System.Resources.ResourceManager": "4.0.1-beta-",
+ "System.Resources.ResourceManager": "4.0.1-beta-*",
"System.Runtime.Extensions": "4.0.11-beta-*",
"System.Threading": "4.0.11-beta-*"
}
diff --git a/test/Microsoft.AspNet.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs b/test/Microsoft.AspNet.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs
index 3aba376094..7c340933ec 100644
--- a/test/Microsoft.AspNet.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs
+++ b/test/Microsoft.AspNet.Html.Abstractions.Test/HtmlContentBuilderExtensionsTest.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using Microsoft.AspNet.Testing;
using Microsoft.Framework.WebEncoders;
using Microsoft.Framework.WebEncoders.Testing;
using Xunit;
@@ -126,6 +128,220 @@ namespace Microsoft.AspNet.Html.Abstractions.Test
entry => Assert.Equal("Hi", Assert.IsType(entry).Value));
}
+ [Fact]
+ public void Builder_AppendFormat()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("{0} {1} {2} {3}!", "First", "Second", "Third", "Fourth");
+
+ // Assert
+ Assert.Equal(
+ "HtmlEncode[[First]] HtmlEncode[[Second]] HtmlEncode[[Third]] HtmlEncode[[Fourth]]!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_HtmlContent()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("{0}!", new EncodedString("First"));
+
+ // Assert
+ Assert.Equal(
+ "First!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormatContent_With1Argument()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("0x{0:X} - hex equivalent for 50.", 50);
+
+ // Assert
+ Assert.Equal(
+ "0xHtmlEncode[[32]] - hex equivalent for 50.",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormatContent_With2Arguments()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("0x{0:X} - hex equivalent for {1}.", 50, 50);
+
+ // Assert
+ Assert.Equal(
+ "0xHtmlEncode[[32]] - hex equivalent for HtmlEncode[[50]].",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormatContent_With3Arguments()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("0x{0:X} - {1} equivalent for {2}.", 50, "hex", 50);
+
+ // Assert
+ Assert.Equal(
+ "0xHtmlEncode[[32]] - HtmlEncode[[hex]] equivalent for HtmlEncode[[50]].",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithAlignmentComponent()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("{0, -25} World!", "Hello");
+
+ // Assert
+ Assert.Equal(
+ "HtmlEncode[[Hello]] World!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithFormatStringComponent()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat("0x{0:X}", 50);
+
+ // Assert
+ Assert.Equal("0xHtmlEncode[[32]]", HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithCulture()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "Numbers in InvariantCulture - {0, -5:N} {1} {2} {3}!",
+ 1.1,
+ 2.98,
+ 145.82,
+ 32.86);
+
+ // Assert
+ Assert.Equal(
+ "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]] " +
+ "HtmlEncode[[145.82]] HtmlEncode[[32.86]]!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithCulture_1Argument()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "Numbers in InvariantCulture - {0:N}!",
+ 1.1);
+
+ // Assert
+ Assert.Equal(
+ "Numbers in InvariantCulture - HtmlEncode[[1.10]]!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithCulture_2Arguments()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "Numbers in InvariantCulture - {0:N} {1}!",
+ 1.1,
+ 2.98);
+
+ // Assert
+ Assert.Equal(
+ "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]]!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithCulture_3Arguments()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat(
+ CultureInfo.InvariantCulture,
+ "Numbers in InvariantCulture - {0:N} {1} {2}!",
+ 1.1,
+ 2.98,
+ 3.12);
+
+ // Assert
+ Assert.Equal(
+ "Numbers in InvariantCulture - HtmlEncode[[1.10]] HtmlEncode[[2.98]] HtmlEncode[[3.12]]!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ public void Builder_AppendFormat_WithDifferentCulture()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+ var culture = new CultureInfo("fr-FR");
+
+ // Act
+ builder.AppendFormat(culture, "{0} in french!", 1.21);
+
+ // Assert
+ Assert.Equal(
+ "HtmlEncode[[1,21]] in french!",
+ HtmlContentToString(builder));
+ }
+
+ [Fact]
+ [ReplaceCulture]
+ public void Builder_AppendFormat_WithDifferentCurrentCulture()
+ {
+ // Arrange
+ var builder = new TestHtmlContentBuilder();
+
+ // Act
+ builder.AppendFormat(CultureInfo.CurrentCulture, "{0:D}", DateTime.Parse("01/02/2015"));
+
+ // Assert
+ Assert.Equal(
+ "HtmlEncode[[01 February 2015]]",
+ HtmlContentToString(builder));
+ }
+
private static string HtmlContentToString(IHtmlContent content)
{
using (var writer = new StringWriter())
@@ -164,7 +380,10 @@ namespace Microsoft.AspNet.Html.Abstractions.Test
public void WriteTo(TextWriter writer, IHtmlEncoder encoder)
{
- throw new NotImplementedException();
+ foreach (var entry in Entries)
+ {
+ entry.WriteTo(writer, encoder);
+ }
}
}
@@ -179,7 +398,7 @@ namespace Microsoft.AspNet.Html.Abstractions.Test
public void WriteTo(TextWriter writer, IHtmlEncoder encoder)
{
- throw new NotImplementedException();
+ writer.Write(Value);
}
}
@@ -194,7 +413,7 @@ namespace Microsoft.AspNet.Html.Abstractions.Test
public void WriteTo(TextWriter writer, IHtmlEncoder encoder)
{
- throw new NotImplementedException();
+ encoder.HtmlEncode(Value, writer);
}
}