Skip to content
This repository was archived by the owner on Dec 19, 2018. It is now read-only.

Commit e0f3c3d

Browse files
committed
Added MarkupElementRewriter
1 parent 5446779 commit e0f3c3d

20 files changed

+853
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Diagnostics;
7+
using System.Linq;
8+
9+
namespace Microsoft.AspNetCore.Razor.Language.Syntax
10+
{
11+
internal static class MarkupElementRewriter
12+
{
13+
public static RazorSyntaxTree AddMarkupElements(RazorSyntaxTree syntaxTree)
14+
{
15+
var rewriter = new AddMarkupElementRewriter();
16+
var rewrittenRoot = rewriter.Visit(syntaxTree.Root);
17+
18+
var newSyntaxTree = RazorSyntaxTree.Create(rewrittenRoot, syntaxTree.Source, syntaxTree.Diagnostics, syntaxTree.Options);
19+
return newSyntaxTree;
20+
}
21+
22+
public static RazorSyntaxTree RemoveMarkupElements(RazorSyntaxTree syntaxTree)
23+
{
24+
var rewriter = new RemoveMarkupElementRewriter();
25+
var rewrittenRoot = rewriter.Visit(syntaxTree.Root);
26+
27+
var newSyntaxTree = RazorSyntaxTree.Create(rewrittenRoot, syntaxTree.Source, syntaxTree.Diagnostics, syntaxTree.Options);
28+
return newSyntaxTree;
29+
}
30+
31+
private class AddMarkupElementRewriter : SyntaxRewriter
32+
{
33+
// From http://dev.w3.org/html5/spec/Overview.html#elements-0
34+
private static readonly HashSet<string> VoidElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
35+
{
36+
"area",
37+
"base",
38+
"br",
39+
"col",
40+
"command",
41+
"embed",
42+
"hr",
43+
"img",
44+
"input",
45+
"keygen",
46+
"link",
47+
"meta",
48+
"param",
49+
"source",
50+
"track",
51+
"wbr"
52+
};
53+
54+
private readonly Stack<TagBlockTracker> _startTagTracker = new Stack<TagBlockTracker>();
55+
56+
private TagBlockTracker CurrentTracker => _startTagTracker.Count > 0 ? _startTagTracker.Peek() : null;
57+
58+
private string CurrentStartTagName => CurrentTracker?.TagName;
59+
60+
public override SyntaxNode Visit(SyntaxNode node)
61+
{
62+
node = base.Visit(node);
63+
64+
if (node != null)
65+
{
66+
node = RewriteNode(node);
67+
}
68+
69+
return node;
70+
}
71+
72+
private SyntaxNode RewriteNode(SyntaxNode node)
73+
{
74+
if (node.IsToken)
75+
{
76+
// Tokens don't have children.
77+
return node;
78+
}
79+
80+
_startTagTracker.Clear();
81+
var children = node.ChildNodes().ToList();
82+
var rewrittenChildren = new List<SyntaxNode>(children.Count);
83+
for (var i = 0; i < children.Count; i++)
84+
{
85+
var child = children[i];
86+
if (!(child is MarkupTagBlockSyntax tagBlock))
87+
{
88+
TrackChild(child, rewrittenChildren);
89+
continue;
90+
}
91+
92+
var tagName = tagBlock.GetTagName();
93+
if (string.IsNullOrWhiteSpace(tagName) ||
94+
IsVoidElement(tagBlock) ||
95+
IsSelfClosing(tagBlock))
96+
{
97+
// Don't want to track incomplete, invalid (Eg. </>, < >), void or self-closing tags.
98+
// Simply wrap it in a block with no body or start/end tag.
99+
if (IsEndTag(tagBlock))
100+
{
101+
// This is an error case.
102+
BuildMarkupElement(rewrittenChildren, startTag: null, tagChildren: new List<RazorSyntaxNode>(), endTag: tagBlock);
103+
}
104+
else
105+
{
106+
BuildMarkupElement(rewrittenChildren, startTag: tagBlock, tagChildren: new List<RazorSyntaxNode>(), endTag: null);
107+
}
108+
}
109+
else if (IsEndTag(tagBlock))
110+
{
111+
if (string.Equals(CurrentStartTagName, tagName, StringComparison.OrdinalIgnoreCase))
112+
{
113+
var startTagTracker = _startTagTracker.Pop();
114+
var startTag = startTagTracker.TagBlock;
115+
116+
// Get the nodes between the start and the end tag.
117+
var tagChildren = startTagTracker.Children;
118+
119+
BuildMarkupElement(rewrittenChildren, startTag, tagChildren, endTag: tagBlock);
120+
}
121+
else
122+
{
123+
// Current tag scope does not match the end tag. Attempt to recover the start tag
124+
// by looking up the previous tag scopes for a matching start tag.
125+
if (!TryRecoverStartTag(rewrittenChildren, tagName, tagBlock))
126+
{
127+
// Could not recover. The end tag doesn't have a corresponding start tag. Wrap it in a block and move on.
128+
var rewritten = SyntaxFactory.MarkupElement(startTag: null, body: new SyntaxList<RazorSyntaxNode>(), endTag: tagBlock);
129+
TrackChild(rewritten, rewrittenChildren);
130+
}
131+
}
132+
}
133+
else
134+
{
135+
// This is a start tag. Keep track of it.
136+
_startTagTracker.Push(new TagBlockTracker(tagBlock));
137+
}
138+
}
139+
140+
while (_startTagTracker.Count > 0)
141+
{
142+
// We reached the end of the list and still have unmatched start tags
143+
var startTagTracker = _startTagTracker.Pop();
144+
var startTag = startTagTracker.TagBlock;
145+
var tagChildren = startTagTracker.Children;
146+
BuildMarkupElement(rewrittenChildren, startTag, tagChildren, endTag: null);
147+
}
148+
149+
// We now have finished building our list of rewritten Children.
150+
// At this point, We should have a one to one replacement for every child. The replacement can be null.
151+
Debug.Assert(children.Count == rewrittenChildren.Count);
152+
node = node.ReplaceNodes(children, (original, rewritten) =>
153+
{
154+
var originalIndex = children.IndexOf(original);
155+
if (originalIndex != -1)
156+
{
157+
// If this returns null, that node will be removed.
158+
return rewrittenChildren[originalIndex];
159+
}
160+
161+
return original;
162+
});
163+
164+
return node;
165+
}
166+
167+
private void BuildMarkupElement(List<SyntaxNode> rewrittenChildren, MarkupTagBlockSyntax startTag, List<RazorSyntaxNode> tagChildren, MarkupTagBlockSyntax endTag)
168+
{
169+
// We are trying to replace multiple nodes (including the start/end tag) with one rewritten node.
170+
// Since we need to have each child node accounted for in our rewritten list,
171+
// we'll add "null" in place of them.
172+
// The call to SyntaxNode.ReplaceNodes() later will take care removing the nodes whose replacement is null.
173+
174+
var body = tagChildren.Where(t => t != null).ToList();
175+
var rewritten = SyntaxFactory.MarkupElement(startTag, new SyntaxList<RazorSyntaxNode>(body), endTag);
176+
if (startTag != null)
177+
{
178+
// If there was a start tag, that is where we want to put our new element.
179+
TrackChild(rewritten, rewrittenChildren);
180+
}
181+
182+
foreach (var child in tagChildren)
183+
{
184+
TrackChild(null, rewrittenChildren);
185+
}
186+
if (endTag != null)
187+
{
188+
TrackChild(startTag == null ? rewritten : null, rewrittenChildren);
189+
}
190+
}
191+
192+
private void TrackChild(SyntaxNode child, List<SyntaxNode> rewrittenChildren)
193+
{
194+
if (CurrentTracker != null)
195+
{
196+
CurrentTracker.Children.Add((RazorSyntaxNode)child);
197+
return;
198+
}
199+
200+
rewrittenChildren.Add(child);
201+
}
202+
203+
private bool TryRecoverStartTag(List<SyntaxNode> rewrittenChildren, string tagName, MarkupTagBlockSyntax endTag)
204+
{
205+
var malformedTagCount = 0;
206+
foreach (var tracker in _startTagTracker)
207+
{
208+
if (tracker.TagName.Equals(tagName, StringComparison.OrdinalIgnoreCase))
209+
{
210+
break;
211+
}
212+
213+
malformedTagCount++;
214+
}
215+
216+
if (_startTagTracker.Count > malformedTagCount)
217+
{
218+
RewriteMalformedTags(rewrittenChildren, malformedTagCount);
219+
220+
// One final rewrite, this is the rewrite that completes our target tag which is not malformed.
221+
var startTagTracker = _startTagTracker.Pop();
222+
var startTag = startTagTracker.TagBlock;
223+
var tagChildren = startTagTracker.Children;
224+
225+
BuildMarkupElement(rewrittenChildren, startTag, tagChildren, endTag);
226+
227+
// We were able to recover
228+
return true;
229+
}
230+
231+
// Could not recover tag. Aka we found an end tag without a corresponding start tag.
232+
return false;
233+
}
234+
235+
private void RewriteMalformedTags(List<SyntaxNode> rewrittenChildren, int malformedTagCount)
236+
{
237+
for (var i = 0; i < malformedTagCount; i++)
238+
{
239+
var startTagTracker = _startTagTracker.Pop();
240+
var startTag = startTagTracker.TagBlock;
241+
242+
// Since this is a start tag with no end tag, treat it as a standalone tag.
243+
// This means its children now belongs to the outer tag.
244+
BuildMarkupElement(rewrittenChildren, startTag, new List<RazorSyntaxNode>(), endTag: null);
245+
246+
var tagChildren = startTagTracker.Children;
247+
foreach (var child in tagChildren)
248+
{
249+
TrackChild(child, rewrittenChildren);
250+
}
251+
}
252+
}
253+
254+
private bool IsEndTag(MarkupTagBlockSyntax tagBlock)
255+
{
256+
var childSpan = (MarkupTextLiteralSyntax)tagBlock.Children.First();
257+
258+
// We grab the token that could be forward slash
259+
var relevantToken = childSpan.LiteralTokens[childSpan.LiteralTokens.Count == 1 ? 0 : 1];
260+
261+
return relevantToken.Kind == SyntaxKind.ForwardSlash;
262+
}
263+
264+
private bool IsVoidElement(MarkupTagBlockSyntax tagBlock)
265+
{
266+
return VoidElements.Contains(tagBlock.GetTagName());
267+
}
268+
269+
private bool IsSelfClosing(MarkupTagBlockSyntax tagBlock)
270+
{
271+
var lastChild = tagBlock.ChildNodes().LastOrDefault();
272+
273+
return lastChild?.GetContent().EndsWith("/>", StringComparison.Ordinal) ?? false;
274+
}
275+
276+
private class TagBlockTracker
277+
{
278+
public TagBlockTracker(MarkupTagBlockSyntax tagBlock)
279+
{
280+
TagBlock = tagBlock;
281+
TagName = tagBlock.GetTagName();
282+
Children = new List<RazorSyntaxNode>();
283+
}
284+
285+
public MarkupTagBlockSyntax TagBlock { get; }
286+
287+
public List<RazorSyntaxNode> Children { get; }
288+
289+
public string TagName { get; }
290+
}
291+
}
292+
293+
private class RemoveMarkupElementRewriter : SyntaxRewriter
294+
{
295+
public override SyntaxNode Visit(SyntaxNode node)
296+
{
297+
if (node != null)
298+
{
299+
node = RewriteNode(node);
300+
}
301+
302+
return base.Visit(node);
303+
}
304+
305+
private SyntaxNode RewriteNode(SyntaxNode node)
306+
{
307+
if (node.IsToken)
308+
{
309+
return node;
310+
}
311+
312+
var children = node.ChildNodes();
313+
for (var i = 0; i < children.Count; i++)
314+
{
315+
var child = children[i];
316+
if (!(child is MarkupElementSyntax tagElement))
317+
{
318+
continue;
319+
}
320+
321+
node = node.ReplaceNode(tagElement, tagElement.ChildNodes());
322+
323+
// Since we rewrote 'node', it's children are different. Update our collection.
324+
children = node.ChildNodes();
325+
}
326+
327+
return node;
328+
}
329+
}
330+
}
331+
}

0 commit comments

Comments
 (0)