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

Commit 08c8f9f

Browse files
committed
Parse the whitespace surrounding equals in attribute correctly
- #123 - Handled the corresponding cases in tag helper scenarios - Added unit and code generation tests
1 parent 6568de3 commit 08c8f9f

File tree

8 files changed

+339
-66
lines changed

8 files changed

+339
-66
lines changed

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

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ private void BeforeAttribute()
463463
// http://dev.w3.org/html5/spec/tokenization.html#attribute-name-state
464464
// Read the 'name' (i.e. read until the '=' or whitespace/newline)
465465
var name = Enumerable.Empty<HtmlSymbol>();
466+
var whitespaceAfterAttributeName = Enumerable.Empty<HtmlSymbol>();
466467
if (At(HtmlSymbolType.Text))
467468
{
468469
name = ReadWhile(sym =>
@@ -472,6 +473,10 @@ private void BeforeAttribute()
472473
sym.Type != HtmlSymbolType.CloseAngle &&
473474
sym.Type != HtmlSymbolType.OpenAngle &&
474475
(sym.Type != HtmlSymbolType.ForwardSlash || !NextIs(HtmlSymbolType.CloseAngle)));
476+
477+
// capture whitespace after attribute name (if any)
478+
whitespaceAfterAttributeName = ReadWhile(
479+
sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
475480
}
476481
else
477482
{
@@ -485,6 +490,10 @@ private void BeforeAttribute()
485490
{
486491
// Minimized attribute
487492

493+
// We are at the prefix of the next attribute or the end of tag. Put it back so it is parsed later.
494+
PutCurrentBack();
495+
PutBack(whitespaceAfterAttributeName);
496+
488497
// Output anything prior to the attribute, in most cases this will be the tag name:
489498
// |<input| checked />. If in-between other attributes this will noop or output malformed attribute
490499
// content (if the previous attribute was malformed).
@@ -507,11 +516,14 @@ private void BeforeAttribute()
507516
// Start a new markup block for the attribute
508517
using (Context.StartBlock(BlockType.Markup))
509518
{
510-
AttributePrefix(whitespace, name);
519+
AttributePrefix(whitespace, name, whitespaceAfterAttributeName);
511520
}
512521
}
513522

514-
private void AttributePrefix(IEnumerable<HtmlSymbol> whitespace, IEnumerable<HtmlSymbol> nameSymbols)
523+
private void AttributePrefix(
524+
IEnumerable<HtmlSymbol> whitespace,
525+
IEnumerable<HtmlSymbol> nameSymbols,
526+
IEnumerable<HtmlSymbol> whitespaceAfterAttributeName)
515527
{
516528
// First, determine if this is a 'data-' attribute (since those can't use conditional attributes)
517529
var name = nameSymbols.GetContent(Span.Start);
@@ -520,14 +532,27 @@ private void AttributePrefix(IEnumerable<HtmlSymbol> whitespace, IEnumerable<Htm
520532
// Accept the whitespace and name
521533
Accept(whitespace);
522534
Accept(nameSymbols);
535+
536+
// Since this is not a minimized attribute, the whitespace after attribute name belongs to this attribute.
537+
Accept(whitespaceAfterAttributeName);
523538
Assert(HtmlSymbolType.Equals); // We should be at "="
524539
AcceptAndMoveNext();
540+
541+
var whitespaceAfterEquals = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
525542
var quote = HtmlSymbolType.Unknown;
526543
if (At(HtmlSymbolType.SingleQuote) || At(HtmlSymbolType.DoubleQuote))
527544
{
545+
// Found a quote, the whitespace belongs to this attribute.
546+
Accept(whitespaceAfterEquals);
528547
quote = CurrentSymbol.Type;
529548
AcceptAndMoveNext();
530549
}
550+
else if (whitespaceAfterEquals.Any())
551+
{
552+
// No quotes found after the whitespace. Put it back so that it can be parsed later.
553+
PutCurrentBack();
554+
PutBack(whitespaceAfterEquals);
555+
}
531556

532557
// We now have the prefix: (i.e. ' foo="')
533558
var prefix = Span.GetContent();
@@ -537,10 +562,15 @@ private void AttributePrefix(IEnumerable<HtmlSymbol> whitespace, IEnumerable<Htm
537562
Span.ChunkGenerator = SpanChunkGenerator.Null; // The block chunk generator will render the prefix
538563
Output(SpanKind.Markup);
539564

540-
// Read the values
541-
while (!EndOfFile && !IsEndOfAttributeValue(quote, CurrentSymbol))
565+
// Read the attribute value only if the value is quoted
566+
// or if there is no whitespace between '=' and the unquoted value.
567+
if (quote != HtmlSymbolType.Unknown || !whitespaceAfterEquals.Any())
542568
{
543-
AttributeValue(quote);
569+
// Read the attribute value.
570+
while (!EndOfFile && !IsEndOfAttributeValue(quote, CurrentSymbol))
571+
{
572+
AttributeValue(quote);
573+
}
544574
}
545575

546576
// Capture the suffix
@@ -567,6 +597,11 @@ private void AttributePrefix(IEnumerable<HtmlSymbol> whitespace, IEnumerable<Htm
567597
// Output the attribute name, the equals and optional quote. Ex: foo="
568598
Output(SpanKind.Markup);
569599

600+
if (quote == HtmlSymbolType.Unknown && whitespaceAfterEquals.Any())
601+
{
602+
return;
603+
}
604+
570605
// Not a "conditional" attribute, so just read the value
571606
SkipToAndParseCode(sym => IsEndOfAttributeValue(quote, sym));
572607

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,23 +206,32 @@ private static TryParseResult TryParseSpan(
206206
// The goal here is to consume the equal sign and the optional single/double-quote.
207207

208208
// The coming symbols will either be a quote or value (in the case that the value is unquoted).
209-
// Spaces after/before the equal symbol are not yet supported:
210-
// https://github.com/aspnet/Razor/issues/123
211209

212210
// TODO: Handle malformed tags, if there's an '=' then there MUST be a value.
213211
// https://github.com/aspnet/Razor/issues/104
214212

215213
SourceLocation symbolStartLocation;
216214

215+
// Skip the whitespace preceding the start of the attribute value.
216+
var valueStartIndex = i + 1; // Start from the symbol after '='.
217+
while (valueStartIndex < htmlSymbols.Length &&
218+
(htmlSymbols[valueStartIndex].Type == HtmlSymbolType.WhiteSpace ||
219+
htmlSymbols[valueStartIndex].Type == HtmlSymbolType.NewLine))
220+
{
221+
valueStartIndex++;
222+
}
223+
217224
// Check for attribute start values, aka single or double quote
218-
if ((i + 1) < htmlSymbols.Length && IsQuote(htmlSymbols[i + 1]))
225+
if (valueStartIndex < htmlSymbols.Length && IsQuote(htmlSymbols[valueStartIndex]))
219226
{
220227
// Move past the attribute start so we can accept the true value.
221-
i++;
222-
symbolStartLocation = htmlSymbols[i].Start;
228+
valueStartIndex++;
229+
symbolStartLocation = htmlSymbols[valueStartIndex].Start;
223230

224231
// If there's a start quote then there must be an end quote to be valid, skip it.
225232
symbolOffset = 1;
233+
234+
i = valueStartIndex - 1;
226235
}
227236
else
228237
{

test/Microsoft.AspNet.Razor.Test/CodeGenerators/CSharpTagHelperRenderingTest.cs

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -848,32 +848,36 @@ public static TheoryData DesignTimeTagHelperTestData
848848
DefaultPAndInputTagHelperDescriptors,
849849
new List<LineMapping>
850850
{
851-
BuildLineMapping(documentAbsoluteIndex: 14,
852-
documentLineIndex: 0,
853-
generatedAbsoluteIndex: 493,
854-
generatedLineIndex: 15,
855-
characterOffsetIndex: 14,
856-
contentLength: 11),
857-
BuildLineMapping(documentAbsoluteIndex: 62,
858-
documentLineIndex: 3,
859-
documentCharacterOffsetIndex: 26,
860-
generatedAbsoluteIndex: 1289,
861-
generatedLineIndex: 39,
862-
generatedCharacterOffsetIndex: 28,
863-
contentLength: 0),
864-
BuildLineMapping(documentAbsoluteIndex: 122,
865-
documentLineIndex: 5,
866-
generatedAbsoluteIndex: 1634,
867-
generatedLineIndex: 48,
868-
characterOffsetIndex: 30,
869-
contentLength: 0),
870-
BuildLineMapping(documentAbsoluteIndex: 88,
871-
documentLineIndex: 4,
872-
documentCharacterOffsetIndex: 12,
873-
generatedAbsoluteIndex: 1789,
874-
generatedLineIndex: 54,
875-
generatedCharacterOffsetIndex: 19,
876-
contentLength: 0)
851+
BuildLineMapping(
852+
documentAbsoluteIndex: 14,
853+
documentLineIndex: 0,
854+
generatedAbsoluteIndex: 493,
855+
generatedLineIndex: 15,
856+
characterOffsetIndex: 14,
857+
contentLength: 11),
858+
BuildLineMapping(
859+
documentAbsoluteIndex: 63,
860+
documentLineIndex: 3,
861+
documentCharacterOffsetIndex: 27,
862+
generatedAbsoluteIndex: 1289,
863+
generatedLineIndex: 39,
864+
generatedCharacterOffsetIndex: 28,
865+
contentLength: 0),
866+
BuildLineMapping(
867+
documentAbsoluteIndex: 122,
868+
documentLineIndex: 5,
869+
generatedAbsoluteIndex: 1634,
870+
generatedLineIndex: 48,
871+
characterOffsetIndex: 30,
872+
contentLength: 0),
873+
BuildLineMapping(
874+
documentAbsoluteIndex: 89,
875+
documentLineIndex: 4,
876+
documentCharacterOffsetIndex: 13,
877+
generatedAbsoluteIndex: 1789,
878+
generatedLineIndex: 54,
879+
generatedCharacterOffsetIndex: 19,
880+
contentLength: 0),
877881
}
878882
},
879883
{
@@ -1484,6 +1488,7 @@ public static TheoryData RuntimeTimeTagHelperTestData
14841488
{
14851489
{ "SingleTagHelper", null, DefaultPAndInputTagHelperDescriptors },
14861490
{ "SingleTagHelperWithNewlineBeforeAttributes", null, DefaultPAndInputTagHelperDescriptors },
1491+
{ "TagHelpersWithWeirdlySpacedAttributes", null, DefaultPAndInputTagHelperDescriptors },
14871492
{ "BasicTagHelpers", null, DefaultPAndInputTagHelperDescriptors },
14881493
{ "BasicTagHelpers.RemoveTagHelper", null, DefaultPAndInputTagHelperDescriptors },
14891494
{ "BasicTagHelpers.Prefixed", null, PrefixedPAndInputTagHelperDescriptors },

0 commit comments

Comments
 (0)