diff --git a/.gitmodules b/.gitmodules index 5eaf7683cc..57b1b97fdf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -42,10 +42,6 @@ path = modules/Hosting url = https://github.com/aspnet/Hosting.git branch = release/2.1 -[submodule "modules/HtmlAbstractions"] - path = modules/HtmlAbstractions - url = https://github.com/aspnet/HtmlAbstractions.git - branch = release/2.1 [submodule "modules/HttpAbstractions"] path = modules/HttpAbstractions url = https://github.com/aspnet/HttpAbstractions.git diff --git a/build/artifacts.props b/build/artifacts.props index a4ec6e8774..3711cb11a1 100644 --- a/build/artifacts.props +++ b/build/artifacts.props @@ -192,7 +192,6 @@ - diff --git a/build/buildorder.props b/build/buildorder.props index 55aceb0800..d3dd091d55 100644 --- a/build/buildorder.props +++ b/build/buildorder.props @@ -8,7 +8,6 @@ - diff --git a/build/dependencies.props b/build/dependencies.props index e0fdd5fcc2..5b8b271540 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -21,16 +21,16 @@ - 2.1.6 - 2.1.6 + 2.1.1 + 2.1.1 2.1.0 2.1.1 2.1.2 2.1.2 2.1.2 2.1.2 - 2.1.6 - 2.1.6 + 2.1.1 + 2.1.1 2.1.1 2.1.1 2.1.1 @@ -43,7 +43,7 @@ 2.1.1 2.1.1 2.1.1 - 2.1.6 + 2.1.1 2.1.1 2.1.1 2.1.1 @@ -53,7 +53,7 @@ 2.1.1 2.1.1 2.1.1 - 2.1.6 + 2.1.1 2.1.1 2.1.1 2.1.1 @@ -64,21 +64,22 @@ 2.1.1 2.1.1 2.1.1 - 2.1.6 + 2.1.1 2.1.6 2.1.1 2.1.1 2.1.1 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 - 2.1.6 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 0.9.9 diff --git a/build/external-dependencies.props b/build/external-dependencies.props index ce501765b3..9740073c77 100644 --- a/build/external-dependencies.props +++ b/build/external-dependencies.props @@ -83,6 +83,7 @@ + diff --git a/build/repo.props b/build/repo.props index a44dd76881..9023800398 100644 --- a/build/repo.props +++ b/build/repo.props @@ -46,6 +46,7 @@ diff --git a/build/submodules.props b/build/submodules.props index 8030fb219f..d921cff07f 100644 --- a/build/submodules.props +++ b/build/submodules.props @@ -59,7 +59,6 @@ - diff --git a/eng/Baseline.props b/eng/Baseline.props index acae8786c0..f638420950 100644 --- a/eng/Baseline.props +++ b/eng/Baseline.props @@ -80,6 +80,13 @@ + + + 2.1.1 + + + + 2.1.1 diff --git a/eng/Dependencies.props b/eng/Dependencies.props index 917b401b0a..6df8435a82 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -13,9 +13,11 @@ + + diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 7381583fb8..de8f54f37a 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -2,6 +2,7 @@ + diff --git a/eng/tools/BaselineGenerator/baseline.xml b/eng/tools/BaselineGenerator/baseline.xml index 3687f918d6..86dc5fab87 100644 --- a/eng/tools/BaselineGenerator/baseline.xml +++ b/eng/tools/BaselineGenerator/baseline.xml @@ -8,6 +8,7 @@ + diff --git a/modules/HtmlAbstractions b/modules/HtmlAbstractions deleted file mode 160000 index 252ae0c434..0000000000 --- a/modules/HtmlAbstractions +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 252ae0c434d93f5bfaf949c35edb9ef59730d0a3 diff --git a/src/Html/Abstractions/src/HtmlContentBuilder.cs b/src/Html/Abstractions/src/HtmlContentBuilder.cs new file mode 100644 index 0000000000..e61d9f7dc8 --- /dev/null +++ b/src/Html/Abstractions/src/HtmlContentBuilder.cs @@ -0,0 +1,207 @@ +// 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.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNetCore.Html +{ + /// + /// An implementation using an in memory list. + /// + public class HtmlContentBuilder : IHtmlContentBuilder + { + /// + /// Creates a new . + /// + public HtmlContentBuilder() + : this(new List()) + { + } + + /// + /// Creates a new with the given initial capacity. + /// + /// The initial capacity of the backing store. + public HtmlContentBuilder(int capacity) + : this(new List(capacity)) + { + } + + /// + /// Gets the number of elements in the . + /// + public int Count => Entries.Count; + + /// + /// Creates a new with the given list of entries. + /// + /// + /// The list of entries. The will use this list without making a copy. + /// + public HtmlContentBuilder(IList entries) + { + if (entries == null) + { + throw new ArgumentNullException(nameof(entries)); + } + + Entries = entries; + } + + // This is not List because that would lead to wrapping all strings to IHtmlContent + // which is not space performant. + // + // In general unencoded strings are added here. We're optimizing for that case, and allocating + // a wrapper when encoded strings are used. + // + // internal for testing. + internal IList Entries { get; } + + /// + public IHtmlContentBuilder Append(string unencoded) + { + if (!string.IsNullOrEmpty(unencoded)) + { + Entries.Add(unencoded); + } + + return this; + } + + /// + public IHtmlContentBuilder AppendHtml(IHtmlContent htmlContent) + { + if (htmlContent == null) + { + return this; + } + + Entries.Add(htmlContent); + return this; + } + + /// + public IHtmlContentBuilder AppendHtml(string encoded) + { + if (!string.IsNullOrEmpty(encoded)) + { + Entries.Add(new HtmlString(encoded)); + } + + return this; + } + + /// + public IHtmlContentBuilder Clear() + { + Entries.Clear(); + return this; + } + + /// + public void CopyTo(IHtmlContentBuilder destination) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + for (var i = 0; i < Entries.Count; i++) + { + var entry = Entries[i]; + + string entryAsString; + IHtmlContentContainer entryAsContainer; + if ((entryAsString = entry as string) != null) + { + destination.Append(entryAsString); + } + else if ((entryAsContainer = entry as IHtmlContentContainer) != null) + { + // Since we're copying, do a deep flatten. + entryAsContainer.CopyTo(destination); + } + else + { + // Only string, IHtmlContent values can be added to the buffer. + destination.AppendHtml((IHtmlContent)entry); + } + } + } + + /// + public void MoveTo(IHtmlContentBuilder destination) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + + for (var i = 0; i < Entries.Count; i++) + { + var entry = Entries[i]; + + string entryAsString; + IHtmlContentContainer entryAsContainer; + if ((entryAsString = entry as string) != null) + { + destination.Append(entryAsString); + } + else if ((entryAsContainer = entry as IHtmlContentContainer) != null) + { + // Since we're moving, do a deep flatten. + entryAsContainer.MoveTo(destination); + } + else + { + // Only string, IHtmlContent values can be added to the buffer. + destination.AppendHtml((IHtmlContent)entry); + } + } + + Entries.Clear(); + } + + /// + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + for (var i = 0; i < Entries.Count; i++) + { + var entry = Entries[i]; + + var entryAsString = entry as string; + if (entryAsString != null) + { + encoder.Encode(writer, entryAsString); + } + else + { + // Only string, IHtmlContent values can be added to the buffer. + ((IHtmlContent)entry).WriteTo(writer, encoder); + } + } + } + + private string DebuggerToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + } +} diff --git a/src/Html/Abstractions/src/HtmlContentBuilderExtensions.cs b/src/Html/Abstractions/src/HtmlContentBuilderExtensions.cs new file mode 100644 index 0000000000..a5a83fdbb3 --- /dev/null +++ b/src/Html/Abstractions/src/HtmlContentBuilderExtensions.cs @@ -0,0 +1,223 @@ +// 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 +{ + /// + /// Extension methods for . + /// + 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 . + /// + /// 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.AppendHtml(new HtmlFormattableString(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. + /// + /// The . + /// 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.AppendHtml(new HtmlFormattableString(formatProvider, format, args)); + return builder; + } + + /// + /// Appends an . + /// + /// The . + /// The . + public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AppendHtml(HtmlString.NewLine); + return builder; + } + + /// + /// Appends an after appending the value. + /// The value is treated as unencoded as-provided, and will be HTML encoded before writing to output. + /// + /// The . + /// The to append. + /// The . + public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder, string unencoded) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Append(unencoded); + builder.AppendHtml(HtmlString.NewLine); + return builder; + } + + /// + /// Appends an after appending the value. + /// + /// The . + /// The to append. + /// The . + public static IHtmlContentBuilder AppendLine(this IHtmlContentBuilder builder, IHtmlContent content) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AppendHtml(content); + builder.AppendHtml(HtmlString.NewLine); + return builder; + } + + /// + /// Appends an after appending the value. + /// The value is treated as HTML encoded as-provided, and no further encoding will be performed. + /// + /// The . + /// The HTML encoded to append. + /// The . + public static IHtmlContentBuilder AppendHtmlLine(this IHtmlContentBuilder builder, string encoded) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.AppendHtml(encoded); + builder.AppendHtml(HtmlString.NewLine); + return builder; + } + + /// + /// Sets the content to the value. The value is treated as unencoded as-provided, + /// and will be HTML encoded before writing to output. + /// + /// The . + /// The value that replaces the content. + /// The . + public static IHtmlContentBuilder SetContent(this IHtmlContentBuilder builder, string unencoded) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Clear(); + builder.Append(unencoded); + return builder; + } + + /// + /// Sets the content to the value. + /// + /// The . + /// The value that replaces the content. + /// The . + public static IHtmlContentBuilder SetHtmlContent(this IHtmlContentBuilder builder, IHtmlContent content) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Clear(); + builder.AppendHtml(content); + return builder; + } + + /// + /// Sets the content to the value. The value is treated as HTML encoded as-provided, and + /// no further encoding will be performed. + /// + /// The . + /// The HTML encoded that replaces the content. + /// The . + public static IHtmlContentBuilder SetHtmlContent(this IHtmlContentBuilder builder, string encoded) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.Clear(); + builder.AppendHtml(encoded); + return builder; + } + } +} diff --git a/src/Html/Abstractions/src/HtmlFormattableString.cs b/src/Html/Abstractions/src/HtmlFormattableString.cs new file mode 100644 index 0000000000..24bc7c5e2f --- /dev/null +++ b/src/Html/Abstractions/src/HtmlFormattableString.cs @@ -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 +{ + /// + /// An implementation of composite string formatting + /// (see https://msdn.microsoft.com/en-us/library/txafckwd(v=vs.110).aspx) which HTML encodes + /// formatted arguments. + /// + [DebuggerDisplay("{DebuggerToString()}")] + public class HtmlFormattableString : IHtmlContent + { + private readonly IFormatProvider _formatProvider; + private readonly string _format; + private readonly object[] _args; + + /// + /// Creates a new with the given and + /// . + /// + /// A composite format string. + /// An array that contains objects to format. + public HtmlFormattableString(string format, params object[] args) + : this(formatProvider: null, format: format, args: args) + { + } + + /// + /// Creates a new with the given , + /// and . + /// + /// An object that provides culture-specific formatting information. + /// A composite format string. + /// An array that contains objects to format. + 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; + } + + /// + 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 HtmlString or IHtmlContent instance + // to do the right thing with encoding. + var htmlString = arg as HtmlString; + 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; + } + } + } +} diff --git a/src/Html/Abstractions/src/HtmlString.cs b/src/Html/Abstractions/src/HtmlString.cs new file mode 100644 index 0000000000..e7a516bd04 --- /dev/null +++ b/src/Html/Abstractions/src/HtmlString.cs @@ -0,0 +1,61 @@ +// 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.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNetCore.Html +{ + /// + /// An implementation that wraps an HTML encoded . + /// + public class HtmlString : IHtmlContent + { + /// + /// An instance for . + /// + public static readonly HtmlString NewLine = new HtmlString(Environment.NewLine); + + /// + /// An instance for . + /// + public static readonly HtmlString Empty = new HtmlString(string.Empty); + + /// + /// Creates a new . + /// + /// The HTML encoded value. + public HtmlString(string value) + { + Value = value; + } + + /// + /// Gets the HTML encoded value. + /// + public string Value { get; } + + /// + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + writer.Write(Value); + } + + /// + public override string ToString() + { + return Value ?? string.Empty; + } + } +} diff --git a/src/Html/Abstractions/src/IHtmlContent.cs b/src/Html/Abstractions/src/IHtmlContent.cs new file mode 100644 index 0000000000..2e9a0f19e2 --- /dev/null +++ b/src/Html/Abstractions/src/IHtmlContent.cs @@ -0,0 +1,22 @@ +// 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.IO; +using System.Text.Encodings.Web; + +namespace Microsoft.AspNetCore.Html +{ + /// + /// HTML content which can be written to a TextWriter. + /// + public interface IHtmlContent + { + /// + /// Writes the content by encoding it with the specified + /// to the specified . + /// + /// The to which the content is written. + /// The which encodes the content to be written. + void WriteTo(TextWriter writer, HtmlEncoder encoder); + } +} \ No newline at end of file diff --git a/src/Html/Abstractions/src/IHtmlContentBuilder.cs b/src/Html/Abstractions/src/IHtmlContentBuilder.cs new file mode 100644 index 0000000000..912fe442aa --- /dev/null +++ b/src/Html/Abstractions/src/IHtmlContentBuilder.cs @@ -0,0 +1,40 @@ +// 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. + +namespace Microsoft.AspNetCore.Html +{ + /// + /// A builder for HTML content. + /// + public interface IHtmlContentBuilder : IHtmlContentContainer + { + /// + /// Appends an instance. + /// + /// The to append. + /// The . + IHtmlContentBuilder AppendHtml(IHtmlContent content); + + /// + /// Appends a value. The value is treated as unencoded as-provided, and will be HTML + /// encoded before writing to output. + /// + /// The to append. + /// The . + IHtmlContentBuilder Append(string unencoded); + + /// + /// Appends an HTML encoded value. The value is treated as HTML encoded as-provided, and + /// no further encoding will be performed. + /// + /// The HTML encoded to append. + /// The . + IHtmlContentBuilder AppendHtml(string encoded); + + /// + /// Clears the content. + /// + /// The . + IHtmlContentBuilder Clear(); + } +} diff --git a/src/Html/Abstractions/src/IHtmlContentContainer.cs b/src/Html/Abstractions/src/IHtmlContentContainer.cs new file mode 100644 index 0000000000..f17811433c --- /dev/null +++ b/src/Html/Abstractions/src/IHtmlContentContainer.cs @@ -0,0 +1,30 @@ +// 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. + +namespace Microsoft.AspNetCore.Html +{ + /// + /// Defines a contract for instances made up of several components which + /// can be copied into an . + /// + public interface IHtmlContentContainer : IHtmlContent + { + /// + /// Copies the contained content of this into . + /// + /// The . + void CopyTo(IHtmlContentBuilder builder); + + /// + /// + /// Moves the contained content of this into . + /// + /// + /// After is called, this instance should be left + /// in an empty state. + /// + /// + /// The . + void MoveTo(IHtmlContentBuilder builder); + } +} diff --git a/src/Html/Abstractions/src/Microsoft.AspNetCore.Html.Abstractions.csproj b/src/Html/Abstractions/src/Microsoft.AspNetCore.Html.Abstractions.csproj new file mode 100644 index 0000000000..f914769d17 --- /dev/null +++ b/src/Html/Abstractions/src/Microsoft.AspNetCore.Html.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + ASP.NET Core HTML abstractions used for building HTML content. + +Commonly used types: +Microsoft.AspNetCore.Html.HtmlString +Microsoft.AspNetCore.Html.IHtmlContent + netstandard2.0 + true + aspnetcore + + + + + + + diff --git a/src/Html/Abstractions/src/Properties/AssemblyInfo.cs b/src/Html/Abstractions/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..cb0b47442c --- /dev/null +++ b/src/Html/Abstractions/src/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// 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.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Html.Abstractions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Html/Abstractions/src/baseline.netcore.json b/src/Html/Abstractions/src/baseline.netcore.json new file mode 100644 index 0000000000..29f855f97b --- /dev/null +++ b/src/Html/Abstractions/src/baseline.netcore.json @@ -0,0 +1,626 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Html.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Html.HtmlContentBuilder", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + ], + "Members": [ + { + "Kind": "Method", + "Name": "CopyTo", + "Parameters": [ + { + "Name": "destination", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContentContainer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveTo", + "Parameters": [ + { + "Name": "destination", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContentContainer", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContent", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Count", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "unencoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendHtml", + "Parameters": [ + { + "Name": "htmlContent", + "Type": "Microsoft.AspNetCore.Html.IHtmlContent" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendHtml", + "Parameters": [ + { + "Name": "encoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "capacity", + "Type": "System.Int32" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "entries", + "Type": "System.Collections.Generic.IList" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Html.HtmlContentBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AppendFormat", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "format", + "Type": "System.String" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendFormat", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "formatProvider", + "Type": "System.IFormatProvider" + }, + { + "Name": "format", + "Type": "System.String" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendLine", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendLine", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "unencoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendLine", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "content", + "Type": "Microsoft.AspNetCore.Html.IHtmlContent" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendHtmlLine", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "encoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetContent", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "unencoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetHtmlContent", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "content", + "Type": "Microsoft.AspNetCore.Html.IHtmlContent" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetHtmlContent", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + }, + { + "Name": "encoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Html.HtmlFormattableString", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Html.IHtmlContent" + ], + "Members": [ + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContent", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "format", + "Type": "System.String" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "formatProvider", + "Type": "System.IFormatProvider" + }, + { + "Name": "format", + "Type": "System.String" + }, + { + "Name": "args", + "Type": "System.Object[]", + "IsParams": true + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Html.HtmlString", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Html.IHtmlContent" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_Value", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Html.IHtmlContent", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ToString", + "Parameters": [], + "ReturnType": "System.String", + "Virtual": true, + "Override": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NewLine", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Html.HtmlString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "Empty", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Html.HtmlString", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Html.IHtmlContent", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "WriteTo", + "Parameters": [ + { + "Name": "writer", + "Type": "System.IO.TextWriter" + }, + { + "Name": "encoder", + "Type": "System.Text.Encodings.Web.HtmlEncoder" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Html.IHtmlContentContainer" + ], + "Members": [ + { + "Kind": "Method", + "Name": "AppendHtml", + "Parameters": [ + { + "Name": "content", + "Type": "Microsoft.AspNetCore.Html.IHtmlContent" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Append", + "Parameters": [ + { + "Name": "unencoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AppendHtml", + "Parameters": [ + { + "Name": "encoded", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "Clear", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Html.IHtmlContentBuilder", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Html.IHtmlContentContainer", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Html.IHtmlContent" + ], + "Members": [ + { + "Kind": "Method", + "Name": "CopyTo", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MoveTo", + "Parameters": [ + { + "Name": "builder", + "Type": "Microsoft.AspNetCore.Html.IHtmlContentBuilder" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Html/Abstractions/test/HtmlContentBuilderExtensionsTest.cs b/src/Html/Abstractions/test/HtmlContentBuilderExtensionsTest.cs new file mode 100644 index 0000000000..c14daeeebb --- /dev/null +++ b/src/Html/Abstractions/test/HtmlContentBuilderExtensionsTest.cs @@ -0,0 +1,463 @@ +// 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.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.WebEncoders.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Html.Test +{ + public class HtmlContentBuilderExtensionsTest + { + [Fact] + public void Builder_AppendLine_Empty() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendLine(); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_AppendLine_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendLine("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value), + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_AppendLine_IHtmlContent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + var content = new OtherHtmlContent("Hi"); + + // Act + builder.AppendLine(content); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Same(content, entry), + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_AppendHtmlLine_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendHtmlLine("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value), + entry => Assert.Equal(Environment.NewLine, HtmlContentToString(entry))); + } + + [Fact] + public void Builder_SetContent_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + builder.Append("Existing Content. Will be Cleared."); + + // Act + builder.SetContent("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Equal("Hi", Assert.IsType(entry).Value)); + } + + [Fact] + public void Builder_SetContent_IHtmlContent() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + builder.Append("Existing Content. Will be Cleared."); + + var content = new OtherHtmlContent("Hi"); + + // Act + builder.SetHtmlContent(content); + + // Assert + Assert.Collection( + builder.Entries, + entry => Assert.Same(content, entry)); + } + + [Fact] + public void Builder_SetHtmlContent_String() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + builder.Append("Existing Content. Will be Cleared."); + + // Act + builder.SetHtmlContent("Hi"); + + // Assert + Assert.Collection( + builder.Entries, + 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_AppendFormat_HtmlString() + { + // Arrange + var builder = new TestHtmlContentBuilder(); + + // Act + builder.AppendFormat("{0}!", new HtmlString("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("de-DE", "de-DE")] + 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[[Sonntag, 1. Februar 2015]]", + HtmlContentToString(builder)); + } + + private static string HtmlContentToString(IHtmlContent content) + { + using (var writer = new StringWriter()) + { + content.WriteTo(writer, new HtmlTestEncoder()); + return writer.ToString(); + } + } + + private class TestHtmlContentBuilder : IHtmlContentBuilder + { + public List Entries { get; } = new List(); + + public IHtmlContentBuilder Append(string unencoded) + { + Entries.Add(new UnencodedString(unencoded)); + return this; + } + + public IHtmlContentBuilder AppendHtml(IHtmlContent content) + { + Entries.Add(content); + return this; + } + + public IHtmlContentBuilder AppendHtml(string encoded) + { + Entries.Add(new EncodedString(encoded)); + return this; + } + + public IHtmlContentBuilder Clear() + { + Entries.Clear(); + return this; + } + + public void CopyTo(IHtmlContentBuilder destination) + { + foreach (var entry in Entries) + { + destination.AppendHtml(entry); + } + } + + public void MoveTo(IHtmlContentBuilder destination) + { + CopyTo(destination); + Clear(); + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + foreach (var entry in Entries) + { + entry.WriteTo(writer, encoder); + } + } + } + + private class EncodedString : IHtmlContent + { + public EncodedString(string value) + { + Value = value; + } + + public string Value { get; } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + writer.Write(Value); + } + } + + private class UnencodedString : IHtmlContent + { + public UnencodedString(string value) + { + Value = value; + } + + public string Value { get; } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + encoder.Encode(writer, Value); + } + } + + private class OtherHtmlContent : IHtmlContent + { + public OtherHtmlContent(string value) + { + Value = value; + } + + public string Value { get; } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/src/Html/Abstractions/test/HtmlContentBuilderTest.cs b/src/Html/Abstractions/test/HtmlContentBuilderTest.cs new file mode 100644 index 0000000000..c3cb7d1954 --- /dev/null +++ b/src/Html/Abstractions/test/HtmlContentBuilderTest.cs @@ -0,0 +1,276 @@ +// 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.IO; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Html; +using Microsoft.Extensions.WebEncoders.Testing; +using Xunit; + +namespace Microsoft.Extensions.Internal +{ + public class HtmlContentBuilderTest + { + [Fact] + public void AppendString_AppendsAString() + { + // Arrange + var content = new HtmlContentBuilder(); + + // Act + content.Append("Hello"); + + // Assert + Assert.Equal(1, content.Count); + var result = Assert.Single(content.Entries); + Assert.IsType(result); + } + + [Fact] + public void AppendString_WrittenAsEncoded() + { + // Arrange + var content = new HtmlContentBuilder(); + content.Append("Hello"); + + var writer = new StringWriter(); + + // Act + content.WriteTo(writer, new HtmlTestEncoder()); + + // Assert + Assert.Equal("HtmlEncode[[Hello]]", writer.ToString()); + } + + [Fact] + public void AppendHtml_DoesNotGetWrittenAsEncoded() + { + // Arrange + var content = new HtmlContentBuilder(); + content.AppendHtml("Hello"); + + var writer = new StringWriter(); + + // Act + content.WriteTo(writer, new HtmlTestEncoder()); + + // Assert + Assert.Equal("Hello", writer.ToString()); + } + + [Fact] + public void AppendIHtmlContent_AppendsAsIs() + { + // Arrange + var content = new HtmlContentBuilder(); + var writer = new StringWriter(); + + // Act + content.AppendHtml(new TestHtmlContent("Hello")); + + // Assert + Assert.Equal(1, content.Count); + var result = Assert.Single(content.Entries); + var testHtmlContent = Assert.IsType(result); + testHtmlContent.WriteTo(writer, new HtmlTestEncoder()); + Assert.Equal("Written from TestHtmlContent: Hello", writer.ToString()); + } + + [Fact] + public void CanAppendMultipleItems() + { + // Arrange + var content = new HtmlContentBuilder(); + + // Act + content.AppendHtml(new TestHtmlContent("hello")); + content.Append("Test"); + + // Assert + Assert.Equal(2, content.Count); + Assert.Collection( + content.Entries, + entry => Assert.Equal("Written from TestHtmlContent: hello", entry.ToString()), + entry => Assert.Equal("Test", entry)); + } + + [Fact] + public void Clear_DeletesAllItems() + { + // Arrange + var content = new HtmlContentBuilder(); + content.AppendHtml(new TestHtmlContent("hello")); + content.Append("Test"); + + // Act + content.Clear(); + + // Assert + Assert.Equal(0, content.Count); + Assert.Empty(content.Entries); + } + + [Fact] + public void CopyTo_CopiesAllItems() + { + // Arrange + var source = new HtmlContentBuilder(); + source.AppendHtml(new TestHtmlContent("hello")); + source.Append("Test"); + + var destination = new HtmlContentBuilder(); + destination.Append("some-content"); + + // Act + source.CopyTo(destination); + + // Assert + Assert.Equal(2, source.Count); + Assert.Equal(3, destination.Count); + Assert.Collection( + destination.Entries, + entry => Assert.Equal("some-content", Assert.IsType(entry)), + entry => Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(entry)), + entry => Assert.Equal("Test", Assert.IsType(entry))); + } + + [Fact] + public void CopyTo_DoesDeepCopy() + { + // Arrange + var source = new HtmlContentBuilder(); + + var nested = new HtmlContentBuilder(); + source.AppendHtml(nested); + nested.AppendHtml(new TestHtmlContent("hello")); + source.Append("Test"); + + var destination = new HtmlContentBuilder(); + destination.Append("some-content"); + + // Act + source.CopyTo(destination); + + // Assert + Assert.Equal(2, source.Count); + Assert.Equal(1, nested.Count); + Assert.Equal(3, destination.Count); + Assert.Collection( + destination.Entries, + entry => Assert.Equal("some-content", Assert.IsType(entry)), + entry => Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(entry)), + entry => Assert.Equal("Test", Assert.IsType(entry))); + } + + [Fact] + public void MoveTo_CopiesAllItems_AndClears() + { + // Arrange + var source = new HtmlContentBuilder(); + source.AppendHtml(new TestHtmlContent("hello")); + source.Append("Test"); + + var destination = new HtmlContentBuilder(); + destination.Append("some-content"); + + // Act + source.MoveTo(destination); + + // Assert + Assert.Equal(0, source.Count); + Assert.Equal(3, destination.Count); + Assert.Collection( + destination.Entries, + entry => Assert.Equal("some-content", Assert.IsType(entry)), + entry => Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(entry)), + entry => Assert.Equal("Test", Assert.IsType(entry))); + } + + [Fact] + public void MoveTo_DoesDeepMove() + { + // Arrange + var source = new HtmlContentBuilder(); + + var nested = new HtmlContentBuilder(); + source.AppendHtml(nested); + nested.AppendHtml(new TestHtmlContent("hello")); + source.Append("Test"); + + var destination = new HtmlContentBuilder(); + destination.Append("some-content"); + + // Act + source.MoveTo(destination); + + // Assert + Assert.Equal(0, source.Count); + Assert.Equal(0, nested.Count); + Assert.Equal(3, destination.Count); + Assert.Collection( + destination.Entries, + entry => Assert.Equal("some-content", Assert.IsType(entry)), + entry => Assert.Equal(new TestHtmlContent("hello"), Assert.IsType(entry)), + entry => Assert.Equal("Test", Assert.IsType(entry))); + } + + [Fact] + public void WriteTo_WritesAllItems() + { + // Arrange + var content = new HtmlContentBuilder(); + var writer = new StringWriter(); + content.AppendHtml(new TestHtmlContent("Hello")); + content.Append("Test"); + + // Act + content.WriteTo(writer, new HtmlTestEncoder()); + + // Assert + Assert.Equal(2, content.Count); + Assert.Equal("Written from TestHtmlContent: HelloHtmlEncode[[Test]]", writer.ToString()); + } + + private class TestHtmlContent : IHtmlContent, IEquatable + { + private string _content; + + public TestHtmlContent(string content) + { + _content = content; + } + + public void WriteTo(TextWriter writer, HtmlEncoder encoder) + { + writer.Write(ToString()); + } + + public override string ToString() + { + return "Written from TestHtmlContent: " + _content; + } + + public override int GetHashCode() + { + return _content.GetHashCode(); + } + + public override bool Equals(object obj) + { + var other = obj as TestHtmlContent; + if (other != null) + { + return Equals(other); + } + + return base.Equals(obj); + } + + public bool Equals(TestHtmlContent other) + { + return string.Equals(_content, other._content); + } + } + } +} diff --git a/src/Html/Abstractions/test/HtmlFormattableStringTest.cs b/src/Html/Abstractions/test/HtmlFormattableStringTest.cs new file mode 100644 index 0000000000..64e000751e --- /dev/null +++ b/src/Html/Abstractions/test/HtmlFormattableStringTest.cs @@ -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_WithHtmlString() + { + // Arrange + var formattableString = new HtmlFormattableString("{0}!", new HtmlString("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 HtmlString("Birthday"), + new HtmlContentBuilder().Append("Billy")); + + // Act + var result = HtmlContentToString(formattableString); + + // Assert + Assert.Equal("Happy Birthday, HtmlEncode[[Billy]]!", result); + } + + [Fact] + public void HtmlFormattableString_WithHtmlString_AndOffset() + { + // Arrange + var formattableString = new HtmlFormattableString("{0, 20}!", new HtmlString("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(); + } + } + } +} diff --git a/src/Html/Abstractions/test/Microsoft.AspNetCore.Html.Abstractions.Tests.csproj b/src/Html/Abstractions/test/Microsoft.AspNetCore.Html.Abstractions.Tests.csproj new file mode 100644 index 0000000000..0e77dbbfb3 --- /dev/null +++ b/src/Html/Abstractions/test/Microsoft.AspNetCore.Html.Abstractions.Tests.csproj @@ -0,0 +1,12 @@ + + + + $(StandardTestTfms) + + + + + + + +