Added MarkupElementRewriter#2624
Conversation
| { | ||
| internal class MarkupElementRewriter | ||
| { | ||
| public static RazorSyntaxTree Rewrite(RazorSyntaxTree syntaxTree) |
There was a problem hiding this comment.
This is called before tag helper phase.
There was a problem hiding this comment.
Names are wacky here because there are two rewritiers. Suggest something like AddElementNodes RemoveElementNodes
| return newSyntaxTree; | ||
| } | ||
|
|
||
| public static RazorSyntaxTree RemoveMarkupElement(RazorSyntaxTree syntaxTree) |
There was a problem hiding this comment.
This is called after tag helper phase.
| BaselineTest(rewritten); | ||
|
|
||
| var unrewritten = MarkupElementRewriter.RemoveMarkupElement(rewritten); | ||
| Assert.Equal(syntaxTree.Root.SerializedValue, unrewritten.Root.SerializedValue); |
There was a problem hiding this comment.
Each test will also run the removal phase and make sure the output matches the original syntax tree. It didn't make sense to add separate tests for the removal phase.
| continue; | ||
| } | ||
|
|
||
| var tagName = tagBlock.GetTagName(); |
There was a problem hiding this comment.
GetTagName() is an extension method to MarkupTagBlockSyntax. This will change in the future when tag block have a first class TagName child.
9d014f6 to
4ac4e54
Compare
|
|
||
| namespace Microsoft.AspNetCore.Razor.Language.Syntax | ||
| { | ||
| internal class MarkupElementRewriter |
|
|
||
| private bool IsVoidElement(MarkupTagBlockSyntax tagBlock) | ||
| { | ||
| return VoidElements.Contains(tagBlock.GetTagName(), StringComparer.OrdinalIgnoreCase); |
There was a problem hiding this comment.
This is doing Enumerable.Contains. Did you mean to do VoidElements.Contains(tagBlock.GetTagName()) since the hashset is already using an OrdinalIgnoreCase comparer?
| return VoidElements.Contains(tagBlock.GetTagName(), StringComparer.OrdinalIgnoreCase); | ||
| } | ||
|
|
||
| private bool IsSelfClosing(MarkupTagBlockSyntax tagBlock) |
There was a problem hiding this comment.
I'm guessing this is is a end tag like </> but don't see a corresponding test. Should this be unit tested?
There was a problem hiding this comment.
There is a corresponding test. https://github.com/aspnet/Razor/pull/2624/files#diff-8dfd8aeca4f15b18bb45a38795fd1c02R94
There was a problem hiding this comment.
Wouldn't either of those have one or more child nodes?
There was a problem hiding this comment.
They can't have any child nodes.
| { | ||
| var lastChild = tagBlock.ChildNodes().LastOrDefault(); | ||
|
|
||
| return lastChild?.GetContent().EndsWith("/>", StringComparison.Ordinal) ?? false; |
There was a problem hiding this comment.
Out of curiosity, why does IsEndTag use literal tokens and this use GetContent()?
There was a problem hiding this comment.
IsEndTag was a copy-paste-modify. I was wondering the same thing as why is it doing all the weird index stuff. But I left it as is for now.
| return false; | ||
| } | ||
|
|
||
| private SyntaxNode RewriteMalformedTags(SyntaxNode parent, int malformedTagCount) |
There was a problem hiding this comment.
This method is invoked when malformedTagCount != _startTagTracker.Count, but reading the code it looks like _startTagTracker.Count must be > malformedTagCount (since it's popped that many times). Does the if need to be changed?
There was a problem hiding this comment.
Sure. It doesn't functionally change anything but I'll change the if.
| node = RewriteNodeCore(node, startTag: tagBlock, tagChildren: Enumerable.Empty<RazorSyntaxNode>(), endTag: null); | ||
|
|
||
| // Since we rewrote 'node', it's children are different. Update our collection. | ||
| children = node.ChildNodes(); |
There was a problem hiding this comment.
How does this work? Should this restart from the beginning since the children are different or continue from the next index because this node is already observed?
There was a problem hiding this comment.
It won't restart. It just continues from where it left off. I still have to update the collection because rewriting node changes the children.
There was a problem hiding this comment.
What if instead you created a new list of children and then rewrote it once.
So like:
A
B
C
D
Iterate through the children of A - (B, C, D) and rewrite just those nodes as needed so you end up with (B, C!, D) (assume C was rewritten). Then rewrite A once to replace its children. That's how I would expect something like this to work.
There was a problem hiding this comment.
The problem is that when I rewrite (B, C, D) => (B, C!, D), it actually becomes (B, C!, D!) because everything after a rewritten node is different because red nodes are created on demand
There was a problem hiding this comment.
Can you explain this in more detail? I'm not sure that I get it.
There was a problem hiding this comment.
Sorry I missed this part "created a new list of children". What I said doesn't apply in that case. I'll give this a shot
|
|
||
| public Rewriter() | ||
| { | ||
| _startTagTracker = new Stack<TagBlockTracker>(); |
| MarkupTagBlockSyntax endTag) | ||
| { | ||
| var body = new SyntaxList<RazorSyntaxNode>(tagChildren); | ||
| var markupElement = SyntaxFactory.MarkupElement((MarkupTagBlockSyntax)startTag, body, endTag); |
There was a problem hiding this comment.
Can this be initialized inline (in the lambda)?
| } | ||
|
|
||
| // Replace nodes | ||
| var rewrittenElement = parent.ReplaceNodes(originalNodes, (original, rewritten) => { |
There was a problem hiding this comment.
Formatting. Also local functions for 💯 points
|
|
||
| var currentIndex = 0; | ||
| var children = node.ChildNodes(); | ||
| while (currentIndex < children.Count) |
There was a problem hiding this comment.
for (var i= 0; i < children.Count; i++) works really well here.
| return newSyntaxTree; | ||
| } | ||
|
|
||
| private class Rewriter : SyntaxRewriter |
There was a problem hiding this comment.
Is there precendent for any of this? Like is this based on something or did you write it fresh?
There was a problem hiding this comment.
There was a problem hiding this comment.
Is there precendent for any of this? Like is this based on something or did you write it fresh?
This is mostly fresh. Some of the malformed tag handling logic is very similar to https://github.com/aspnet/Razor/blob/master/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs#L730
| if (node != null) | ||
| { | ||
| node = RewriteNode(node); | ||
| } |
There was a problem hiding this comment.
Yes. Visit() is called on every child and Visit() calls node.Accept(this) if the node is not null.
| node = RewriteNode(node); | ||
| } | ||
|
|
||
| return base.Visit(node); |
There was a problem hiding this comment.
Does it make sense for this to be a preorder traversal instead? https://github.com/aspnet/Blazor/blob/master/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/ComponentDocumentRewritePass.cs#L55
There was a problem hiding this comment.
I'll take a look
There was a problem hiding this comment.
That works as well
| { | ||
| // Nothing to replace | ||
| return parent; | ||
| } |
There was a problem hiding this comment.
Does this preserve order correctly? What about a case like:
<div>Hi</div>
@Something!
<div>bye</div>
There was a problem hiding this comment.
I think this would make a little more sense if you mutated originalNodes in place and preserved ordering while doing so.
| return true; | ||
| } | ||
|
|
||
| // Could not recover tag. Aka we found an end tag without a corresponding start tag. |
There was a problem hiding this comment.
Make sure you have a test for recovery when the cause is a malformed void element. We had a bug for this recently in Blazor.
<input ....></input>
5df1ece to
f1492ef
Compare
|
🆙 📅 |
| TrackChild(rewritten, rewrittenChildren); | ||
| } | ||
|
|
||
| tagChildren.ForEach(c => TrackChild(null, rewrittenChildren)); |
There was a problem hiding this comment.
Can we not use lambdas with side effects? Just write the foreach, it's more readable
There was a problem hiding this comment.
It'd also let you pass in Array.Empty<RazorSyntaxNode>() instead of new List<>
NTaylorMullen
left a comment
There was a problem hiding this comment.
Overall things are good. The only beef I have with the PR is that the two core methods BuildMarkupElement and TrackChild are supppper confusing to follow.
| { | ||
| internal static class MarkupElementRewriter | ||
| { | ||
| public static RazorSyntaxTree AddMarkupElement(RazorSyntaxTree syntaxTree) |
There was a problem hiding this comment.
Should be plural, AddMarkupElements Ditto with the remove
| { | ||
| // Don't want to track incomplete, void or self-closing tags. | ||
| // Simply wrap it in a block with no body or start/end tag. | ||
| if (IsEndTag(tagBlock)) |
There was a problem hiding this comment.
lol this is super odd. This case handles:
</ ></foo />
Should update the comment.
| return node; | ||
| } | ||
|
|
||
| private void BuildMarkupElement(List<SyntaxNode> rewrittenChildren, MarkupTagBlockSyntax startTag, List<RazorSyntaxNode> tagChildren, MarkupTagBlockSyntax endTag) |
There was a problem hiding this comment.
IReadOnlyList<RazorSyntaxNode> tagChildren, don't want to give the perception it's edited.
| // The call to SyntaxNode.ReplaceNodes() later will take care removing the nodes whose replacement is null. | ||
|
|
||
| var body = tagChildren.Where(t => t != null).ToList(); | ||
| var rewritten = SyntaxFactory.MarkupElement(startTag, new SyntaxList<RazorSyntaxNode>(body), endTag: endTag); |
| return node; | ||
| } | ||
|
|
||
| _startTagTracker.Clear(); |
There was a problem hiding this comment.
In practice is this ever not already clear?
There was a problem hiding this comment.
It should be. Just being extra defensive.
| } | ||
| } | ||
|
|
||
| private void TrackChild(SyntaxNode child, List<SyntaxNode> rewrittenChildren) |
There was a problem hiding this comment.
Added this to https://github.com/aspnet/Razor/issues/2619.
a7846a1 to
e7706ab
Compare
|
I've addressed most of the feedback here. I've added the one I didn't address to this list https://github.com/aspnet/Razor/issues/2619 to be addressed later. Merging this now. |
e7706ab to
e0f3c3d
Compare
#2584
This is going to be run before and after the tag helper phase. I am currently not adding any errors at this level. All errors will be added during the tag helper phase. Sending this as a separate PR just to make sure everyone is on the same page.
Added tests