Make HtmlFormattableString public

We've had this class for a while backing the implementation of the
AppendFormat extension method. Making this public so we can use it in MVC
in localization.

Some updates to the API surface and name to be aligned with
System.FormattableString
This commit is contained in:
Ryan Nowak 2016-04-06 13:46:26 -07:00
parent ffaf2c8b23
commit 89c9c3260b
3 changed files with 403 additions and 144 deletions

View File

@ -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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="IHtmlContent"/> implementation of composite string formatting
/// (see https://msdn.microsoft.com/en-us/library/txafckwd(v=vs.110).aspx) which HTML encodes
/// formatted arguments.
/// </summary>
[DebuggerDisplay("{DebuggerToString()}")]
public class HtmlFormattableString : IHtmlContent
{
private readonly IFormatProvider _formatProvider;
private readonly string _format;
private readonly object[] _args;
/// <summary>
/// Creates a new <see cref="HtmlFormattableString"/> with the given <paramref name="format"/> and
/// <paramref name="args"/>.
/// </summary>
/// <param name="format">A composite format string.</param>
/// <param name="args">An array that contains objects to format.</param>
public HtmlFormattableString(string format, params object[] args)
: this(formatProvider: null, format: format, args: args)
{
}
/// <summary>
/// Creates a new <see cref="HtmlFormattableString"/> with the given <paramref name="formatProvider"/>,
/// <paramref name="format"/> and <paramref name="args"/>.
/// </summary>
/// <param name="formatProvider">An object that provides culture-specific formatting information.</param>
/// <param name="format">A composite format string.</param>
/// <param name="args">An array that contains objects to format.</param>
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;
}
/// <inheritdoc />
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;
}
}
}
}

View File

@ -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();
}
}
}
}