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