Skip to content

Commit a602b47

Browse files
committed
Add AppendFormat extension methods on IHtmlContent
1 parent d82bc7c commit a602b47

File tree

4 files changed

+438
-5
lines changed

4 files changed

+438
-5
lines changed

src/Microsoft.AspNet.Html.Abstractions/HtmlContentBuilderExtensions.cs

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
using System;
55
using System.Diagnostics;
6+
using System.Globalization;
67
using System.IO;
8+
using System.Text;
79
using Microsoft.Framework.WebEncoders;
810

911
namespace Microsoft.AspNet.Html.Abstractions
@@ -13,6 +15,82 @@ namespace Microsoft.AspNet.Html.Abstractions
1315
/// </summary>
1416
public static class HtmlContentBuilderExtensions
1517
{
18+
/// <summary>
19+
/// Appends the specified <paramref name="format"/> to the existing content after replacing each format
20+
/// item with the HTML encoded <see cref="string"/> representation of the corresponding item in the
21+
/// <paramref name="args"/> array.
22+
/// </summary>
23+
/// <param name="format">
24+
/// The composite format <see cref="string"/> (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx).
25+
/// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed.
26+
/// </param>
27+
/// <param name="args">
28+
/// The object array to format. Each element in the array will be formatted and then HTML encoded.
29+
/// </param>
30+
/// <returns>A reference to this instance after the append operation has completed.</returns>
31+
public static IHtmlContentBuilder AppendFormat(
32+
this IHtmlContentBuilder builder,
33+
string format,
34+
params object[] args)
35+
{
36+
if (builder == null)
37+
{
38+
throw new ArgumentNullException(nameof(builder));
39+
}
40+
41+
if (format == null)
42+
{
43+
throw new ArgumentNullException(nameof(format));
44+
}
45+
46+
if (args == null)
47+
{
48+
throw new ArgumentNullException(nameof(args));
49+
}
50+
51+
builder.Append(new HtmlFormatString(format, args));
52+
return builder;
53+
}
54+
55+
/// <summary>
56+
/// Appends the specified <paramref name="format"/> to the existing content with information from the
57+
/// <paramref name="provider"/> after replacing each format item with the HTML encoded <see cref="string"/>
58+
/// representation of the corresponding item in the <paramref name="args"/> array.
59+
/// </summary>
60+
/// <param name="formatProvider">An object that supplies culture-specific formatting information.</param>
61+
/// <param name="format">
62+
/// The composite format <see cref="string"/> (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx).
63+
/// The format string is assumed to be HTML encoded as-provided, and no further encoding will be performed.
64+
/// </param>
65+
/// <param name="args">
66+
/// The object array to format. Each element in the array will be formatted and then HTML encoded.
67+
/// </param>
68+
/// <returns>A reference to this instance after the append operation has completed.</returns>
69+
public static IHtmlContentBuilder AppendFormat(
70+
this IHtmlContentBuilder builder,
71+
IFormatProvider formatProvider,
72+
string format,
73+
params object[] args)
74+
{
75+
if (builder == null)
76+
{
77+
throw new ArgumentNullException(nameof(builder));
78+
}
79+
80+
if (format == null)
81+
{
82+
throw new ArgumentNullException(nameof(format));
83+
}
84+
85+
if (args == null)
86+
{
87+
throw new ArgumentNullException(nameof(args));
88+
}
89+
90+
builder.Append(new HtmlFormatString(formatProvider, format, args));
91+
return builder;
92+
}
93+
1694
/// <summary>
1795
/// Appends an <see cref="Environment.NewLine"/>.
1896
/// </summary>
@@ -132,5 +210,141 @@ private string DebuggerToString()
132210
}
133211
}
134212
}
213+
214+
[DebuggerDisplay("{DebuggerToString()}")]
215+
private class HtmlFormatString : IHtmlContent
216+
{
217+
private readonly IFormatProvider _formatProvider;
218+
private readonly string _format;
219+
private readonly object[] _args;
220+
221+
public HtmlFormatString(string format, object[] args)
222+
: this(null, format, args)
223+
{
224+
}
225+
226+
public HtmlFormatString(IFormatProvider formatProvider, string format, object[] args)
227+
{
228+
Debug.Assert(format != null);
229+
Debug.Assert(args != null);
230+
231+
_formatProvider = formatProvider ?? CultureInfo.CurrentCulture;
232+
_format = format;
233+
_args = args;
234+
}
235+
236+
public void WriteTo(TextWriter writer, IHtmlEncoder encoder)
237+
{
238+
if (writer == null)
239+
{
240+
throw new ArgumentNullException(nameof(writer));
241+
}
242+
243+
if (encoder == null)
244+
{
245+
throw new ArgumentNullException(nameof(encoder));
246+
}
247+
248+
var formatProvider = new EncodingFormatProvider(_formatProvider, encoder);
249+
writer.Write(string.Format(formatProvider, _format, _args));
250+
}
251+
252+
private string DebuggerToString()
253+
{
254+
using (var writer = new StringWriter())
255+
{
256+
WriteTo(writer, HtmlEncoder.Default);
257+
return writer.ToString();
258+
}
259+
}
260+
}
261+
262+
// This class implements Html encoding via an ICustomFormatter. Passing an instance of this
263+
// class into a string.Format method or anything similar will evaluate arguments implementing
264+
// IHtmlContent without HTML encoding them, and will give other arguments the standard
265+
// composite format string treatment, and then HTML encode the result.
266+
//
267+
// Plenty of examples of ICustomFormatter and the interactions with string.Format here:
268+
// https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx#Format6_Example
269+
private class EncodingFormatProvider : IFormatProvider, ICustomFormatter
270+
{
271+
private readonly IHtmlEncoder _encoder;
272+
private readonly IFormatProvider _formatProvider;
273+
274+
public EncodingFormatProvider(IFormatProvider formatProvider, IHtmlEncoder encoder)
275+
{
276+
Debug.Assert(formatProvider != null);
277+
Debug.Assert(encoder != null);
278+
279+
_formatProvider = formatProvider;
280+
_encoder = encoder;
281+
}
282+
283+
public string Format(string format, object arg, IFormatProvider formatProvider)
284+
{
285+
// This is the case we need to special case. We trust the IHtmlContent instance to do the
286+
// right thing with encoding.
287+
var htmlContent = arg as IHtmlContent;
288+
if (htmlContent != null)
289+
{
290+
using (var writer = new StringWriter())
291+
{
292+
htmlContent.WriteTo(writer, _encoder);
293+
return writer.ToString();
294+
}
295+
}
296+
297+
// If we get here then 'arg' is not an IHtmlContent, and we want to handle it the way a normal
298+
// string.Format would work, but then HTML encode the result.
299+
//
300+
// First check for an ICustomFormatter - if the IFormatProvider is a CultureInfo, then it's likely
301+
// that ICustomFormatter will be null.
302+
var customFormatter = (ICustomFormatter)_formatProvider.GetFormat(typeof(ICustomFormatter));
303+
if (customFormatter != null)
304+
{
305+
var result = customFormatter.Format(format, arg, _formatProvider);
306+
if (result != null)
307+
{
308+
return _encoder.HtmlEncode(result);
309+
}
310+
}
311+
312+
// Next check if 'arg' is an IFormattable (DateTime is an example).
313+
//
314+
// An IFormattable will likely call back into the IFormatterProvider and ask for more information
315+
// about how to format itself. This is the typical case when IFormatterProvider is a CultureInfo.
316+
var formattable = arg as IFormattable;
317+
if (formattable != null)
318+
{
319+
var result = formattable.ToString(format, _formatProvider);
320+
if (result != null)
321+
{
322+
return _encoder.HtmlEncode(result);
323+
}
324+
}
325+
326+
// If we get here then there's nothing really smart left to try.
327+
if (arg != null)
328+
{
329+
var result = arg.ToString();
330+
if (result != null)
331+
{
332+
return _encoder.HtmlEncode(result);
333+
}
334+
}
335+
336+
return string.Empty;
337+
}
338+
339+
public object GetFormat(Type formatType)
340+
{
341+
if (formatType == typeof(ICustomFormatter))
342+
{
343+
return this;
344+
}
345+
346+
return null;
347+
}
348+
}
135349
}
136350
}

src/Microsoft.AspNet.Html.Abstractions/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"dnx451": { },
1717
"dnxcore50": {
1818
"dependencies": {
19-
"System.Resources.ResourceManager": "4.0.1-beta-"
19+
"System.Resources.ResourceManager": "4.0.1-beta-*"
2020
}
2121
}
2222
}

src/Microsoft.Framework.WebEncoders.Core/project.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"System.Diagnostics.Debug": "4.0.11-beta-*",
1919
"System.IO": "4.0.11-beta-*",
2020
"System.Reflection": "4.0.10-*",
21-
"System.Resources.ResourceManager": "4.0.1-beta-",
21+
"System.Resources.ResourceManager": "4.0.1-beta-*",
2222
"System.Runtime.Extensions": "4.0.11-beta-*",
2323
"System.Threading": "4.0.11-beta-*"
2424
}

0 commit comments

Comments
 (0)