Port WhiteSpaceRewriter and ConditionalAttributeCollapser.
- Add an `HtmlNodeOptimizationPass` that does all of the `RazorSyntaxTreeRewriting` that legacy used to achieve outside of `TagHelper`s. - Add tests for `HtmlNodeOptimizationPass` to verify it's executing appropriate bits. #849
This commit is contained in:
parent
aa58ea6907
commit
cf7489e600
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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 Microsoft.AspNetCore.Razor.Evolution.Legacy;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
|
{
|
||||||
|
internal class HtmlNodeOptimizationPass : IRazorSyntaxTreePass
|
||||||
|
{
|
||||||
|
public RazorEngine Engine { get; set; }
|
||||||
|
|
||||||
|
public int Order => 150;
|
||||||
|
|
||||||
|
public RazorSyntaxTree Execute(RazorCodeDocument codeDocument, RazorSyntaxTree syntaxTree)
|
||||||
|
{
|
||||||
|
var conditionalAttributeCollapser = new ConditionalAttributeCollapser();
|
||||||
|
var rewritten = conditionalAttributeCollapser.Rewrite(syntaxTree.Root);
|
||||||
|
|
||||||
|
var whitespaceRewriter = new WhiteSpaceRewriter();
|
||||||
|
rewritten = whitespaceRewriter.Rewrite(rewritten);
|
||||||
|
|
||||||
|
var rewrittenSyntaxTree = RazorSyntaxTree.Create(rewritten, syntaxTree.Diagnostics);
|
||||||
|
return rewrittenSyntaxTree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
{
|
||||||
|
internal class ConditionalAttributeCollapser : MarkupRewriter
|
||||||
|
{
|
||||||
|
protected override bool CanRewrite(Block block)
|
||||||
|
{
|
||||||
|
var generator = block.ChunkGenerator as AttributeBlockChunkGenerator;
|
||||||
|
if (generator != null && block.Children.Count > 0)
|
||||||
|
{
|
||||||
|
// Perf: Avoid allocating an enumerator.
|
||||||
|
for (var i = 0; i < block.Children.Count; i++)
|
||||||
|
{
|
||||||
|
if (!IsLiteralAttributeValue(block.Children[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block)
|
||||||
|
{
|
||||||
|
// Collect the content of this node
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
for (var i = 0; i < block.Children.Count; i++)
|
||||||
|
{
|
||||||
|
var childSpan = (Span)block.Children[i];
|
||||||
|
builder.Append(childSpan.Content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new span containing this content
|
||||||
|
var span = new SpanBuilder();
|
||||||
|
|
||||||
|
span.EditHandler = SpanEditHandler.CreateDefault(HtmlLanguageCharacteristics.Instance.TokenizeString);
|
||||||
|
Debug.Assert(block.Children.Count > 0);
|
||||||
|
var start = ((Span)block.Children[0]).Start;
|
||||||
|
FillSpan(span, start, builder.ToString());
|
||||||
|
return span.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsLiteralAttributeValue(SyntaxTreeNode node)
|
||||||
|
{
|
||||||
|
if (node.IsBlock)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = node as Span;
|
||||||
|
Debug.Assert(span != null);
|
||||||
|
|
||||||
|
return span != null &&
|
||||||
|
(span.ChunkGenerator is LiteralAttributeChunkGenerator ||
|
||||||
|
span.ChunkGenerator is MarkupChunkGenerator ||
|
||||||
|
span.ChunkGenerator == SpanChunkGenerator.Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,14 +8,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
{
|
{
|
||||||
internal class LiteralAttributeChunkGenerator : SpanChunkGenerator
|
internal class LiteralAttributeChunkGenerator : SpanChunkGenerator
|
||||||
{
|
{
|
||||||
public LiteralAttributeChunkGenerator(
|
|
||||||
LocationTagged<string> prefix,
|
|
||||||
LocationTagged<SpanChunkGenerator> valueGenerator)
|
|
||||||
{
|
|
||||||
Prefix = prefix;
|
|
||||||
ValueGenerator = valueGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LiteralAttributeChunkGenerator(LocationTagged<string> prefix, LocationTagged<string> value)
|
public LiteralAttributeChunkGenerator(LocationTagged<string> prefix, LocationTagged<string> value)
|
||||||
{
|
{
|
||||||
Prefix = prefix;
|
Prefix = prefix;
|
||||||
|
|
@ -26,8 +18,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
|
||||||
public LocationTagged<string> Value { get; }
|
public LocationTagged<string> Value { get; }
|
||||||
|
|
||||||
public LocationTagged<SpanChunkGenerator> ValueGenerator { get; }
|
|
||||||
|
|
||||||
public override void Accept(ParserVisitor visitor, Span span)
|
public override void Accept(ParserVisitor visitor, Span span)
|
||||||
{
|
{
|
||||||
visitor.VisitLiteralAttributeSpan(this, span);
|
visitor.VisitLiteralAttributeSpan(this, span);
|
||||||
|
|
@ -53,14 +43,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
if (ValueGenerator == null)
|
return string.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F}", Prefix);
|
||||||
{
|
|
||||||
return string.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},{1:F}", Prefix, Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return string.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},<Sub:{1:F}>", Prefix, ValueGenerator);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
|
|
@ -68,8 +51,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
var other = obj as LiteralAttributeChunkGenerator;
|
var other = obj as LiteralAttributeChunkGenerator;
|
||||||
return other != null &&
|
return other != null &&
|
||||||
Equals(other.Prefix, Prefix) &&
|
Equals(other.Prefix, Prefix) &&
|
||||||
Equals(other.Value, Value) &&
|
Equals(other.Value, Value);
|
||||||
Equals(other.ValueGenerator, ValueGenerator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
|
|
@ -78,7 +60,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
|
||||||
hashCodeCombiner.Add(Prefix);
|
hashCodeCombiner.Add(Prefix);
|
||||||
hashCodeCombiner.Add(Value);
|
hashCodeCombiner.Add(Value);
|
||||||
hashCodeCombiner.Add(ValueGenerator);
|
|
||||||
|
|
||||||
return hashCodeCombiner;
|
return hashCodeCombiner;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
// 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.Diagnostics;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
{
|
||||||
|
internal abstract class MarkupRewriter : ParserVisitor
|
||||||
|
{
|
||||||
|
private Stack<BlockBuilder> _blocks;
|
||||||
|
|
||||||
|
protected MarkupRewriter()
|
||||||
|
{
|
||||||
|
_blocks = new Stack<BlockBuilder>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BlockBuilder Parent => _blocks.Count > 0 ? _blocks.Peek() : null;
|
||||||
|
|
||||||
|
public Block Rewrite(Block root)
|
||||||
|
{
|
||||||
|
root.Accept(this);
|
||||||
|
Debug.Assert(_blocks.Count == 1);
|
||||||
|
var rewrittenRoot = _blocks.Pop().Build();
|
||||||
|
|
||||||
|
return rewrittenRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitBlock(Block block)
|
||||||
|
{
|
||||||
|
if (CanRewrite(block))
|
||||||
|
{
|
||||||
|
var newNode = RewriteBlock(Parent, block);
|
||||||
|
if (newNode != null)
|
||||||
|
{
|
||||||
|
Parent.Children.Add(newNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Not rewritable.
|
||||||
|
var builder = new BlockBuilder(block);
|
||||||
|
builder.Children.Clear();
|
||||||
|
_blocks.Push(builder);
|
||||||
|
base.VisitBlock(block);
|
||||||
|
Debug.Assert(ReferenceEquals(builder, Parent));
|
||||||
|
|
||||||
|
if (_blocks.Count > 1)
|
||||||
|
{
|
||||||
|
_blocks.Pop();
|
||||||
|
Parent.Children.Add(builder.Build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract bool CanRewrite(Block block);
|
||||||
|
|
||||||
|
protected abstract SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block);
|
||||||
|
|
||||||
|
public override void VisitSpan(Span span)
|
||||||
|
{
|
||||||
|
Parent.Children.Add(span);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void FillSpan(SpanBuilder builder, SourceLocation start, string content)
|
||||||
|
{
|
||||||
|
builder.Kind = SpanKind.Markup;
|
||||||
|
builder.ChunkGenerator = new MarkupChunkGenerator();
|
||||||
|
|
||||||
|
foreach (ISymbol sym in HtmlLanguageCharacteristics.Instance.TokenizeString(start, content))
|
||||||
|
{
|
||||||
|
builder.Accept(sym);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -560,14 +560,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
|
||||||
if (literalGenerator != null)
|
if (literalGenerator != null)
|
||||||
{
|
{
|
||||||
if (literalGenerator.ValueGenerator == null || literalGenerator.ValueGenerator.Value == null)
|
newChunkGenerator = new MarkupChunkGenerator();
|
||||||
{
|
|
||||||
newChunkGenerator = new MarkupChunkGenerator();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newChunkGenerator = literalGenerator.ValueGenerator.Value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (isDynamic && childSpan.ChunkGenerator == SpanChunkGenerator.Null)
|
else if (isDynamic && childSpan.ChunkGenerator == SpanChunkGenerator.Null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// 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.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
{
|
||||||
|
internal class WhiteSpaceRewriter : MarkupRewriter
|
||||||
|
{
|
||||||
|
protected override bool CanRewrite(Block block)
|
||||||
|
{
|
||||||
|
return block.Type == BlockType.Expression && Parent != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block)
|
||||||
|
{
|
||||||
|
var newBlock = new BlockBuilder(block);
|
||||||
|
newBlock.Children.Clear();
|
||||||
|
var ws = block.Children.FirstOrDefault() as Span;
|
||||||
|
IEnumerable<SyntaxTreeNode> newNodes = block.Children;
|
||||||
|
if (ws.Content.All(char.IsWhiteSpace))
|
||||||
|
{
|
||||||
|
// Add this node to the parent
|
||||||
|
var builder = new SpanBuilder(ws);
|
||||||
|
builder.ClearSymbols();
|
||||||
|
FillSpan(builder, ws.Start, ws.Content);
|
||||||
|
parent.Children.Add(builder.Build());
|
||||||
|
|
||||||
|
// Remove the old whitespace node
|
||||||
|
newNodes = block.Children.Skip(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (SyntaxTreeNode node in newNodes)
|
||||||
|
{
|
||||||
|
newBlock.Children.Add(node);
|
||||||
|
}
|
||||||
|
return newBlock.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
builder.Phases.Add(new DefaultRazorIRLoweringPhase());
|
builder.Phases.Add(new DefaultRazorIRLoweringPhase());
|
||||||
|
|
||||||
builder.Features.Add(new TagHelperBinderSyntaxTreePass());
|
builder.Features.Add(new TagHelperBinderSyntaxTreePass());
|
||||||
|
builder.Features.Add(new HtmlNodeOptimizationPass());
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract IReadOnlyList<IRazorEngineFeature> Features { get; }
|
public abstract IReadOnlyList<IRazorEngineFeature> Features { get; }
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Razor.Parser.Internal;
|
|
||||||
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
|
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Parser
|
namespace Microsoft.AspNetCore.Razor.Parser
|
||||||
|
|
@ -29,21 +28,21 @@ namespace Microsoft.AspNetCore.Razor.Parser
|
||||||
{
|
{
|
||||||
var newBlock = new BlockBuilder(block);
|
var newBlock = new BlockBuilder(block);
|
||||||
newBlock.Children.Clear();
|
newBlock.Children.Clear();
|
||||||
var ws = block.Children.FirstOrDefault() as Span;
|
var whitespace = block.Children.FirstOrDefault() as Span;
|
||||||
IEnumerable<SyntaxTreeNode> newNodes = block.Children;
|
IEnumerable<SyntaxTreeNode> newNodes = block.Children;
|
||||||
if (ws.Content.All(Char.IsWhiteSpace))
|
if (whitespace.Content.All(char.IsWhiteSpace))
|
||||||
{
|
{
|
||||||
// Add this node to the parent
|
// Add this node to the parent
|
||||||
var builder = new SpanBuilder(ws);
|
var builder = new SpanBuilder(whitespace);
|
||||||
builder.ClearSymbols();
|
builder.ClearSymbols();
|
||||||
FillSpan(builder, ws.Start, ws.Content);
|
FillSpan(builder, whitespace.Start, whitespace.Content);
|
||||||
parent.Children.Add(builder.Build());
|
parent.Children.Add(builder.Build());
|
||||||
|
|
||||||
// Remove the old whitespace node
|
// Remove the old whitespace node
|
||||||
newNodes = block.Children.Skip(1);
|
newNodes = block.Children.Skip(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (SyntaxTreeNode node in newNodes)
|
foreach (var node in newNodes)
|
||||||
{
|
{
|
||||||
newBlock.Children.Add(node);
|
newBlock.Children.Add(node);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
|
{
|
||||||
|
public class HtmlNodeOptimizationPassTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Execute_CollapsesConditionalAttributes()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
var content = "<input value='hello world' />";
|
||||||
|
var sourceDocument = TestRazorSourceDocument.Create(content);
|
||||||
|
var originalTree = RazorSyntaxTree.Parse(sourceDocument);
|
||||||
|
var pass = new HtmlNodeOptimizationPass();
|
||||||
|
var codeDocument = RazorCodeDocument.Create(sourceDocument);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var outputTree = pass.Execute(codeDocument, originalTree);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var tag = Assert.Single(outputTree.Root.Children);
|
||||||
|
var tagBlock = Assert.IsType<Block>(tag);
|
||||||
|
Assert.Equal(BlockType.Tag, tagBlock.Type);
|
||||||
|
Assert.Equal(3, tagBlock.Children.Count);
|
||||||
|
Assert.IsType<Span>(tagBlock.Children[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Execute_RewritesWhitespace()
|
||||||
|
{
|
||||||
|
// Assert
|
||||||
|
var content = Environment.NewLine + " @true";
|
||||||
|
var sourceDocument = TestRazorSourceDocument.Create(content);
|
||||||
|
var originalTree = RazorSyntaxTree.Parse(sourceDocument);
|
||||||
|
var pass = new HtmlNodeOptimizationPass();
|
||||||
|
var codeDocument = RazorCodeDocument.Create(sourceDocument);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var outputTree = pass.Execute(codeDocument, originalTree);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(4, outputTree.Root.Children.Count);
|
||||||
|
var whitespace = Assert.IsType<Span>(outputTree.Root.Children[1]);
|
||||||
|
Assert.True(whitespace.Content.All(char.IsWhiteSpace));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,683 @@
|
||||||
|
// 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.Linq;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
{
|
||||||
|
public class HtmlAttributeTest : CsHtmlMarkupParserTestBase
|
||||||
|
{
|
||||||
|
public static TheoryData SymbolBoundAttributeNames
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string>
|
||||||
|
{
|
||||||
|
"[item]",
|
||||||
|
"[(item,",
|
||||||
|
"(click)",
|
||||||
|
"(^click)",
|
||||||
|
"*something",
|
||||||
|
"#local",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SymbolBoundAttributeNames))]
|
||||||
|
public void SymbolBoundAttributes_BeforeEqualWhitespace(string attributeName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var attributeNameLength = attributeName.Length;
|
||||||
|
var newlineLength = Environment.NewLine.Length;
|
||||||
|
var prefixLocation1 = new SourceLocation(
|
||||||
|
absoluteIndex: 2,
|
||||||
|
lineIndex: 0,
|
||||||
|
characterIndex: 2);
|
||||||
|
var suffixLocation1 = new SourceLocation(
|
||||||
|
absoluteIndex: 8 + newlineLength + attributeNameLength,
|
||||||
|
lineIndex: 1,
|
||||||
|
characterIndex: 5 + attributeNameLength);
|
||||||
|
var valueLocation1 = new SourceLocation(
|
||||||
|
absoluteIndex: 5 + attributeNameLength + newlineLength,
|
||||||
|
lineIndex: 1,
|
||||||
|
characterIndex: 2 + attributeNameLength);
|
||||||
|
var prefixLocation2 = SourceLocation.Advance(suffixLocation1, "'");
|
||||||
|
var suffixLocation2 = new SourceLocation(
|
||||||
|
absoluteIndex: 15 + attributeNameLength * 2 + newlineLength * 2,
|
||||||
|
lineIndex: 2,
|
||||||
|
characterIndex: 4);
|
||||||
|
var valueLocation2 = new SourceLocation(
|
||||||
|
absoluteIndex: 12 + attributeNameLength * 2 + newlineLength * 2,
|
||||||
|
lineIndex: 2,
|
||||||
|
characterIndex: 1);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ParseBlockTest(
|
||||||
|
$"<a {attributeName}{Environment.NewLine}='Foo'\t{attributeName}={Environment.NewLine}'Bar' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
attributeName,
|
||||||
|
prefix: new LocationTagged<string>(
|
||||||
|
$" {attributeName}{Environment.NewLine}='", prefixLocation1),
|
||||||
|
suffix: new LocationTagged<string>("'", suffixLocation1)),
|
||||||
|
Factory.Markup($" {attributeName}{Environment.NewLine}='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, valueLocation1),
|
||||||
|
value: new LocationTagged<string>("Foo", valueLocation1))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
attributeName,
|
||||||
|
prefix: new LocationTagged<string>(
|
||||||
|
$"\t{attributeName}={Environment.NewLine}'", prefixLocation2),
|
||||||
|
suffix: new LocationTagged<string>("'", suffixLocation2)),
|
||||||
|
Factory.Markup($"\t{attributeName}={Environment.NewLine}'").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Bar").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, valueLocation2),
|
||||||
|
value: new LocationTagged<string>("Bar", valueLocation2))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SymbolBoundAttributeNames))]
|
||||||
|
public void SymbolBoundAttributes_Whitespace(string attributeName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var attributeNameLength = attributeName.Length;
|
||||||
|
var newlineLength = Environment.NewLine.Length;
|
||||||
|
var prefixLocation1 = new SourceLocation(
|
||||||
|
absoluteIndex: 2,
|
||||||
|
lineIndex: 0,
|
||||||
|
characterIndex: 2);
|
||||||
|
var suffixLocation1 = new SourceLocation(
|
||||||
|
absoluteIndex: 10 + newlineLength + attributeNameLength,
|
||||||
|
lineIndex: 1,
|
||||||
|
characterIndex: 5 + attributeNameLength + newlineLength);
|
||||||
|
var valueLocation1 = new SourceLocation(
|
||||||
|
absoluteIndex: 7 + attributeNameLength + newlineLength,
|
||||||
|
lineIndex: 1,
|
||||||
|
characterIndex: 4 + attributeNameLength);
|
||||||
|
var prefixLocation2 = SourceLocation.Advance(suffixLocation1, "'");
|
||||||
|
var suffixLocation2 = new SourceLocation(
|
||||||
|
absoluteIndex: 17 + attributeNameLength * 2 + newlineLength * 2,
|
||||||
|
lineIndex: 2,
|
||||||
|
characterIndex: 5 + attributeNameLength);
|
||||||
|
var valueLocation2 = new SourceLocation(
|
||||||
|
absoluteIndex: 14 + attributeNameLength * 2 + newlineLength * 2,
|
||||||
|
lineIndex: 2,
|
||||||
|
characterIndex: 2 + attributeNameLength);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ParseBlockTest(
|
||||||
|
$"<a {Environment.NewLine} {attributeName}='Foo'\t{Environment.NewLine}{attributeName}='Bar' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
attributeName,
|
||||||
|
prefix: new LocationTagged<string>(
|
||||||
|
$" {Environment.NewLine} {attributeName}='", prefixLocation1),
|
||||||
|
suffix: new LocationTagged<string>("'", suffixLocation1)),
|
||||||
|
Factory.Markup($" {Environment.NewLine} {attributeName}='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, valueLocation1),
|
||||||
|
value: new LocationTagged<string>("Foo", valueLocation1))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
attributeName,
|
||||||
|
prefix: new LocationTagged<string>(
|
||||||
|
$"\t{Environment.NewLine}{attributeName}='", prefixLocation2),
|
||||||
|
suffix: new LocationTagged<string>("'", suffixLocation2)),
|
||||||
|
Factory.Markup($"\t{Environment.NewLine}{attributeName}='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Bar").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, valueLocation2),
|
||||||
|
value: new LocationTagged<string>("Bar", valueLocation2))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(SymbolBoundAttributeNames))]
|
||||||
|
public void SymbolBoundAttributes(string attributeName)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var attributeNameLength = attributeName.Length;
|
||||||
|
var suffixLocation = 8 + attributeNameLength;
|
||||||
|
var valueLocation = 5 + attributeNameLength;
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
ParseBlockTest($"<a {attributeName}='Foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
attributeName,
|
||||||
|
prefix: new LocationTagged<string>($" {attributeName}='", 2, 0, 2),
|
||||||
|
suffix: new LocationTagged<string>("'", suffixLocation, 0, suffixLocation)),
|
||||||
|
Factory.Markup($" {attributeName}='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, valueLocation, 0, valueLocation),
|
||||||
|
value: new LocationTagged<string>("Foo", valueLocation, 0, valueLocation))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SimpleLiteralAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href='Foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 12, 0, 12)),
|
||||||
|
Factory.Markup(" href='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, 9, 0, 9), value: new LocationTagged<string>("Foo", 9, 0, 9))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SimpleLiteralAttributeWithWhitespaceSurroundingEquals()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href \f\r\n= \t\n'Foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
name: "href",
|
||||||
|
prefix: new LocationTagged<string>(" href \f\r\n= \t\n'", 2, 0, 2),
|
||||||
|
suffix: new LocationTagged<string>("'", 19, 2, 4)),
|
||||||
|
Factory.Markup(" href \f\r\n= \t\n'").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, 16, 2, 1), value: new LocationTagged<string>("Foo", 16, 2, 1))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DynamicAttributeWithWhitespaceSurroundingEquals()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href \n= \r\n'@Foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
name: "href",
|
||||||
|
prefix: new LocationTagged<string>(" href \n= \r\n'", 2, 0, 2),
|
||||||
|
suffix: new LocationTagged<string>("'", 18, 2, 5)),
|
||||||
|
Factory.Markup(" href \n= \r\n'").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(string.Empty, 14, 2, 1), 14, 2, 1),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("Foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiPartLiteralAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href='Foo Bar Baz' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 20, 0, 20)),
|
||||||
|
Factory.Markup(" href='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(string.Empty, 9, 0, 9), value: new LocationTagged<string>("Foo", 9, 0, 9))),
|
||||||
|
Factory.Markup(" Bar").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(" ", 12, 0, 12), value: new LocationTagged<string>("Bar", 13, 0, 13))),
|
||||||
|
Factory.Markup(" Baz").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(" ", 16, 0, 16), value: new LocationTagged<string>("Baz", 17, 0, 17))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DoubleQuotedLiteralAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href=\"Foo Bar Baz\" />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "href", prefix: new LocationTagged<string>(" href=\"", 2, 0, 2), suffix: new LocationTagged<string>("\"", 20, 0, 20)),
|
||||||
|
Factory.Markup(" href=\"").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(string.Empty, 9, 0, 9), value: new LocationTagged<string>("Foo", 9, 0, 9))),
|
||||||
|
Factory.Markup(" Bar").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(" ", 12, 0, 12), value: new LocationTagged<string>("Bar", 13, 0, 13))),
|
||||||
|
Factory.Markup(" Baz").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(" ", 16, 0, 16), value: new LocationTagged<string>("Baz", 17, 0, 17))),
|
||||||
|
Factory.Markup("\"").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewLinePrecedingAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a\r\nhref='Foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
name: "href",
|
||||||
|
prefix: new LocationTagged<string>("\r\nhref='", 2, 0, 2),
|
||||||
|
suffix: new LocationTagged<string>("'", 13, 1, 9)),
|
||||||
|
Factory.Markup("\r\nhref='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, 10, 1, 6),
|
||||||
|
value: new LocationTagged<string>("Foo", 10, 1, 6))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewLineBetweenAttributes()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a\nhref='Foo'\r\nabcd='Bar' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(
|
||||||
|
name: "href",
|
||||||
|
prefix: new LocationTagged<string>("\nhref='", 2, 0, 2),
|
||||||
|
suffix: new LocationTagged<string>("'", 12, 1, 9)),
|
||||||
|
Factory.Markup("\nhref='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, 9, 1, 6),
|
||||||
|
value: new LocationTagged<string>("Foo", 9, 1, 6))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
name: "abcd",
|
||||||
|
prefix: new LocationTagged<string>("\r\nabcd='", 13, 1, 10),
|
||||||
|
suffix: new LocationTagged<string>("'", 24, 2, 9)),
|
||||||
|
Factory.Markup("\r\nabcd='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Bar").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, 21, 2, 6),
|
||||||
|
value: new LocationTagged<string>("Bar", 21, 2, 6))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WhitespaceAndNewLinePrecedingAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a \t\r\nhref='Foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator(
|
||||||
|
name: "href",
|
||||||
|
prefix: new LocationTagged<string>(" \t\r\nhref='", 2, 0, 2),
|
||||||
|
suffix: new LocationTagged<string>("'", 15, 1, 9)),
|
||||||
|
Factory.Markup(" \t\r\nhref='").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(
|
||||||
|
new LiteralAttributeChunkGenerator(
|
||||||
|
prefix: new LocationTagged<string>(string.Empty, 12, 1, 6),
|
||||||
|
value: new LocationTagged<string>("Foo", 12, 1, 6))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnquotedLiteralAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href=Foo Bar Baz />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "href", prefix: new LocationTagged<string>(" href=", 2, 0, 2), suffix: new LocationTagged<string>(string.Empty, 11, 0, 11)),
|
||||||
|
Factory.Markup(" href=").With(SpanChunkGenerator.Null),
|
||||||
|
Factory.Markup("Foo").With(new LiteralAttributeChunkGenerator(prefix: new LocationTagged<string>(string.Empty, 8, 0, 8), value: new LocationTagged<string>("Foo", 8, 0, 8)))),
|
||||||
|
new MarkupBlock(Factory.Markup(" Bar")),
|
||||||
|
new MarkupBlock(Factory.Markup(" Baz")),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SimpleExpressionAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href='@foo' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 13, 0, 13)),
|
||||||
|
Factory.Markup(" href='").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(string.Empty, 9, 0, 9), 9, 0, 9),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultiValueExpressionAttribute()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href='@foo bar @baz' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 22, 0, 22)),
|
||||||
|
Factory.Markup(" href='").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(string.Empty, 9, 0, 9), 9, 0, 9),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace))),
|
||||||
|
Factory.Markup(" bar").With(new LiteralAttributeChunkGenerator(new LocationTagged<string>(" ", 13, 0, 13), new LocationTagged<string>("bar", 14, 0, 14))),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(" ", 17, 0, 17), 18, 0, 18),
|
||||||
|
Factory.Markup(" ").With(SpanChunkGenerator.Null),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("baz")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VirtualPathAttributesWorkWithConditionalAttributes()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<a href='@foo ~/Foo/Bar' />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<a"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "href", prefix: new LocationTagged<string>(" href='", 2, 0, 2), suffix: new LocationTagged<string>("'", 23, 0, 23)),
|
||||||
|
Factory.Markup(" href='").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(string.Empty, 9, 0, 9), 9, 0, 9),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace))),
|
||||||
|
Factory.Markup(" ~/Foo/Bar")
|
||||||
|
.With(new LiteralAttributeChunkGenerator(
|
||||||
|
new LocationTagged<string>(" ", 13, 0, 13),
|
||||||
|
new LocationTagged<string>("~/Foo/Bar", 14, 0, 14))),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnquotedAttributeWithCodeWithSpacesInBlock()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<input value=@foo />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<input"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "value", prefix: new LocationTagged<string>(" value=", 6, 0, 6), suffix: new LocationTagged<string>(string.Empty, 17, 0, 17)),
|
||||||
|
Factory.Markup(" value=").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(string.Empty, 13, 0, 13), 13, 0, 13),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace)))),
|
||||||
|
Factory.Markup(" />").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UnquotedAttributeWithCodeWithSpacesInDocument()
|
||||||
|
{
|
||||||
|
ParseDocumentTest("<input value=@foo />",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<input"),
|
||||||
|
new MarkupBlock(new AttributeBlockChunkGenerator(name: "value", prefix: new LocationTagged<string>(" value=", 6, 0, 6), suffix: new LocationTagged<string>(string.Empty, 17, 0, 17)),
|
||||||
|
Factory.Markup(" value=").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(new DynamicAttributeBlockChunkGenerator(new LocationTagged<string>(string.Empty, 13, 0, 13), 13, 0, 13),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace)))),
|
||||||
|
Factory.Markup(" />"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalAttributeCollapserDoesNotRewriteEscapedTransitions()
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var results = ParseDocument("<span foo='@@' />");
|
||||||
|
var attributeCollapser = new ConditionalAttributeCollapser();
|
||||||
|
var rewritten = attributeCollapser.Rewrite(results.Root);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
EvaluateParseTree(rewritten,
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<span"),
|
||||||
|
new MarkupBlock(
|
||||||
|
new AttributeBlockChunkGenerator("foo", new LocationTagged<string>(" foo='", 5, 0, 5), new LocationTagged<string>("'", 13, 0, 13)),
|
||||||
|
Factory.Markup(" foo='").With(SpanChunkGenerator.Null),
|
||||||
|
new MarkupBlock(
|
||||||
|
Factory.Markup("@").With(new LiteralAttributeChunkGenerator(new LocationTagged<string>(string.Empty, 11, 0, 11), new LocationTagged<string>("@", 11, 0, 11))).Accepts(AcceptedCharacters.None),
|
||||||
|
Factory.Markup("@").With(SpanChunkGenerator.Null).Accepts(AcceptedCharacters.None)),
|
||||||
|
Factory.Markup("'").With(SpanChunkGenerator.Null)),
|
||||||
|
Factory.Markup(" />"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalAttributesDoNotCreateExtraDataForEntirelyLiteralAttribute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const string code =
|
||||||
|
@"<div class=""sidebar"">
|
||||||
|
<h1>Title</h1>
|
||||||
|
<p>
|
||||||
|
As the author, you can <a href=""/Photo/Edit/photoId"">edit</a>
|
||||||
|
or <a href=""/Photo/Remove/photoId"">remove</a> this photo.
|
||||||
|
</p>
|
||||||
|
<dl>
|
||||||
|
<dt class=""description"">Description</dt>
|
||||||
|
<dd class=""description"">
|
||||||
|
The uploader did not provide a description for this photo.
|
||||||
|
</dd>
|
||||||
|
<dt class=""uploaded-by"">Uploaded by</dt>
|
||||||
|
<dd class=""uploaded-by""><a href=""/User/View/user.UserId"">user.DisplayName</a></dd>
|
||||||
|
<dt class=""upload-date"">Upload date</dt>
|
||||||
|
<dd class=""upload-date"">photo.UploadDate</dd>
|
||||||
|
<dt class=""part-of-gallery"">Gallery</dt>
|
||||||
|
<dd><a href=""/View/gallery.Id"" title=""View gallery.Name gallery"">gallery.Name</a></dd>
|
||||||
|
<dt class=""tags"">Tags</dt>
|
||||||
|
<dd class=""tags"">
|
||||||
|
<ul class=""tags"">
|
||||||
|
<li>This photo has no tags.</li>
|
||||||
|
</ul>
|
||||||
|
<a href=""/Photo/EditTags/photoId"">edit tags</a>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a class=""download"" href=""/Photo/Full/photoId"" title=""Download: (photo.FileTitle + photo.FileExtension)"">Download full photo</a> ((photo.FileSize / 1024) KB)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class=""main"">
|
||||||
|
<img class=""large-photo"" alt=""photo.FileTitle"" src=""/Photo/Thumbnail"" />
|
||||||
|
<h2>Nobody has commented on this photo</h2>
|
||||||
|
<ol class=""comments"">
|
||||||
|
<li>
|
||||||
|
<h3 class=""comment-header"">
|
||||||
|
<a href=""/User/View/comment.UserId"" title=""View comment.DisplayName's profile"">comment.DisplayName</a> commented at comment.CommentDate:
|
||||||
|
</h3>
|
||||||
|
<p class=""comment-body"">comment.CommentText</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<form method=""post"" action="""">
|
||||||
|
<fieldset id=""addComment"">
|
||||||
|
<legend>Post new comment</legend>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<label for=""newComment"">Comment</label>
|
||||||
|
<textarea id=""newComment"" name=""newComment"" title=""Your comment"" rows=""6"" cols=""70""></textarea>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p class=""form-actions"">
|
||||||
|
<input type=""submit"" title=""Add comment"" value=""Add comment"" />
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</div>";
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var results = ParseDocument(code);
|
||||||
|
var attributeCollapser = new ConditionalAttributeCollapser();
|
||||||
|
var rewritten = attributeCollapser.Rewrite(results.Root);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(rewritten.Children.Count(), results.Root.Children.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalAttributesAreDisabledForDataAttributesInBlock()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<span data-foo='@foo'></span>",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<span"),
|
||||||
|
new MarkupBlock(
|
||||||
|
Factory.Markup(" data-foo='"),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace)),
|
||||||
|
Factory.Markup("'")),
|
||||||
|
Factory.Markup(">").Accepts(AcceptedCharacters.None)),
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("</span>").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalAttributesWithWeirdSpacingAreDisabledForDataAttributesInBlock()
|
||||||
|
{
|
||||||
|
ParseBlockTest("<span data-foo = '@foo'></span>",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<span"),
|
||||||
|
new MarkupBlock(
|
||||||
|
Factory.Markup(" data-foo = '"),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace)),
|
||||||
|
Factory.Markup("'")),
|
||||||
|
Factory.Markup(">").Accepts(AcceptedCharacters.None)),
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("</span>").Accepts(AcceptedCharacters.None))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalAttributesAreDisabledForDataAttributesInDocument()
|
||||||
|
{
|
||||||
|
ParseDocumentTest("<span data-foo='@foo'></span>",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<span"),
|
||||||
|
new MarkupBlock(
|
||||||
|
Factory.Markup(" data-foo='"),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace)),
|
||||||
|
Factory.Markup("'")),
|
||||||
|
Factory.Markup(">")),
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("</span>"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalAttributesWithWeirdSpacingAreDisabledForDataAttributesInDocument()
|
||||||
|
{
|
||||||
|
ParseDocumentTest("<span data-foo=@foo ></span>",
|
||||||
|
new MarkupBlock(
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("<span"),
|
||||||
|
new MarkupBlock(
|
||||||
|
Factory.Markup(" data-foo="),
|
||||||
|
new ExpressionBlock(
|
||||||
|
Factory.CodeTransition(),
|
||||||
|
Factory.Code("foo")
|
||||||
|
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
|
||||||
|
.Accepts(AcceptedCharacters.NonWhiteSpace))),
|
||||||
|
Factory.Markup(" >")),
|
||||||
|
new MarkupTagBlock(
|
||||||
|
Factory.Markup("</span>"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EmptyTestDocument : ITextDocument
|
||||||
|
{
|
||||||
|
public int Length
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public SourceLocation Location
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Position
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Peek()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Read()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Evolution.Legacy
|
||||||
|
{
|
||||||
|
public class WhiteSpaceRewriterTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Rewrite_Moves_Whitespace_Preceeding_ExpressionBlock_To_Parent_Block()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var factory = new SpanFactory();
|
||||||
|
var start = new MarkupBlock(
|
||||||
|
factory.Markup("test"),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.Code(" ").AsExpression(),
|
||||||
|
factory.CodeTransition(SyntaxConstants.TransitionString),
|
||||||
|
factory.Code("foo").AsExpression()),
|
||||||
|
factory.Markup("test"));
|
||||||
|
var rewriter = new WhiteSpaceRewriter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var rewritten = rewriter.Rewrite(start);
|
||||||
|
factory.Reset();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
ParserTestBase.EvaluateParseTree(
|
||||||
|
rewritten,
|
||||||
|
new MarkupBlock(
|
||||||
|
factory.Markup("test"),
|
||||||
|
factory.Markup(" "),
|
||||||
|
new ExpressionBlock(
|
||||||
|
factory.CodeTransition(SyntaxConstants.TransitionString),
|
||||||
|
factory.Code("foo").AsExpression()),
|
||||||
|
factory.Markup("test")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -76,7 +76,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution
|
||||||
{
|
{
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
features,
|
features,
|
||||||
feature => Assert.IsType<TagHelperBinderSyntaxTreePass>(feature));
|
feature => Assert.IsType<TagHelperBinderSyntaxTreePass>(feature),
|
||||||
|
feature => Assert.IsType<HtmlNodeOptimizationPass>(feature));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertDefaultPhases(IReadOnlyList<IRazorEnginePhase> phases)
|
private static void AssertDefaultPhases(IReadOnlyList<IRazorEnginePhase> phases)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue