Change TagHelpers to work in partial parsing.

- Prior to this change any query for ownership within a TagHelper would never succeed.
- TagHelpers structure can be influenced greatly by changes to the tags start/end body; therefore, only allowed modifications to TagHelper attribute values to avoid complexity and enable the more widespread scenario of a user typing C# code in an attribute value.
- Updated existing tests to reflect the new edit handlers that were added to TagHelper attributes.
- Added partial parsing tests to verify partial parses succeed/fail when expected.

#792
This commit is contained in:
N. Taylor Mullen 2016-08-12 15:34:52 -07:00
parent 82c9c40709
commit 8d7c51bd60
8 changed files with 416 additions and 97 deletions

View File

@ -154,11 +154,13 @@ namespace Microsoft.AspNetCore.Razor.Parser.SyntaxTree
} }
} }
public Span LocateOwner(TextChange change) public virtual Span LocateOwner(TextChange change) => LocateOwner(change, Children);
protected static Span LocateOwner(TextChange change, IEnumerable<SyntaxTreeNode> elements)
{ {
// Ask each child recursively // Ask each child recursively
Span owner = null; Span owner = null;
foreach (SyntaxTreeNode element in Children) foreach (var element in elements)
{ {
var span = element as Span; var span = element as Span;
if (span == null) if (span == null)

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.Chunks.Generators;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Editor;
using Microsoft.AspNetCore.Razor.Parser.Internal; using Microsoft.AspNetCore.Razor.Parser.Internal;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
@ -468,7 +469,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
spanBuilder.ChunkGenerator = new MarkupChunkGenerator(); spanBuilder.ChunkGenerator = new MarkupChunkGenerator();
} }
spanBuilder.Kind = SpanKind.Code; ConfigureNonStringAttribute(spanBuilder);
span = spanBuilder.Build(); span = spanBuilder.Build();
} }
} }
@ -628,7 +630,7 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
// SyntaxTreeNode reflects that. // SyntaxTreeNode reflects that.
if (isBoundNonStringAttribute) if (isBoundNonStringAttribute)
{ {
builder.Kind = SpanKind.Code; ConfigureNonStringAttribute(builder);
} }
return builder.Build(); return builder.Build();
@ -701,6 +703,18 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
htmlSymbol.Type == HtmlSymbolType.SingleQuote; htmlSymbol.Type == HtmlSymbolType.SingleQuote;
} }
private static void ConfigureNonStringAttribute(SpanBuilder builder)
{
builder.Kind = SpanKind.Code;
builder.EditHandler = new ImplicitExpressionEditHandler(
builder.EditHandler.Tokenizer,
CSharpCodeParser.DefaultKeywords,
acceptTrailingDot: true)
{
AcceptedCharacters = AcceptedCharacters.AnyExceptNewline
};
}
private class TryParseResult private class TryParseResult
{ {
public string AttributeName { get; set; } public string AttributeName { get; set; }

View File

@ -5,9 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Text;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers
@ -123,6 +124,45 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers
} }
} }
public override Span LocateOwner(TextChange change)
{
var oldPosition = change.OldPosition;
if (oldPosition < Start.AbsoluteIndex)
{
// Change occurs prior to the TagHelper.
return null;
}
var bodyEndLocation = SourceStartTag?.Start.AbsoluteIndex + SourceStartTag?.Length + base.Length;
if (oldPosition > bodyEndLocation)
{
// Change occurs after the TagHelpers body. End tags for TagHelpers cannot claim ownership of changes
// because any change to them impacts whether or not a tag is a TagHelper.
return null;
}
var startTagEndLocation = Start.AbsoluteIndex + SourceStartTag?.Length;
if (oldPosition < startTagEndLocation)
{
// Change occurs in the start tag.
var attributeElements = Attributes
.Select(attribute => attribute.Value)
.Where(value => value != null);
return LocateOwner(change, attributeElements);
}
if (oldPosition < bodyEndLocation)
{
// Change occurs in the body
return base.LocateOwner(change);
}
// TagHelper does not contain a Span that can claim ownership.
return null;
}
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {

View File

@ -139,7 +139,27 @@ namespace Microsoft.AspNetCore.Razor.Test.Framework
public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content) public static SpanConstructor CodeMarkup(this SpanFactory self, params string[] content)
{ {
return self.Span(SpanKind.Code, content, markup: true).With(new MarkupChunkGenerator()); return self
.Span(SpanKind.Code, content, markup: true)
.AsCodeMarkup();
}
public static SpanConstructor CSharpCodeMarkup(this SpanFactory self, string content)
{
return self.Code(content)
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.AsCodeMarkup();
}
public static SpanConstructor AsCodeMarkup(this SpanConstructor self)
{
return self
.With(new ImplicitExpressionEditHandler(
SpanConstructor.TestTokenizer,
CSharpCodeParser.DefaultKeywords,
acceptTrailingDot: true))
.With(new MarkupChunkGenerator())
.Accepts(AcceptedCharacters.AnyExceptNewline);
} }
public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content) public static SourceLocation GetLocationAndAdvance(this SourceLocationTracker self, string content)

View File

@ -1013,9 +1013,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new ExpressionBlock( new ExpressionBlock(
factory.CodeTransition(), factory.CodeTransition(),
factory factory
.Code("DateTime.Now.Year") .CSharpCodeMarkup("DateTime.Now.Year")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords) .With(new ExpressionChunkGenerator())))))
.Accepts(AcceptedCharacters.NonWhiteSpace)))))
})) }))
}, },
{ {
@ -1031,14 +1030,10 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup(" "), factory.CodeMarkup(" "),
new ExpressionBlock( new ExpressionBlock(
factory.CSharpCodeMarkup("@"),
factory factory
.CodeTransition() .CSharpCodeMarkup("DateTime.Now.Year")
.As(SpanKind.Code) .With(new ExpressionChunkGenerator())))))
.With(new MarkupChunkGenerator()),
factory
.Code("DateTime.Now.Year")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)))))
})) }))
}, },
{ {
@ -1078,14 +1073,9 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup(" "), factory.CodeMarkup(" "),
new ExpressionBlock( new ExpressionBlock(
factory factory.CSharpCodeMarkup("@"),
.CodeTransition() factory.CSharpCodeMarkup("value")
.As(SpanKind.Code) .With(new ExpressionChunkGenerator()))),
.With(new MarkupChunkGenerator()),
factory
.Code("value")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace))),
factory.CodeMarkup(" +"), factory.CodeMarkup(" +"),
factory.CodeMarkup(" 2"))), factory.CodeMarkup(" 2"))),
new TagHelperAttributeNode( new TagHelperAttributeNode(
@ -1094,33 +1084,26 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
factory.CodeMarkup("(bool)"), factory.CodeMarkup("(bool)"),
new MarkupBlock( new MarkupBlock(
new ExpressionBlock( new ExpressionBlock(
factory.CSharpCodeMarkup("@"),
factory factory
.CodeTransition() .CSharpCodeMarkup("Bag[\"val\"]")
.As(SpanKind.Code) .With(new ExpressionChunkGenerator()))),
.With(new MarkupChunkGenerator()),
factory
.Code("Bag[\"val\"]")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace))),
factory.CodeMarkup(" ?"), factory.CodeMarkup(" ?"),
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup(" @").Accepts(AcceptedCharacters.None), factory.CodeMarkup(" @")
.As(SpanKind.Code),
factory.CodeMarkup("@") factory.CodeMarkup("@")
.With(SpanChunkGenerator.Null) .As(SpanKind.Code)
.Accepts(AcceptedCharacters.None)), .With(SpanChunkGenerator.Null)),
factory.CodeMarkup("DateTime"), factory.CodeMarkup("DateTime"),
factory.CodeMarkup(" :"), factory.CodeMarkup(" :"),
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup(" "), factory.CodeMarkup(" "),
new ExpressionBlock( new ExpressionBlock(
factory.CSharpCodeMarkup("@"),
factory factory
.CodeTransition() .CSharpCodeMarkup("DateTime.Now")
.As(SpanKind.Code) .With(new ExpressionChunkGenerator())))),
.With(new MarkupChunkGenerator()),
factory
.Code("DateTime.Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)))),
HtmlAttributeValueStyle.SingleQuotes) HtmlAttributeValueStyle.SingleQuotes)
})) }))
}, },
@ -1196,27 +1179,19 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
"age", "age",
new MarkupBlock( new MarkupBlock(
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup("@"),
factory.CodeMarkup("@") factory.CodeMarkup("@")
.Accepts(AcceptedCharacters.None) .With(SpanChunkGenerator.Null)),
.With(new MarkupChunkGenerator()),
factory.CodeMarkup("@")
.With(SpanChunkGenerator.Null)
.Accepts(AcceptedCharacters.None)),
new MarkupBlock( new MarkupBlock(
factory.EmptyHtml().As(SpanKind.Code), factory.EmptyHtml()
.AsCodeMarkup()
.As(SpanKind.Code),
new ExpressionBlock( new ExpressionBlock(
factory.CodeTransition() factory.CSharpCodeMarkup("@"),
.As(SpanKind.Code) factory.CSharpCodeMarkup("("),
.With(new MarkupChunkGenerator()), factory.CSharpCodeMarkup("11+1")
factory.MetaCode("(") .With(new ExpressionChunkGenerator()),
.Accepts(AcceptedCharacters.None) factory.CSharpCodeMarkup(")"))))),
.As(SpanKind.Code)
.With(new MarkupChunkGenerator()),
factory.Code("11+1").AsExpression(),
factory.MetaCode(")")
.Accepts(AcceptedCharacters.None)
.As(SpanKind.Code)
.With(new MarkupChunkGenerator()))))),
new TagHelperAttributeNode( new TagHelperAttributeNode(
"birthday", "birthday",
factory.CodeMarkup("DateTime.Now")), factory.CodeMarkup("DateTime.Now")),
@ -2237,12 +2212,9 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup(" "), factory.CodeMarkup(" "),
new ExpressionBlock( new ExpressionBlock(
factory.CodeTransition() factory.CSharpCodeMarkup("@"),
.As(SpanKind.Code) factory.CSharpCodeMarkup("true")
.With(new MarkupChunkGenerator()), .With(new ExpressionChunkGenerator()))),
factory.Code("true")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace))),
factory.CodeMarkup(" ")), factory.CodeMarkup(" ")),
HtmlAttributeValueStyle.SingleQuotes) HtmlAttributeValueStyle.SingleQuotes)
} }
@ -2264,18 +2236,11 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup(" "), factory.CodeMarkup(" "),
new ExpressionBlock( new ExpressionBlock(
factory.CodeTransition() factory.CSharpCodeMarkup("@"),
.As(SpanKind.Code) factory.CSharpCodeMarkup("("),
.With(new MarkupChunkGenerator()), factory.CSharpCodeMarkup("true")
factory.MetaCode("(") .With(new ExpressionChunkGenerator()),
.Accepts(AcceptedCharacters.None) factory.CSharpCodeMarkup(")"))),
.As(SpanKind.Code)
.With(new MarkupChunkGenerator()),
factory.Code("true").AsExpression(),
factory.MetaCode(")")
.Accepts(AcceptedCharacters.None)
.As(SpanKind.Code)
.With(new MarkupChunkGenerator()))),
factory.CodeMarkup(" ")), factory.CodeMarkup(" ")),
HtmlAttributeValueStyle.SingleQuotes) HtmlAttributeValueStyle.SingleQuotes)
} }

View File

@ -7,6 +7,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Razor.Chunks.Generators; using Microsoft.AspNetCore.Razor.Chunks.Generators;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers; using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Editor;
using Microsoft.AspNetCore.Razor.Parser.Internal; using Microsoft.AspNetCore.Razor.Parser.Internal;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
@ -2452,8 +2453,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new ExpressionBlock( new ExpressionBlock(
factory.CodeTransition(), factory.CodeTransition(),
factory.Code("DateTime.Now") factory.Code("DateTime.Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords) .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharacters.NonWhiteSpace))))) .Accepts(AcceptedCharacters.AnyExceptNewline)))))
} }
})), })),
availableDescriptorsColon availableDescriptorsColon
@ -2474,8 +2475,8 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
new ExpressionBlock( new ExpressionBlock(
factory.CodeTransition(), factory.CodeTransition(),
factory.Code("DateTime.Now") factory.Code("DateTime.Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords) .AsImplicitExpression(CSharpCodeParser.DefaultKeywords, acceptTrailingDot: true)
.Accepts(AcceptedCharacters.NonWhiteSpace))))) .Accepts(AcceptedCharacters.AnyExceptNewline)))))
} }
})), })),
availableDescriptorsText availableDescriptorsText
@ -2493,24 +2494,19 @@ namespace Microsoft.AspNetCore.Razor.Parser.TagHelpers.Internal
"bound", "bound",
new MarkupBlock( new MarkupBlock(
new MarkupBlock( new MarkupBlock(
factory.CodeMarkup("@"),
factory factory
.CodeMarkup("@") .CodeMarkup("@")
.With(new MarkupChunkGenerator()) .With(SpanChunkGenerator.Null)),
.Accepts(AcceptedCharacters.None),
factory
.CodeMarkup("@")
.With(SpanChunkGenerator.Null)
.Accepts(AcceptedCharacters.None)),
new MarkupBlock( new MarkupBlock(
factory.EmptyHtml().As(SpanKind.Code), factory
.EmptyHtml()
.As(SpanKind.Code)
.AsCodeMarkup(),
new ExpressionBlock( new ExpressionBlock(
factory factory.CSharpCodeMarkup("@"),
.CodeTransition() factory.CSharpCodeMarkup("DateTime.Now")
.As(SpanKind.Code) .With(new ExpressionChunkGenerator())))))
.With(new MarkupChunkGenerator()),
factory.Code("DateTime.Now")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)))))
} }
})), })),
availableDescriptorsText availableDescriptorsText

View File

@ -5,6 +5,7 @@ using System;
using System.Threading; using System.Threading;
using System.Web.WebPages.TestUtils; using System.Web.WebPages.TestUtils;
using Microsoft.AspNetCore.Razor.CodeGenerators; using Microsoft.AspNetCore.Razor.CodeGenerators;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.Test.Framework; using Microsoft.AspNetCore.Razor.Test.Framework;
using Microsoft.AspNetCore.Razor.Test.Utils; using Microsoft.AspNetCore.Razor.Test.Utils;
@ -58,7 +59,7 @@ namespace Microsoft.AspNetCore.Razor
return new TestParserManager(parser); return new TestParserManager(parser);
} }
protected static RazorEngineHost CreateHost() protected static RazorEngineHost CreateHost(ITagHelperDescriptorResolver descriptorResolver = null)
{ {
return new RazorEngineHost(new TLanguage()) return new RazorEngineHost(new TLanguage())
{ {
@ -71,7 +72,8 @@ namespace Microsoft.AspNetCore.Razor
"Template", "Template",
"DefineSection", "DefineSection",
new GeneratedTagHelperContext()), new GeneratedTagHelperContext()),
DesignTimeMode = true DesignTimeMode = true,
TagHelperDescriptorResolver = descriptorResolver
}; };
} }

View File

@ -2,17 +2,21 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Web.WebPages.TestUtils; using System.Web.WebPages.TestUtils;
using Microsoft.AspNetCore.Razor.Compilation.TagHelpers;
using Microsoft.AspNetCore.Razor.Editor; using Microsoft.AspNetCore.Razor.Editor;
using Microsoft.AspNetCore.Razor.Parser; using Microsoft.AspNetCore.Razor.Parser;
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree; using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
using Microsoft.AspNetCore.Razor.Parser.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.Test.CodeGenerators; using Microsoft.AspNetCore.Razor.Test.CodeGenerators;
using Microsoft.AspNetCore.Razor.Test.Framework; using Microsoft.AspNetCore.Razor.Test.Framework;
using Microsoft.AspNetCore.Razor.Test.Utils; using Microsoft.AspNetCore.Razor.Test.Utils;
using Microsoft.AspNetCore.Razor.Text; using Microsoft.AspNetCore.Razor.Text;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Razor namespace Microsoft.AspNetCore.Razor
@ -23,6 +27,273 @@ namespace Microsoft.AspNetCore.Razor
private static readonly TestFile SimpleCSHTMLDocumentGenerated = TestFile.Create("TestFiles/DesignTime/Simple.txt"); private static readonly TestFile SimpleCSHTMLDocumentGenerated = TestFile.Create("TestFiles/DesignTime/Simple.txt");
private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml"; private const string TestLinePragmaFileName = "C:\\This\\Path\\Is\\Just\\For\\Line\\Pragmas.cshtml";
public static TheoryData TagHelperPartialParseRejectData
{
get
{
var factory = SpanFactory.CreateCsHtml();
// change, expectedDocument
return new TheoryData<TextChange, MarkupBlock>
{
{
CreateInsertionChange("<p></p>", 2, " "),
new MarkupBlock(
new MarkupTagHelperBlock("p"))
},
{
CreateInsertionChange("<p></p>", 6, " "),
new MarkupBlock(
new MarkupTagHelperBlock("p"))
},
{
CreateInsertionChange("<p some-attr></p>", 12, " "),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"some-attr",
value: null,
valueStyle: HtmlAttributeValueStyle.Minimized)
}))
},
{
CreateInsertionChange("<p some-attr></p>", 12, "ibute"),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"some-attribute",
value: null,
valueStyle: HtmlAttributeValueStyle.Minimized)
}))
},
{
CreateInsertionChange("<p some-attr></p>", 2, " before"),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"before",
value: null,
valueStyle: HtmlAttributeValueStyle.Minimized),
new TagHelperAttributeNode(
"some-attr",
value: null,
valueStyle: HtmlAttributeValueStyle.Minimized)
}))
},
};
}
}
[Theory]
[MemberData(nameof(TagHelperPartialParseRejectData))]
public void TagHelperTagBodiesRejectPartialChanges(TextChange change, MarkupBlock expectedDocument)
{
// Arrange
var descriptors = new[]
{
new TagHelperDescriptor
{
TagName = "p",
TypeName = "PTagHelper"
},
};
var descriptorResolver = new Mock<ITagHelperDescriptorResolver>();
descriptorResolver
.Setup(resolver => resolver.Resolve(It.IsAny<TagHelperDescriptorResolutionContext>()))
.Returns(descriptors);
var host = CreateHost(descriptorResolver.Object);
var parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path");
using (var manager = new TestParserManager(parser))
{
manager.InitializeWithDocument(change.OldBuffer);
// Act
var result = manager.CheckForStructureChangesAndWait(change);
// Assert
Assert.Equal(PartialParseResult.Rejected, result);
Assert.Equal(2, manager.ParseCount);
ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, expectedDocument);
}
}
public static TheoryData TagHelperAttributeAcceptData
{
get
{
var factory = SpanFactory.CreateCsHtml();
// change, expectedDocument, partialParseResult
return new TheoryData<TextChange, MarkupBlock, PartialParseResult>
{
{
CreateInsertionChange("<p str-attr='@DateTime'></p>", 22, "."),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"str-attr",
new MarkupBlock(
new MarkupBlock(
new ExpressionBlock(
factory.CodeTransition(),
factory
.Code("DateTime.")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)))),
HtmlAttributeValueStyle.SingleQuotes)
})),
PartialParseResult.Accepted | PartialParseResult.Provisional
},
{
CreateInsertionChange("<p obj-attr='DateTime'></p>", 21, "."),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"obj-attr",
factory.CodeMarkup("DateTime."),
HtmlAttributeValueStyle.SingleQuotes)
})),
PartialParseResult.Accepted
},
{
CreateInsertionChange("<p obj-attr='1 + DateTime'></p>", 25, "."),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"obj-attr",
factory.CodeMarkup("1 + DateTime."),
HtmlAttributeValueStyle.SingleQuotes)
})),
PartialParseResult.Accepted
},
{
CreateInsertionChange("<p before-attr str-attr='@DateTime' after-attr></p>", 34, "."),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"before-attr",
value: null,
valueStyle: HtmlAttributeValueStyle.Minimized),
new TagHelperAttributeNode(
"str-attr",
new MarkupBlock(
new MarkupBlock(
new ExpressionBlock(
factory.CodeTransition(),
factory
.Code("DateTime.")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace)))),
HtmlAttributeValueStyle.SingleQuotes),
new TagHelperAttributeNode(
"after-attr",
value: null,
valueStyle: HtmlAttributeValueStyle.Minimized),
})),
PartialParseResult.Accepted | PartialParseResult.Provisional
},
{
CreateInsertionChange("<p str-attr='before @DateTime after'></p>", 29, "."),
new MarkupBlock(
new MarkupTagHelperBlock(
"p",
attributes: new List<TagHelperAttributeNode>
{
new TagHelperAttributeNode(
"str-attr",
new MarkupBlock(
factory.Markup("before"),
new MarkupBlock(
factory.Markup(" "),
new ExpressionBlock(
factory.CodeTransition(),
factory
.Code("DateTime.")
.AsImplicitExpression(CSharpCodeParser.DefaultKeywords)
.Accepts(AcceptedCharacters.NonWhiteSpace))),
factory.Markup(" after")),
HtmlAttributeValueStyle.SingleQuotes)
})),
PartialParseResult.Accepted | PartialParseResult.Provisional
},
};
}
}
[Theory]
[MemberData(nameof(TagHelperAttributeAcceptData))]
public void TagHelperAttributesAreLocatedAndAcceptChangesCorrectly(
TextChange change,
MarkupBlock expectedDocument,
PartialParseResult partialParseResult)
{
// Arrange
var descriptors = new[]
{
new TagHelperDescriptor
{
TagName = "p",
TypeName = "PTagHelper",
Attributes = new[]
{
new TagHelperAttributeDescriptor
{
Name = "obj-attr",
TypeName = typeof(object).FullName,
PropertyName = "ObjectAttribute",
},
new TagHelperAttributeDescriptor
{
Name = "str-attr",
TypeName = typeof(string).FullName,
PropertyName = "StringAttribute",
},
}
},
};
var descriptorResolver = new Mock<ITagHelperDescriptorResolver>();
descriptorResolver
.Setup(resolver => resolver.Resolve(It.IsAny<TagHelperDescriptorResolutionContext>()))
.Returns(descriptors);
var host = CreateHost(descriptorResolver.Object);
var parser = new RazorEditorParser(host, @"C:\This\Is\A\Test\Path");
using (var manager = new TestParserManager(parser))
{
manager.InitializeWithDocument(change.OldBuffer);
// Act
var result = manager.CheckForStructureChangesAndWait(change);
// Assert
Assert.Equal(partialParseResult, result);
Assert.Equal(1, manager.ParseCount);
ParserTestBase.EvaluateParseTree(manager.Parser.CurrentParseTree, expectedDocument);
}
}
[Fact] [Fact]
public void ConstructorRequiresNonNullPhysicalPath() public void ConstructorRequiresNonNullPhysicalPath()
{ {
@ -898,5 +1169,14 @@ namespace Microsoft.AspNetCore.Razor
{ {
return new CodeGenTestHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = true }; return new CodeGenTestHost(new CSharpRazorCodeLanguage()) { DesignTimeMode = true };
} }
private static TextChange CreateInsertionChange(string initialText, int insertionLocation, string insertionText)
{
var changedText = initialText.Insert(insertionLocation, insertionText);
var original = new StringTextBuffer(initialText);
var changed = new StringTextBuffer(changedText);
return new TextChange(insertionLocation, 0, original, insertionText.Length, changed);
}
} }
} }