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:
parent
ffaf2c8b23
commit
89c9c3260b
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue