| File: HtmlFormattableString.cs | Web Access |
| Project: src\aspnetcore\src\Html.Abstractions\src\Microsoft.AspNetCore.Html.Abstractions.csproj (Microsoft.AspNetCore.Html.Abstractions) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.Encodings.Web; namespace Microsoft.AspNetCore.Html; /// <summary> /// An <see cref="IHtmlContent"/> implementation of composite string formatting /// (see <see href="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([StringSyntax(StringSyntaxAttribute.CompositeFormat)] 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, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { ArgumentNullException.ThrowIfNull(format); ArgumentNullException.ThrowIfNull(args); _formatProvider = formatProvider ?? CultureInfo.CurrentCulture; _format = format; _args = args; } /// <inheritdoc /> public void WriteTo(TextWriter writer, HtmlEncoder encoder) { ArgumentNullException.ThrowIfNull(writer); ArgumentNullException.ThrowIfNull(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 sealed 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 HtmlString or IHtmlContent instance // to do the right thing with encoding. if (arg is HtmlString htmlString) { return htmlString.ToString(); } if (arg is IHtmlContent htmlContent) { _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. if (arg is IFormattable formattable) { 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; } } }