React to changes in HTML abstractions
This change implements the new API for flattening content in Razor. Also, some optimizations to avoid allocations on paths where we need to encode HTML attribute values.
This commit is contained in:
parent
5b7dc0d1bc
commit
9bd06a5dfc
|
|
@ -148,6 +148,84 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void CopyTo(IHtmlContentBuilder destination)
|
||||
{
|
||||
if (destination == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
}
|
||||
|
||||
if (_buffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Buffer.Count; i++)
|
||||
{
|
||||
var entry = Buffer[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string entryAsString;
|
||||
IHtmlContentContainer entryAsContainer;
|
||||
if ((entryAsString = entry as string) != null)
|
||||
{
|
||||
destination.Append(entryAsString);
|
||||
}
|
||||
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
|
||||
{
|
||||
entryAsContainer.CopyTo(destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
destination.AppendHtml((IHtmlContent)entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void MoveTo(IHtmlContentBuilder destination)
|
||||
{
|
||||
if (destination == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
}
|
||||
|
||||
if (_buffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < Buffer.Count; i++)
|
||||
{
|
||||
var entry = Buffer[i];
|
||||
if (entry == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string entryAsString;
|
||||
IHtmlContentContainer entryAsContainer;
|
||||
if ((entryAsString = entry as string) != null)
|
||||
{
|
||||
destination.Append(entryAsString);
|
||||
}
|
||||
else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
|
||||
{
|
||||
entryAsContainer.MoveTo(destination);
|
||||
}
|
||||
else
|
||||
{
|
||||
destination.AppendHtml((IHtmlContent)entry);
|
||||
}
|
||||
}
|
||||
|
||||
Buffer.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TagHelperContent Clear()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -128,6 +128,12 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
/// <returns>A reference to this instance after the clear operation has completed.</returns>
|
||||
public abstract TagHelperContent Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void CopyTo(IHtmlContentBuilder destination);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void MoveTo(IHtmlContentBuilder destination);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the content.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
/// <summary>
|
||||
/// Class used to represent the output of an <see cref="ITagHelper"/>.
|
||||
/// </summary>
|
||||
public class TagHelperOutput : IHtmlContent
|
||||
public class TagHelperOutput : IHtmlContentContainer
|
||||
{
|
||||
private readonly Func<bool, HtmlEncoder, Task<TagHelperContent>> _getChildContentAsync;
|
||||
private TagHelperAttributeList _attributes;
|
||||
|
|
@ -276,7 +276,100 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
return _getChildContentAsync(useCachedResult, encoder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IHtmlContentContainer.CopyTo(IHtmlContentBuilder destination)
|
||||
{
|
||||
if (destination == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
}
|
||||
|
||||
_preElement?.CopyTo(destination);
|
||||
|
||||
var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);
|
||||
|
||||
if (!isTagNameNullOrWhitespace)
|
||||
{
|
||||
destination.AppendHtml("<");
|
||||
destination.AppendHtml(TagName);
|
||||
|
||||
CopyAttributesTo(destination);
|
||||
|
||||
if (TagMode == TagMode.SelfClosing)
|
||||
{
|
||||
destination.AppendHtml(" /");
|
||||
}
|
||||
|
||||
destination.AppendHtml(">");
|
||||
}
|
||||
|
||||
if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
|
||||
{
|
||||
_preContent?.CopyTo(destination);
|
||||
|
||||
_content?.CopyTo(destination);
|
||||
|
||||
_postContent?.CopyTo(destination);
|
||||
}
|
||||
|
||||
if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
|
||||
{
|
||||
destination.AppendHtml("</");
|
||||
destination.AppendHtml(TagName);
|
||||
destination.AppendHtml(">");
|
||||
}
|
||||
|
||||
_postElement?.CopyTo(destination);
|
||||
}
|
||||
|
||||
void IHtmlContentContainer.MoveTo(IHtmlContentBuilder destination)
|
||||
{
|
||||
if (destination == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
}
|
||||
|
||||
_preElement?.MoveTo(destination);
|
||||
|
||||
var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);
|
||||
|
||||
if (!isTagNameNullOrWhitespace)
|
||||
{
|
||||
destination.AppendHtml("<");
|
||||
destination.AppendHtml(TagName);
|
||||
|
||||
CopyAttributesTo(destination);
|
||||
|
||||
if (TagMode == TagMode.SelfClosing)
|
||||
{
|
||||
destination.AppendHtml(" /");
|
||||
}
|
||||
|
||||
destination.AppendHtml(">");
|
||||
}
|
||||
|
||||
if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
|
||||
{
|
||||
_preContent?.MoveTo(destination);
|
||||
_content?.MoveTo(destination);
|
||||
_postContent?.MoveTo(destination);
|
||||
}
|
||||
|
||||
if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
|
||||
{
|
||||
destination.AppendHtml("</");
|
||||
destination.AppendHtml(TagName);
|
||||
destination.AppendHtml(">");
|
||||
}
|
||||
|
||||
_postElement?.MoveTo(destination);
|
||||
|
||||
// Depending on the code path we took, these might need to be cleared.
|
||||
_preContent?.Clear();
|
||||
_content?.Clear();
|
||||
_postContent?.Clear();
|
||||
_attributes?.Clear();
|
||||
}
|
||||
|
||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||
{
|
||||
if (writer == null)
|
||||
|
|
@ -284,6 +377,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
throw new ArgumentNullException(nameof(writer));
|
||||
}
|
||||
|
||||
if (encoder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(encoder));
|
||||
}
|
||||
|
||||
_preElement?.WriteTo(writer, encoder);
|
||||
|
||||
var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);
|
||||
|
|
@ -310,16 +408,25 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
var htmlContent = value as IHtmlContent;
|
||||
if (htmlContent != null)
|
||||
{
|
||||
// There's no way of tracking the attribute value quotations in the Razor source. Therefore, we
|
||||
// must escape any IHtmlContent double quote values in the case that a user wrote:
|
||||
// <p name='A " is valid in single quotes'></p>
|
||||
using (var stringWriter = new StringWriter())
|
||||
// Perf: static text in a bound attribute go down this path. Avoid allocating if possible (common case).
|
||||
var htmlEncodedString = value as HtmlEncodedString;
|
||||
if (htmlEncodedString != null && !htmlEncodedString.Value.Contains("\""))
|
||||
{
|
||||
htmlContent.WriteTo(stringWriter, encoder);
|
||||
stringWriter.GetStringBuilder().Replace("\"", """);
|
||||
writer.Write(htmlEncodedString.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// There's no way of tracking the attribute value quotations in the Razor source. Therefore, we
|
||||
// must escape any IHtmlContent double quote values in the case that a user wrote:
|
||||
// <p name='A " is valid in single quotes'></p>
|
||||
using (var stringWriter = new StringWriter())
|
||||
{
|
||||
htmlContent.WriteTo(stringWriter, encoder);
|
||||
stringWriter.GetStringBuilder().Replace("\"", """);
|
||||
|
||||
var stringValue = stringWriter.ToString();
|
||||
writer.Write(stringValue);
|
||||
var stringValue = stringWriter.ToString();
|
||||
writer.Write(stringValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (value != null)
|
||||
|
|
@ -356,5 +463,76 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
|
||||
_postElement?.WriteTo(writer, encoder);
|
||||
}
|
||||
|
||||
private void CopyAttributesTo(IHtmlContentBuilder destination)
|
||||
{
|
||||
StringWriter stringWriter = null;
|
||||
|
||||
// Perf: Avoid allocating enumerator
|
||||
for (var i = 0; i < (_attributes?.Count ?? 0); i++)
|
||||
{
|
||||
var attribute = _attributes[i];
|
||||
destination.AppendHtml(" ");
|
||||
destination.AppendHtml(attribute.Name);
|
||||
|
||||
if (attribute.Minimized)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
destination.AppendHtml("=\"");
|
||||
var value = attribute.Value;
|
||||
var htmlContent = value as IHtmlContent;
|
||||
if (htmlContent != null)
|
||||
{
|
||||
// Perf: static text in a bound attribute go down this path. Avoid allocating if possible (common case).
|
||||
var htmlEncodedString = value as HtmlEncodedString;
|
||||
if (htmlEncodedString != null && !htmlEncodedString.Value.Contains("\""))
|
||||
{
|
||||
destination.AppendHtml(htmlEncodedString);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Perf: We'll share this writer implementation for all attributes since
|
||||
// they can't nest.
|
||||
stringWriter = stringWriter ?? new StringWriter();
|
||||
|
||||
destination.AppendHtml(new AttributeContent(htmlContent, stringWriter));
|
||||
}
|
||||
}
|
||||
else if (value != null)
|
||||
{
|
||||
destination.Append(value.ToString());
|
||||
}
|
||||
|
||||
destination.AppendHtml("\"");
|
||||
}
|
||||
}
|
||||
|
||||
private class AttributeContent : IHtmlContent
|
||||
{
|
||||
private readonly IHtmlContent _inner;
|
||||
private readonly StringWriter _stringWriter;
|
||||
|
||||
public AttributeContent(IHtmlContent inner, StringWriter stringWriter)
|
||||
{
|
||||
_inner = inner;
|
||||
_stringWriter = stringWriter;
|
||||
}
|
||||
|
||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||
{
|
||||
// There's no way of tracking the attribute value quotations in the Razor source. Therefore, we
|
||||
// must escape any IHtmlContent double quote values in the case that a user wrote:
|
||||
// <p name='A " is valid in single quotes'></p>
|
||||
_inner.WriteTo(_stringWriter, encoder);
|
||||
_stringWriter.GetStringBuilder().Replace("\"", """);
|
||||
|
||||
var stringValue = _stringWriter.ToString();
|
||||
writer.Write(stringValue);
|
||||
|
||||
_stringWriter.GetStringBuilder().Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
// 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.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.Extensions.WebEncoders.Testing;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -83,6 +85,107 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
Assert.Equal(expected, copiedTagHelperContent.GetContent(new HtmlTestEncoder()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_CopiesAllItems()
|
||||
{
|
||||
// Arrange
|
||||
var source = new DefaultTagHelperContent();
|
||||
source.AppendHtml(new HtmlEncodedString("hello"));
|
||||
source.Append("Test");
|
||||
|
||||
var items = new List<object>();
|
||||
var destination = new HtmlContentBuilder(items);
|
||||
destination.Append("some-content");
|
||||
|
||||
// Act
|
||||
source.CopyTo(destination);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, items.Count);
|
||||
|
||||
Assert.Equal("some-content", Assert.IsType<string>(items[0]));
|
||||
Assert.Equal("hello", Assert.IsType<HtmlEncodedString>(items[1]).Value);
|
||||
Assert.Equal("Test", Assert.IsType<string>(items[2]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CopyTo_DoesDeepCopy()
|
||||
{
|
||||
// Arrange
|
||||
var source = new DefaultTagHelperContent();
|
||||
|
||||
var nested = new DefaultTagHelperContent();
|
||||
source.AppendHtml(nested);
|
||||
nested.AppendHtml(new HtmlEncodedString("hello"));
|
||||
source.Append("Test");
|
||||
|
||||
var items = new List<object>();
|
||||
var destination = new HtmlContentBuilder(items);
|
||||
destination.Append("some-content");
|
||||
|
||||
// Act
|
||||
source.CopyTo(destination);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(3, items.Count);
|
||||
|
||||
Assert.Equal("some-content", Assert.IsType<string>(items[0]));
|
||||
Assert.Equal("hello", Assert.IsType<HtmlEncodedString>(items[1]).Value);
|
||||
Assert.Equal("Test", Assert.IsType<string>(items[2]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveTo_CopiesAllItems_AndClears()
|
||||
{
|
||||
// Arrange
|
||||
var source = new DefaultTagHelperContent();
|
||||
source.AppendHtml(new HtmlEncodedString("hello"));
|
||||
source.Append("Test");
|
||||
|
||||
var items = new List<object>();
|
||||
var destination = new HtmlContentBuilder(items);
|
||||
destination.Append("some-content");
|
||||
|
||||
// Act
|
||||
source.MoveTo(destination);
|
||||
|
||||
// Assert
|
||||
Assert.True(source.IsEmpty);
|
||||
Assert.Equal(3, items.Count);
|
||||
|
||||
Assert.Equal("some-content", Assert.IsType<string>(items[0]));
|
||||
Assert.Equal("hello", Assert.IsType<HtmlEncodedString>(items[1]).Value);
|
||||
Assert.Equal("Test", Assert.IsType<string>(items[2]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoveTo_DoesDeepMove()
|
||||
{
|
||||
// Arrange
|
||||
var source = new DefaultTagHelperContent();
|
||||
|
||||
var nested = new DefaultTagHelperContent();
|
||||
source.AppendHtml(nested);
|
||||
nested.AppendHtml(new HtmlEncodedString("hello"));
|
||||
source.Append("Test");
|
||||
|
||||
var items = new List<object>();
|
||||
var destination = new HtmlContentBuilder(items);
|
||||
destination.Append("some-content");
|
||||
|
||||
// Act
|
||||
source.MoveTo(destination);
|
||||
|
||||
// Assert
|
||||
Assert.True(source.IsEmpty);
|
||||
Assert.True(nested.IsEmpty);
|
||||
Assert.Equal(3, items.Count);
|
||||
|
||||
Assert.Equal("some-content", Assert.IsType<string>(items[0]));
|
||||
Assert.Equal("hello", Assert.IsType<HtmlEncodedString>(items[1]).Value);
|
||||
Assert.Equal("Test", Assert.IsType<string>(items[2]));
|
||||
}
|
||||
|
||||
// GetContent - this one relies on the default encoder.
|
||||
[Fact]
|
||||
public void CanGetContent()
|
||||
|
|
|
|||
|
|
@ -972,16 +972,17 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
// This tests a separate code path that's used by THO when the writer is an HtmlTextWriter.
|
||||
// The output should be the same, but we do some specific perf optimizations on this path.
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteTagHelper_InputData))]
|
||||
public void WriteTo_WritesFormattedTagHelper_HtmlTextWriter(TagHelperOutput output, string expected)
|
||||
public void CopyTo_CopiesToBuilder(TagHelperOutput output, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var inner = new StringWriter();
|
||||
var attributeCount = output.Attributes.Count;
|
||||
|
||||
var writer = new StringWriter();
|
||||
var testEncoder = new HtmlTestEncoder();
|
||||
var writer = new MockHtmlTextWriter(inner, testEncoder);
|
||||
|
||||
var buffer = new HtmlContentBuilder();
|
||||
|
||||
var tagHelperExecutionContext = new TagHelperExecutionContext(
|
||||
tagName: output.TagName,
|
||||
|
|
@ -994,10 +995,49 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
tagHelperExecutionContext.Output = output;
|
||||
|
||||
// Act
|
||||
output.WriteTo(writer, testEncoder);
|
||||
((IHtmlContentContainer)output).CopyTo(buffer);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, inner.ToString(), StringComparer.Ordinal);
|
||||
buffer.WriteTo(writer, testEncoder);
|
||||
|
||||
Assert.Equal(attributeCount, output.Attributes.Count);
|
||||
Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(WriteTagHelper_InputData))]
|
||||
public void MoveTo_MovesToBuilder(TagHelperOutput output, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var writer = new StringWriter();
|
||||
var testEncoder = new HtmlTestEncoder();
|
||||
|
||||
var buffer = new HtmlContentBuilder();
|
||||
|
||||
var tagHelperExecutionContext = new TagHelperExecutionContext(
|
||||
tagName: output.TagName,
|
||||
tagMode: output.TagMode,
|
||||
items: new Dictionary<object, object>(),
|
||||
uniqueId: string.Empty,
|
||||
executeChildContentAsync: () => Task.FromResult(result: true),
|
||||
startTagHelperWritingScope: _ => { },
|
||||
endTagHelperWritingScope: () => new DefaultTagHelperContent());
|
||||
tagHelperExecutionContext.Output = output;
|
||||
|
||||
// Act
|
||||
((IHtmlContentContainer)output).MoveTo(buffer);
|
||||
|
||||
// Assert
|
||||
buffer.WriteTo(writer, testEncoder);
|
||||
|
||||
Assert.True(output.PreElement.IsEmpty);
|
||||
Assert.True(output.PreContent.IsEmpty);
|
||||
Assert.True(output.Content.IsEmpty);
|
||||
Assert.True(output.PostContent.IsEmpty);
|
||||
Assert.True(output.PostElement.IsEmpty);
|
||||
Assert.Empty(output.Attributes);
|
||||
|
||||
Assert.Equal(expected, writer.ToString(), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private static TagHelperOutput GetTagHelperOutput(
|
||||
|
|
@ -1046,29 +1086,5 @@ namespace Microsoft.AspNetCore.Razor.TagHelpers
|
|||
|
||||
return output;
|
||||
}
|
||||
|
||||
private class MockHtmlTextWriter : HtmlTextWriter
|
||||
{
|
||||
private readonly HtmlEncoder _encoder;
|
||||
private readonly TextWriter _inner;
|
||||
|
||||
public MockHtmlTextWriter(TextWriter inner, HtmlEncoder encoder)
|
||||
{
|
||||
_inner = inner;
|
||||
_encoder = encoder;
|
||||
}
|
||||
|
||||
public override Encoding Encoding => _inner.Encoding;
|
||||
|
||||
public override void Write(IHtmlContent value)
|
||||
{
|
||||
value.WriteTo(_inner, _encoder);
|
||||
}
|
||||
|
||||
public override void Write(char value)
|
||||
{
|
||||
_inner.Write(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue