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

Commit 6efc7df

Browse files
author
N. Taylor Mullen
committed
Add support for minimized attributes in TagHelpers.
- Updated the Razor parser to understand minimized attributes instead of just treating them like plain text. This just involved encompassing minimized attributes in their own blocks just like the other attributes found on the HTML tag. - Updated TagHelperParseTreeRewriter to only accept minimized attributes for unbound attributes. - Updated IReadOnlyTagHelperAttribute/TagHelperAttribute to have a Minimized property to indicate that an attribute was minimized. - Updated parser level block structures to represent minimized attributes as null syntax tree nodes. - Updated chunk level structures to represent minimized attributes as null chunks. #220
1 parent 42552c9 commit 6efc7df

File tree

9 files changed

+132
-57
lines changed

9 files changed

+132
-57
lines changed

src/Microsoft.AspNet.Razor.Runtime/TagHelpers/IReadOnlyTagHelperAttribute.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,11 @@ public interface IReadOnlyTagHelperAttribute : IEquatable<IReadOnlyTagHelperAttr
1919
/// Gets the value of the attribute.
2020
/// </summary>
2121
object Value { get; }
22+
23+
/// <summary>
24+
/// Indicates whether the attribute is minimized or not.
25+
/// </summary>
26+
/// <remarks>If <c>true</c>, <see cref="Value"/> will be ignored.</remarks>
27+
bool Minimized { get; }
2228
}
2329
}

src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperAttribute.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ public TagHelperAttribute(string name, object value)
4141
/// </summary>
4242
public object Value { get; set; }
4343

44+
/// <summary>
45+
/// Gets or sets the value indicating if the attribute is minimized.
46+
/// </summary>
47+
/// <remarks>If <c>true</c>, <see cref="Value"/> will be ignored.</remarks>
48+
public bool Minimized { get; set; }
49+
50+
4451
/// <summary>
4552
/// Converts the specified <paramref name="value"/> into a <see cref="TagHelperAttribute"/>.
4653
/// </summary>
@@ -61,7 +68,8 @@ public bool Equals(IReadOnlyTagHelperAttribute other)
6168
return
6269
other != null &&
6370
string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) &&
64-
Equals(Value, other.Value);
71+
Equals(Value, other.Value) &&
72+
Minimized == other.Minimized;
6573
}
6674

6775
/// <inheritdoc />

src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperExecutionContext.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal TagHelperExecutionContext(string tagName, bool selfClosing)
3939
/// <param name="tagName">The HTML tag name in the Razor source.</param>
4040
/// <param name="selfClosing">
4141
/// <see cref="bool"/> indicating whether or not the tag in the Razor source was self-closing.</param>
42-
/// <param name="items">The collection of items used to communicate with other
42+
/// <param name="items">The collection of items used to communicate with other
4343
/// <see cref="ITagHelper"/>s</param>
4444
/// <param name="uniqueId">An identifier unique to the HTML element this context is for.</param>
4545
/// <param name="executeChildContentAsync">A delegate used to execute the child content asynchronously.</param>
@@ -133,6 +133,26 @@ public void Add([NotNull] ITagHelper tagHelper)
133133
_tagHelpers.Add(tagHelper);
134134
}
135135

136+
/// <summary>
137+
/// Tracks the minimized HTML attribute in <see cref="AllAttributes"/> and <see cref="HTMLAttributes"/>.
138+
/// </summary>
139+
/// <param name="name">The minimized HTML attribute name.</param>
140+
public void AddHtmlAttribute([NotNull] string name)
141+
{
142+
HTMLAttributes.Add(
143+
new TagHelperAttribute
144+
{
145+
Name = name,
146+
Minimized = true
147+
});
148+
AllAttributes.Add(
149+
new TagHelperAttribute
150+
{
151+
Name = name,
152+
Minimized = true
153+
});
154+
}
155+
136156
/// <summary>
137157
/// Tracks the HTML attribute in <see cref="AllAttributes"/> and <see cref="HTMLAttributes"/>.
138158
/// </summary>
@@ -168,7 +188,7 @@ public Task ExecuteChildContentAsync()
168188
/// </summary>
169189
/// <returns>A <see cref="Task"/> that on completion returns the rendered child content.</returns>
170190
/// <remarks>
171-
/// Child content is only executed once. Successive calls to this method or successive executions of the
191+
/// Child content is only executed once. Successive calls to this method or successive executions of the
172192
/// returned <see cref="Task{TagHelperContent}"/> return a cached result.
173193
/// </remarks>
174194
public async Task<TagHelperContent> GetChildContentAsync()

src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/CSharpTagHelperCodeRenderer.cs

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -324,15 +324,21 @@ private void RenderUnboundHTMLAttributes(IEnumerable<KeyValuePair<string, Chunk>
324324
// Build out the unbound HTML attributes for the tag builder
325325
foreach (var htmlAttribute in unboundHTMLAttributes)
326326
{
327-
string textValue;
327+
string textValue = null;
328+
var isPlainTextValue = false;
328329
var attributeValue = htmlAttribute.Value;
329-
var isPlainTextValue = TryGetPlainTextValue(attributeValue, out textValue);
330330

331-
// HTML attributes are always strings. So if this value is not plain text i.e. if the value contains
332-
// C# code, then we need to buffer it.
333-
if (!isPlainTextValue)
331+
// A null attribute value means the HTML attribute is minimized.
332+
if (attributeValue != null)
334333
{
335-
BuildBufferedWritingScope(attributeValue, htmlEncodeValues: true);
334+
isPlainTextValue = TryGetPlainTextValue(attributeValue, out textValue);
335+
336+
// HTML attributes are always strings. So if this value is not plain text i.e. if the value contains
337+
// C# code, then we need to buffer it.
338+
if (!isPlainTextValue)
339+
{
340+
BuildBufferedWritingScope(attributeValue, htmlEncodeValues: true);
341+
}
336342
}
337343

338344
// Execution contexts are a runtime feature, therefore no need to add anything to them.
@@ -345,23 +351,28 @@ private void RenderUnboundHTMLAttributes(IEnumerable<KeyValuePair<string, Chunk>
345351
.WriteStartInstanceMethodInvocation(
346352
ExecutionContextVariableName,
347353
_tagHelperContext.ExecutionContextAddHtmlAttributeMethodName)
348-
.WriteStringLiteral(htmlAttribute.Key)
349-
.WriteParameterSeparator()
350-
.WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName);
354+
.WriteStringLiteral(htmlAttribute.Key);
351355

352-
// If it's a plain text value then we need to surround the value with quotes.
353-
if (isPlainTextValue)
356+
// If we have a minimized attribute we don't want to provide a value
357+
if (attributeValue != null)
354358
{
355-
_writer.WriteStringLiteral(textValue);
356-
}
357-
else
358-
{
359-
RenderBufferedAttributeValueAccessor(_writer);
359+
_writer.WriteParameterSeparator()
360+
.WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName);
361+
362+
// If it's a plain text value then we need to surround the value with quotes.
363+
if (isPlainTextValue)
364+
{
365+
_writer.WriteStringLiteral(textValue);
366+
}
367+
else
368+
{
369+
RenderBufferedAttributeValueAccessor(_writer);
370+
}
371+
372+
_writer.WriteEndMethodInvocation(endLine: false);
360373
}
361374

362-
_writer
363-
.WriteEndMethodInvocation(endLine: false)
364-
.WriteEndMethodInvocation();
375+
_writer.WriteEndMethodInvocation();
365376
}
366377
}
367378

src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,25 @@ public override void GenerateStartBlockCode(Block target, CodeGeneratorContext c
5757

5858
foreach (var attribute in tagHelperBlock.Attributes)
5959
{
60-
// Populates the code tree with chunks associated with attributes
61-
attribute.Value.Accept(codeGenerator);
60+
ChunkBlock attributeChunkValue = null;
6261

63-
var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks;
64-
var first = chunks.FirstOrDefault();
62+
if (attribute.Value != null)
63+
{
64+
// Populates the code tree with chunks associated with attributes
65+
attribute.Value.Accept(codeGenerator);
6566

66-
attributes.Add(new KeyValuePair<string, Chunk>(attribute.Key,
67-
new ChunkBlock
67+
var chunks = codeGenerator.Context.CodeTreeBuilder.CodeTree.Chunks;
68+
var first = chunks.FirstOrDefault();
69+
70+
attributeChunkValue = new ChunkBlock
6871
{
6972
Association = first?.Association,
7073
Children = chunks,
7174
Start = first == null ? SourceLocation.Zero : first.Start
72-
}));
75+
};
76+
}
77+
78+
attributes.Add(new KeyValuePair<string, Chunk>(attribute.Key, attributeChunkValue));
7379

7480
// Reset the code tree builder so we can build a new one for the next attribute
7581
codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder();

src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ private bool EndTag(SourceLocation tagStart,
316316
var matched = RemoveTag(tags, tagName, tagStart);
317317

318318
if (tags.Count == 0 &&
319-
// Note tagName may contain a '!' escape character. This ensures </!text> doesn't match here.
319+
// Note tagName may contain a '!' escape character. This ensures </!text> doesn't match here.
320320
// </!text> tags are treated like any other escaped HTML end tag.
321321
string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) &&
322322
matched)
@@ -471,11 +471,21 @@ private void BeforeAttribute()
471471
return;
472472
}
473473

474+
// Minimized attribute
475+
// Need to modify this code to account for space inbetween equals when
476+
// https://github.com/aspnet/Razor/issues/123 is completed.
474477
if (!At(HtmlSymbolType.Equals))
475478
{
476-
// Saw a space or newline after the name, so just skip this attribute and continue around the loop
477-
Accept(whitespace);
478-
Accept(name);
479+
// Output anything prior to the attribute, in most cases this will be the tag name:
480+
// |<input| checked />
481+
Output(SpanKind.Markup);
482+
using (Context.StartBlock(BlockType.Markup))
483+
{
484+
Accept(whitespace);
485+
Accept(name);
486+
Output(SpanKind.Markup);
487+
}
488+
479489
return;
480490
}
481491

@@ -719,7 +729,7 @@ private bool StartTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, IDisposable
719729
Tuple<HtmlSymbol, SourceLocation> tag = Tuple.Create(tagName, _lastTagStart);
720730

721731
if (tags.Count == 0 &&
722-
// Note tagName may contain a '!' escape character. This ensures <!text> doesn't match here.
732+
// Note tagName may contain a '!' escape character. This ensures <!text> doesn't match here.
723733
// <!text> tags are treated like any other escaped HTML start tag.
724734
string.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
725735
{

src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlock.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ public TagHelperBlock(TagHelperBlockBuilder source)
3838

3939
foreach (var attributeChildren in Attributes)
4040
{
41-
attributeChildren.Value.Parent = this;
41+
if (attributeChildren.Value != null)
42+
{
43+
attributeChildren.Value.Parent = this;
44+
}
4245
}
4346
}
4447

src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperBlockRewriter.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -71,11 +71,12 @@ private static IList<KeyValuePair<string, SyntaxTreeNode>> GetTagAttributes(
7171
// Only want to track the attribute if we succeeded in parsing its corresponding Block/Span.
7272
if (succeeded)
7373
{
74-
// Check if it's a bound attribute that is not of type string and happens to be null or whitespace.
74+
// Check if it's a bound attribute that is minimized or not of type string and null or whitespace.
7575
string attributeValueType;
7676
if (attributeValueTypes.TryGetValue(attribute.Key, out attributeValueType) &&
77+
(attribute.Value == null ||
7778
!IsStringAttribute(attributeValueType) &&
78-
IsNullOrWhitespaceAttributeValue(attribute.Value))
79+
IsNullOrWhitespaceAttributeValue(attribute.Value)))
7980
{
8081
var errorLocation = GetAttributeNameStartLocation(child);
8182

@@ -167,7 +168,7 @@ private static bool TryParseSpan(
167168
{
168169
Debug.Assert(
169170
name != null,
170-
"Name should never be null here. The parser should guaruntee an attribute has a name.");
171+
"Name should never be null here. The parser should guarantee an attribute has a name.");
171172

172173
// We've captured all leading whitespace and the attribute name.
173174
// We're now at: " asp-for|='...'" or " asp-for|=..."
@@ -239,7 +240,10 @@ private static bool TryParseSpan(
239240
return false;
240241
}
241242

242-
attribute = CreateMarkupAttribute(name, builder, attributeValueTypes);
243+
// If we're not after an equal then we should treat the value as if it were a minimized attribute.
244+
// Aka null value.
245+
var attributeValueBuilder = afterEquals ? builder : null;
246+
attribute = CreateMarkupAttribute(name, attributeValueBuilder, attributeValueTypes);
243247

244248
return true;
245249
}
@@ -431,17 +435,24 @@ private static KeyValuePair<string, SyntaxTreeNode> CreateMarkupAttribute(
431435
IReadOnlyDictionary<string, string> attributeValueTypes)
432436
{
433437
string attributeTypeName;
438+
Span value = null;
434439

435-
// If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat
436-
// its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode
437-
// reflects that.
438-
if (attributeValueTypes.TryGetValue(name, out attributeTypeName) &&
439-
!IsStringAttribute(attributeTypeName))
440+
// Builder will be null in the case of minimized attributes
441+
if (builder != null)
440442
{
441-
builder.Kind = SpanKind.Code;
443+
// If the attribute was requested by the tag helper and doesn't happen to be a string then we need to treat
444+
// its value as code. Any non-string value can be any C# value so we need to ensure the SyntaxTreeNode
445+
// reflects that.
446+
if (attributeValueTypes.TryGetValue(name, out attributeTypeName) &&
447+
!IsStringAttribute(attributeTypeName))
448+
{
449+
builder.Kind = SpanKind.Code;
450+
}
451+
452+
value = builder.Build();
442453
}
443454

444-
return new KeyValuePair<string, SyntaxTreeNode>(name, builder.Build());
455+
return new KeyValuePair<string, SyntaxTreeNode>(name, value);
445456
}
446457

447458
private static bool IsNullOrWhitespaceAttributeValue(SyntaxTreeNode attributeValue)

0 commit comments

Comments
 (0)