// 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.Text.Encodings.Web; namespace Microsoft.AspNetCore.Html; /// /// An implementation using an in memory list. /// [DebuggerDisplay("{DebuggerToString()}")] 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)); } var count = Entries.Count; for (var i = 0; i < count; i++) { var entry = Entries[i]; if (entry is string entryAsString) { destination.Append(entryAsString); } else if (entry is IHtmlContentContainer entryAsContainer) { // 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)); } var count = Entries.Count; for (var i = 0; i < count; i++) { var entry = Entries[i]; if (entry is string entryAsString) { destination.Append(entryAsString); } else if (entry is IHtmlContentContainer entryAsContainer) { // 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)); } var count = Entries.Count; for (var i = 0; i < 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(); } }