builder)
{
- // Calling to legacy code for now.
- LegacyParseTagContent(builder);
+ if (!At(SyntaxKind.Whitespace) && !At(SyntaxKind.NewLine))
+ {
+ // We should be right after the tag name, so if there's no whitespace or new line, something is wrong
+ ParseMiscAttribute(builder);
+ return;
+ }
+
+ // We are here ($):
+
+ while (!EndOfFile && !IsEndOfTag())
+ {
+ if (At(SyntaxKind.ForwardSlash))
+ {
+ // This means we're at a '/' but it's not considered end of tag. E.g.
+ // We are at the '/' but the tag isn't closed. Accept and continue parsing the next attribute.
+ AcceptAndMoveNext();
+ }
+
+ ParseAttribute(builder);
+ }
+ }
+
+ private bool IsEndOfTag()
+ {
+ if (At(SyntaxKind.ForwardSlash))
+ {
+ if (NextIs(SyntaxKind.CloseAngle) || NextIs(SyntaxKind.OpenAngle))
+ {
+ return true;
+ }
+ }
+
+ return At(SyntaxKind.CloseAngle) || At(SyntaxKind.OpenAngle);
+ }
+
+ private void ParseMiscAttribute(in SyntaxListBuilder builder)
+ {
+ using (var pooledResult = Pool.Allocate())
+ {
+ var miscAttributeContentBuilder = pooledResult.Builder;
+ while (!EndOfFile)
+ {
+ ParseMarkupNodes(miscAttributeContentBuilder, ParseMode.Text, IsTagRecoveryStopPoint);
+ if (!EndOfFile)
+ {
+ EnsureCurrent();
+ switch (CurrentToken.Kind)
+ {
+ case SyntaxKind.SingleQuote:
+ case SyntaxKind.DoubleQuote:
+ // We should parse until we reach a matching quote.
+ var openQuoteKind = CurrentToken.Kind;
+ AcceptAndMoveNext();
+ ParseMarkupNodes(miscAttributeContentBuilder, ParseMode.Text, token => token.Kind == openQuoteKind);
+ if (!EndOfFile)
+ {
+ Assert(openQuoteKind);
+ AcceptAndMoveNext();
+ }
+ break;
+ case SyntaxKind.OpenAngle: // Another "<" means this tag is invalid.
+ case SyntaxKind.ForwardSlash: // Empty tag
+ case SyntaxKind.CloseAngle: // End of tag
+ miscAttributeContentBuilder.Add(OutputAsMarkupLiteral());
+ if (miscAttributeContentBuilder.Count > 0)
+ {
+ var miscAttributeContent = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeContentBuilder.ToList());
+ builder.Add(miscAttributeContent);
+ }
+ return;
+ default:
+ AcceptAndMoveNext();
+ break;
+ }
+ }
+ }
+
+ miscAttributeContentBuilder.Add(OutputAsMarkupLiteral());
+ if (miscAttributeContentBuilder.Count > 0)
+ {
+ var miscAttributeContent = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeContentBuilder.ToList());
+ builder.Add(miscAttributeContent);
+ }
+ }
+ }
+
+ private void ParseAttribute(in SyntaxListBuilder builder)
+ {
+ // Output anything prior to the attribute, in most cases this will be any invalid content after the tag name or a previous attribute:
+ // . If there is nothing in-between other attributes this will noop.
+ using (var pooledResult = Pool.Allocate())
+ {
+ var miscAttributeContentBuilder = pooledResult.Builder;
+ miscAttributeContentBuilder.Add(OutputAsMarkupLiteral());
+ if (miscAttributeContentBuilder.Count > 0)
+ {
+ var invalidAttributeBlock = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeContentBuilder.ToList());
+ builder.Add(invalidAttributeBlock);
+ }
+ }
+
+ // http://dev.w3.org/html5/spec/tokenization.html#before-attribute-name-state
+ // Capture whitespace
+ var attributePrefixWhitespace = ReadWhile(token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
+
+ // http://dev.w3.org/html5/spec/tokenization.html#attribute-name-state
+ // Read the 'name' (i.e. read until the '=' or whitespace/newline)
+ if (!TryParseAttributeName(out var nameTokens))
+ {
+ // Unexpected character in tag, enter recovery
+ Accept(attributePrefixWhitespace);
+ ParseMiscAttribute(builder);
+ return;
+ }
+
+ Accept(attributePrefixWhitespace); // Whitespace before attribute name
+ var namePrefix = OutputAsMarkupLiteral();
+ Accept(nameTokens); // Attribute name
+ var name = OutputAsMarkupLiteral();
+
+ var atMinimizedAttribute = !TokenExistsAfterWhitespace(SyntaxKind.Equals);
+ if (atMinimizedAttribute)
+ {
+ // Minimized attribute
+ var minimizedAttributeBlock = SyntaxFactory.MarkupMinimizedAttributeBlock(namePrefix, name);
+ builder.Add(minimizedAttributeBlock);
+ }
+ else
+ {
+ // Not a minimized attribute
+ var attributeBlock = ParseRemainingAttribute(namePrefix, name);
+ builder.Add(attributeBlock);
+ }
+ }
+
+ private bool TryParseAttributeName(out IEnumerable nameTokens)
+ {
+ nameTokens = Enumerable.Empty();
+ if (At(SyntaxKind.Transition) || At(SyntaxKind.RazorCommentTransition))
+ {
+ return false;
+ }
+
+ if (IsValidAttributeNameToken(CurrentToken))
+ {
+ nameTokens = ReadWhile(token =>
+ token.Kind != SyntaxKind.Whitespace &&
+ token.Kind != SyntaxKind.NewLine &&
+ token.Kind != SyntaxKind.Equals &&
+ token.Kind != SyntaxKind.CloseAngle &&
+ token.Kind != SyntaxKind.OpenAngle &&
+ (token.Kind != SyntaxKind.ForwardSlash || !NextIs(SyntaxKind.CloseAngle)));
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private MarkupAttributeBlockSyntax ParseRemainingAttribute(MarkupTextLiteralSyntax namePrefix, MarkupTextLiteralSyntax name)
+ {
+ // Since this is not a minimized attribute, the whitespace after attribute name belongs to this attribute.
+ AcceptWhile(token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
+ var nameSuffix = OutputAsMarkupLiteral();
+
+ Assert(SyntaxKind.Equals); // We should be at "="
+ var equalsToken = EatCurrentToken();
+
+ var whitespaceAfterEquals = ReadWhile(token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
+ var quote = SyntaxKind.Marker;
+ if (At(SyntaxKind.SingleQuote) || At(SyntaxKind.DoubleQuote))
+ {
+ // Found a quote, the whitespace belongs to this attribute.
+ Accept(whitespaceAfterEquals);
+ quote = CurrentToken.Kind;
+ AcceptAndMoveNext();
+ }
+ else if (whitespaceAfterEquals.Any())
+ {
+ // No quotes found after the whitespace. Put it back so that it can be parsed later.
+ PutCurrentBack();
+ PutBack(whitespaceAfterEquals);
+ }
+
+ MarkupTextLiteralSyntax valuePrefix = null;
+ RazorBlockSyntax attributeValue = null;
+ MarkupTextLiteralSyntax valueSuffix = null;
+
+ // First, determine if this is a 'data-' attribute (since those can't use conditional attributes)
+ var nameContent = string.Concat(name.LiteralTokens.Nodes.Select(s => s.Content));
+ if (IsConditionalAttributeName(nameContent))
+ {
+ SpanContext.ChunkGenerator = SpanChunkGenerator.Null; // The block chunk generator will render the prefix
+
+ // We now have the value prefix which is usually whitespace and/or a quote
+ valuePrefix = OutputAsMarkupLiteral();
+
+ // Read the attribute value only if the value is quoted
+ // or if there is no whitespace between '=' and the unquoted value.
+ if (quote != SyntaxKind.Marker || !whitespaceAfterEquals.Any())
+ {
+ using (var pooledResult = Pool.Allocate())
+ {
+ var attributeValueBuilder = pooledResult.Builder;
+ // Read the attribute value.
+ while (!EndOfFile && !IsEndOfAttributeValue(quote, CurrentToken))
+ {
+ ParseConditionalAttributeValue(attributeValueBuilder, quote);
+ }
+
+ if (attributeValueBuilder.Count > 0)
+ {
+ attributeValue = SyntaxFactory.GenericBlock(attributeValueBuilder.ToList());
+ }
+ }
+ }
+
+ // Capture the suffix
+ if (quote != SyntaxKind.Marker && At(quote))
+ {
+ AcceptAndMoveNext();
+ // Again, block chunk generator will render the suffix
+ SpanContext.ChunkGenerator = SpanChunkGenerator.Null;
+ valueSuffix = OutputAsMarkupLiteral();
+ }
+ }
+ else if (quote != SyntaxKind.Marker || !whitespaceAfterEquals.Any())
+ {
+ valuePrefix = OutputAsMarkupLiteral();
+
+ attributeValue = ParseNonConditionalAttributeValue(quote);
+
+ if (quote != SyntaxKind.Marker)
+ {
+ TryAccept(quote);
+ valueSuffix = OutputAsMarkupLiteral();
+ }
+ }
+ else
+ {
+ // There is no quote and there is whitespace after equals. There is no attribute value.
+ }
+
+ return SyntaxFactory.MarkupAttributeBlock(namePrefix, name, nameSuffix, equalsToken, valuePrefix, attributeValue, valueSuffix);
+ }
+
+ private RazorBlockSyntax ParseNonConditionalAttributeValue(SyntaxKind quote)
+ {
+ using (var pooledResult = Pool.Allocate())
+ {
+ var attributeValueBuilder = pooledResult.Builder;
+ // Not a "conditional" attribute, so just read the value
+ ParseMarkupNodes(attributeValueBuilder, ParseMode.Text, token => IsEndOfAttributeValue(quote, token));
+
+ // Output already accepted tokens if any as markup literal
+ var literalValue = OutputAsMarkupLiteral();
+ attributeValueBuilder.Add(literalValue);
+
+ // Capture the attribute value (will include everything in-between the attribute's quotes).
+ return SyntaxFactory.GenericBlock(attributeValueBuilder.ToList());
+ }
+ }
+
+ private void ParseConditionalAttributeValue(in SyntaxListBuilder builder, SyntaxKind quote)
+ {
+ var prefixStart = CurrentStart;
+ var prefixTokens = ReadWhile(token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
+
+ if (At(SyntaxKind.Transition))
+ {
+ if (NextIs(SyntaxKind.Transition))
+ {
+ using (var pooledResult = Pool.Allocate())
+ {
+ var markupBuilder = pooledResult.Builder;
+ Accept(prefixTokens);
+
+ // Render a single "@" in place of "@@".
+ SpanContext.ChunkGenerator = new LiteralAttributeChunkGenerator(
+ new LocationTagged(string.Concat(prefixTokens.Select(s => s.Content)), prefixStart),
+ new LocationTagged(CurrentToken.Content, CurrentStart));
+ AcceptAndMoveNext();
+ SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
+ markupBuilder.Add(OutputAsMarkupLiteral());
+
+ SpanContext.ChunkGenerator = SpanChunkGenerator.Null;
+ AcceptAndMoveNext();
+ SpanContext.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.None;
+ markupBuilder.Add(OutputAsMarkupEphemeralLiteral());
+
+ var markupBlock = SyntaxFactory.MarkupBlock(markupBuilder.ToList());
+ builder.Add(markupBlock);
+ }
+ }
+ else
+ {
+ Accept(prefixTokens);
+ var valueStart = CurrentStart;
+ PutCurrentBack();
+
+ var prefix = OutputAsMarkupLiteral();
+
+ // Dynamic value, start a new block and set the chunk generator
+ using (var pooledResult = Pool.Allocate())
+ {
+ var dynamicAttributeValueBuilder = pooledResult.Builder;
+
+ OtherParserBlock(dynamicAttributeValueBuilder);
+ var value = SyntaxFactory.MarkupDynamicAttributeValue(prefix, SyntaxFactory.GenericBlock(dynamicAttributeValueBuilder.ToList()));
+ builder.Add(value);
+ }
+ }
+ }
+ else
+ {
+ Accept(prefixTokens);
+ var prefix = OutputAsMarkupLiteral();
+
+ // Literal value
+ // 'quote' should be "Unknown" if not quoted and tokens coming from the tokenizer should never have
+ // "Unknown" type.
+ var valueTokens = ReadWhile(token =>
+ // These three conditions find separators which break the attribute value into portions
+ token.Kind != SyntaxKind.Whitespace &&
+ token.Kind != SyntaxKind.NewLine &&
+ token.Kind != SyntaxKind.Transition &&
+ // This condition checks for the end of the attribute value (it repeats some of the checks above
+ // but for now that's ok)
+ !IsEndOfAttributeValue(quote, token));
+ Accept(valueTokens);
+ var value = OutputAsMarkupLiteral();
+
+ var literalAttributeValue = SyntaxFactory.MarkupLiteralAttributeValue(prefix, value);
+ builder.Add(literalAttributeValue);
+ }
+ }
+
+ private bool IsEndOfAttributeValue(SyntaxKind quote, SyntaxToken token)
+ {
+ return EndOfFile || token == null ||
+ (quote != SyntaxKind.Marker
+ ? token.Kind == quote // If quoted, just wait for the quote
+ : IsUnquotedEndOfAttributeValue(token));
+ }
+
+ private bool IsUnquotedEndOfAttributeValue(SyntaxToken token)
+ {
+ // If unquoted, we have a larger set of terminating characters:
+ // http://dev.w3.org/html5/spec/tokenization.html#attribute-value-unquoted-state
+ // Also we need to detect "/" and ">"
+ return token.Kind == SyntaxKind.DoubleQuote ||
+ token.Kind == SyntaxKind.SingleQuote ||
+ token.Kind == SyntaxKind.OpenAngle ||
+ token.Kind == SyntaxKind.Equals ||
+ (token.Kind == SyntaxKind.ForwardSlash && NextIs(SyntaxKind.CloseAngle)) ||
+ token.Kind == SyntaxKind.CloseAngle ||
+ token.Kind == SyntaxKind.Whitespace ||
+ token.Kind == SyntaxKind.NewLine;
}
private void ParseJavascriptAndEndScriptTag(in SyntaxListBuilder builder, AcceptedCharactersInternal endTagAcceptedCharacters = AcceptedCharactersInternal.Any)
@@ -811,6 +1170,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
}
}
+ private bool IsConditionalAttributeName(string name)
+ {
+ var attributeCanBeConditional =
+ Context.FeatureFlags.EXPERIMENTAL_AllowConditionalDataDashAttributes ||
+ !name.StartsWith("data-", StringComparison.OrdinalIgnoreCase);
+ return attributeCanBeConditional;
+ }
+
private void OtherParserBlock(in SyntaxListBuilder builder)
{
AcceptMarkerTokenIfNecessary();
@@ -860,6 +1227,38 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
return token.Kind == SyntaxKind.Text && token.Content == "-";
}
+ internal static bool IsValidAttributeNameToken(SyntaxToken token)
+ {
+ if (token == null)
+ {
+ return false;
+ }
+
+ // These restrictions cover most of the spec defined: http://www.w3.org/TR/html5/syntax.html#attributes-0
+ // However, it's not all of it. For instance we don't special case control characters or allow OpenAngle.
+ // It also doesn't try to exclude Razor specific features such as the @ transition. This is based on the
+ // expectation that the parser handles such scenarios prior to falling through to name resolution.
+ var tokenType = token.Kind;
+ return tokenType != SyntaxKind.Whitespace &&
+ tokenType != SyntaxKind.NewLine &&
+ tokenType != SyntaxKind.CloseAngle &&
+ tokenType != SyntaxKind.OpenAngle &&
+ tokenType != SyntaxKind.ForwardSlash &&
+ tokenType != SyntaxKind.DoubleQuote &&
+ tokenType != SyntaxKind.SingleQuote &&
+ tokenType != SyntaxKind.Equals &&
+ tokenType != SyntaxKind.Marker;
+ }
+
+ private static bool IsTagRecoveryStopPoint(SyntaxToken token)
+ {
+ return token.Kind == SyntaxKind.CloseAngle ||
+ token.Kind == SyntaxKind.ForwardSlash ||
+ token.Kind == SyntaxKind.OpenAngle ||
+ token.Kind == SyntaxKind.SingleQuote ||
+ token.Kind == SyntaxKind.DoubleQuote;
+ }
+
private void DefaultMarkupSpanContext(SpanContextBuilder spanContext)
{
spanContext.ChunkGenerator = new MarkupChunkGenerator();
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs
index 2a3d016224..de1945316e 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs
@@ -16,8 +16,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
throw new ArgumentNullException(nameof(syntaxTree));
}
+ var rewriter = new ClassifiedSpanRewriter();
+ var rewritten = rewriter.Visit(syntaxTree.Root);
var visitor = new ClassifiedSpanVisitor(syntaxTree.Source);
- visitor.Visit(syntaxTree.Root);
+ visitor.Visit(rewritten);
return visitor.ClassifiedSpans;
}
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs
index 421485fdcc..4c11269695 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Legacy/TagHelperBlockRewriter.cs
@@ -107,29 +107,36 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy
processedBoundAttributeNames);
tagHelperBuilder.Add(result.RewrittenAttribute);
}
- else if (child is CSharpCodeBlockSyntax)
+ else if (child is MarkupMiscAttributeContentSyntax miscContent)
{
- // TODO: Accept more than just Markup attributes: https://github.com/aspnet/Razor/issues/96.
- // Something like:
- //
- var location = new SourceSpan(child.GetSourceLocation(source), child.FullWidth);
- var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName);
- errorSink.OnError(diagnostic);
-
- result = null;
- }
- else if (child is MarkupTextLiteralSyntax)
- {
- // If the original span content was whitespace it ultimately means the tag
- // that owns this "attribute" is malformed and is expecting a user to type a new attribute.
- // ex:
+ var location = new SourceSpan(codeBlock.GetSourceLocation(source), codeBlock.FullWidth);
+ var diagnostic = RazorDiagnosticFactory.CreateParsing_TagHelpersCannotHaveCSharpInTagDeclaration(location, tagName);
+ errorSink.OnError(diagnostic);
+ break;
+ }
+ else
+ {
+ // If the original span content was whitespace it ultimately means the tag
+ // that owns this "attribute" is malformed and is expecting a user to type a new attribute.
+ // ex:
+ token.Kind == SyntaxKind.Whitespace ||
+ (includeNewLines && token.Kind == SyntaxKind.NewLine));
+ tokenFound = At(kind);
+
+ PutCurrentBack();
+ PutBack(whitespace);
+ EnsureCurrent();
+
+ return tokenFound;
+ }
+
protected bool EnsureCurrent()
{
if (CurrentToken == null)
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Internal.Generated.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Internal.Generated.cs
index 1849a7b410..b495c48787 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Internal.Generated.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Internal.Generated.cs
@@ -1176,6 +1176,87 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
}
}
+ internal sealed partial class MarkupMiscAttributeContentSyntax : MarkupSyntaxNode
+ {
+ private readonly GreenNode _children;
+
+ internal MarkupMiscAttributeContentSyntax(SyntaxKind kind, GreenNode children, RazorDiagnostic[] diagnostics, SyntaxAnnotation[] annotations)
+ : base(kind, diagnostics, annotations)
+ {
+ SlotCount = 1;
+ if (children != null)
+ {
+ AdjustFlagsAndWidth(children);
+ _children = children;
+ }
+ }
+
+
+ internal MarkupMiscAttributeContentSyntax(SyntaxKind kind, GreenNode children)
+ : base(kind)
+ {
+ SlotCount = 1;
+ if (children != null)
+ {
+ AdjustFlagsAndWidth(children);
+ _children = children;
+ }
+ }
+
+ public SyntaxList Children { get { return new SyntaxList(_children); } }
+
+ internal override GreenNode GetSlot(int index)
+ {
+ switch (index)
+ {
+ case 0: return _children;
+ default: return null;
+ }
+ }
+
+ internal override SyntaxNode CreateRed(SyntaxNode parent, int position)
+ {
+ return new Syntax.MarkupMiscAttributeContentSyntax(this, parent, position);
+ }
+
+ public override TResult Accept(SyntaxVisitor visitor)
+ {
+ return visitor.VisitMarkupMiscAttributeContent(this);
+ }
+
+ public override void Accept(SyntaxVisitor visitor)
+ {
+ visitor.VisitMarkupMiscAttributeContent(this);
+ }
+
+ public MarkupMiscAttributeContentSyntax Update(Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax.SyntaxList children)
+ {
+ if (children != Children)
+ {
+ var newNode = SyntaxFactory.MarkupMiscAttributeContent(children);
+ var diags = GetDiagnostics();
+ if (diags != null && diags.Length > 0)
+ newNode = newNode.WithDiagnosticsGreen(diags);
+ var annotations = GetAnnotations();
+ if (annotations != null && annotations.Length > 0)
+ newNode = newNode.WithAnnotationsGreen(annotations);
+ return newNode;
+ }
+
+ return this;
+ }
+
+ internal override GreenNode SetDiagnostics(RazorDiagnostic[] diagnostics)
+ {
+ return new MarkupMiscAttributeContentSyntax(Kind, _children, diagnostics, GetAnnotations());
+ }
+
+ internal override GreenNode SetAnnotations(SyntaxAnnotation[] annotations)
+ {
+ return new MarkupMiscAttributeContentSyntax(Kind, _children, GetDiagnostics(), annotations);
+ }
+ }
+
internal sealed partial class MarkupLiteralAttributeValueSyntax : MarkupSyntaxNode
{
private readonly MarkupTextLiteralSyntax _prefix;
@@ -3282,6 +3363,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
return DefaultVisit(node);
}
+ public virtual TResult VisitMarkupMiscAttributeContent(MarkupMiscAttributeContentSyntax node)
+ {
+ return DefaultVisit(node);
+ }
+
public virtual TResult VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
{
return DefaultVisit(node);
@@ -3466,6 +3552,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
DefaultVisit(node);
}
+ public virtual void VisitMarkupMiscAttributeContent(MarkupMiscAttributeContentSyntax node)
+ {
+ DefaultVisit(node);
+ }
+
public virtual void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
{
DefaultVisit(node);
@@ -3673,6 +3764,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
return node.Update(namePrefix, name, nameSuffix, equalsToken, valuePrefix, value, valueSuffix);
}
+ public override GreenNode VisitMarkupMiscAttributeContent(MarkupMiscAttributeContentSyntax node)
+ {
+ var children = VisitList(node.Children);
+ return node.Update(children);
+ }
+
public override GreenNode VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
{
var prefix = (MarkupTextLiteralSyntax)Visit(node.Prefix);
@@ -3987,6 +4084,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
return new MarkupAttributeBlockSyntax(SyntaxKind.MarkupAttributeBlock, namePrefix, name, nameSuffix, equalsToken, valuePrefix, value, valueSuffix);
}
+ public static MarkupMiscAttributeContentSyntax MarkupMiscAttributeContent(Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax.SyntaxList children)
+ {
+ var result = new MarkupMiscAttributeContentSyntax(SyntaxKind.MarkupMiscAttributeContent, children.Node);
+
+ return result;
+ }
+
public static MarkupLiteralAttributeValueSyntax MarkupLiteralAttributeValue(MarkupTextLiteralSyntax prefix, MarkupTextLiteralSyntax value)
{
var result = new MarkupLiteralAttributeValueSyntax(SyntaxKind.MarkupLiteralAttributeValue, prefix, value);
@@ -4235,6 +4339,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax
typeof(MarkupTagBlockSyntax),
typeof(MarkupMinimizedAttributeBlockSyntax),
typeof(MarkupAttributeBlockSyntax),
+ typeof(MarkupMiscAttributeContentSyntax),
typeof(MarkupLiteralAttributeValueSyntax),
typeof(MarkupDynamicAttributeValueSyntax),
typeof(MarkupElementSyntax),
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Main.Generated.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Main.Generated.cs
index b3c122bb05..019d64dc8d 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Main.Generated.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Main.Generated.cs
@@ -89,6 +89,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax
return DefaultVisit(node);
}
+ /// Called when the visitor visits a MarkupMiscAttributeContentSyntax node.
+ public virtual TResult VisitMarkupMiscAttributeContent(MarkupMiscAttributeContentSyntax node)
+ {
+ return DefaultVisit(node);
+ }
+
/// Called when the visitor visits a MarkupLiteralAttributeValueSyntax node.
public virtual TResult VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
{
@@ -308,6 +314,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax
DefaultVisit(node);
}
+ /// Called when the visitor visits a MarkupMiscAttributeContentSyntax node.
+ public virtual void VisitMarkupMiscAttributeContent(MarkupMiscAttributeContentSyntax node)
+ {
+ DefaultVisit(node);
+ }
+
/// Called when the visitor visits a MarkupLiteralAttributeValueSyntax node.
public virtual void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
{
@@ -538,6 +550,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax
return node.Update(namePrefix, name, nameSuffix, equalsToken, valuePrefix, value, valueSuffix);
}
+ public override SyntaxNode VisitMarkupMiscAttributeContent(MarkupMiscAttributeContentSyntax node)
+ {
+ var children = VisitList(node.Children);
+ return node.Update(children);
+ }
+
public override SyntaxNode VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
{
var prefix = (MarkupTextLiteralSyntax)Visit(node.Prefix);
@@ -905,6 +923,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax
return SyntaxFactory.MarkupAttributeBlock(default(MarkupTextLiteralSyntax), SyntaxFactory.MarkupTextLiteral(), default(MarkupTextLiteralSyntax), SyntaxFactory.Token(SyntaxKind.Equals), default(MarkupTextLiteralSyntax), default(RazorBlockSyntax), default(MarkupTextLiteralSyntax));
}
+ /// Creates a new MarkupMiscAttributeContentSyntax instance.
+ public static MarkupMiscAttributeContentSyntax MarkupMiscAttributeContent(SyntaxList children)
+ {
+ return (MarkupMiscAttributeContentSyntax)InternalSyntax.SyntaxFactory.MarkupMiscAttributeContent(children.Node.ToGreenList()).CreateRed();
+ }
+
+ /// Creates a new MarkupMiscAttributeContentSyntax instance.
+ public static MarkupMiscAttributeContentSyntax MarkupMiscAttributeContent()
+ {
+ return SyntaxFactory.MarkupMiscAttributeContent(default(SyntaxList));
+ }
+
/// Creates a new MarkupLiteralAttributeValueSyntax instance.
public static MarkupLiteralAttributeValueSyntax MarkupLiteralAttributeValue(MarkupTextLiteralSyntax prefix, MarkupTextLiteralSyntax value)
{
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Syntax.Generated.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Syntax.Generated.cs
index 8196bdb0ea..4c0878d99f 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Syntax.Generated.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Generated/Syntax.xml.Syntax.Generated.cs
@@ -1133,6 +1133,75 @@ namespace Microsoft.AspNetCore.Razor.Language.Syntax
}
}
+ internal sealed partial class MarkupMiscAttributeContentSyntax : MarkupSyntaxNode
+ {
+ private SyntaxNode _children;
+
+ internal MarkupMiscAttributeContentSyntax(GreenNode green, SyntaxNode parent, int position)
+ : base(green, parent, position)
+ {
+ }
+
+ public SyntaxList Children
+ {
+ get
+ {
+ return new SyntaxList(GetRed(ref _children, 0));
+ }
+ }
+
+ internal override SyntaxNode GetNodeSlot(int index)
+ {
+ switch (index)
+ {
+ case 0: return GetRedAtZero(ref _children);
+ default: return null;
+ }
+ }
+ internal override SyntaxNode GetCachedSlot(int index)
+ {
+ switch (index)
+ {
+ case 0: return _children;
+ default: return null;
+ }
+ }
+
+ public override TResult Accept(SyntaxVisitor visitor)
+ {
+ return visitor.VisitMarkupMiscAttributeContent(this);
+ }
+
+ public override void Accept(SyntaxVisitor visitor)
+ {
+ visitor.VisitMarkupMiscAttributeContent(this);
+ }
+
+ public MarkupMiscAttributeContentSyntax Update(SyntaxList children)
+ {
+ if (children != Children)
+ {
+ var newNode = SyntaxFactory.MarkupMiscAttributeContent(children);
+ var annotations = GetAnnotations();
+ if (annotations != null && annotations.Length > 0)
+ return newNode.WithAnnotations(annotations);
+ return newNode;
+ }
+
+ return this;
+ }
+
+ public MarkupMiscAttributeContentSyntax WithChildren(SyntaxList children)
+ {
+ return Update(children);
+ }
+
+ public MarkupMiscAttributeContentSyntax AddChildren(params RazorSyntaxNode[] items)
+ {
+ return WithChildren(this.Children.AddRange(items));
+ }
+ }
+
internal sealed partial class MarkupLiteralAttributeValueSyntax : MarkupSyntaxNode
{
private MarkupTextLiteralSyntax _prefix;
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Syntax.xml b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Syntax.xml
index 7880c49622..bb0ae07673 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Syntax.xml
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/Syntax.xml
@@ -86,6 +86,10 @@
+
+
+
+
@@ -99,7 +103,7 @@
-
+
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxKind.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxKind.cs
index 75dd554ab6..d8c68ec77e 100644
--- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxKind.cs
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxKind.cs
@@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Language
MarkupCommentBlock,
MarkupAttributeBlock,
MarkupMinimizedAttributeBlock,
+ MarkupMiscAttributeContent,
MarkupLiteralAttributeValue,
MarkupDynamicAttributeValue,
MarkupTagHelperElement,
diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxUtilities.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxUtilities.cs
new file mode 100644
index 0000000000..e308c92934
--- /dev/null
+++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/Syntax/SyntaxUtilities.cs
@@ -0,0 +1,46 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Razor.Language.Syntax
+{
+ internal static class SyntaxUtilities
+ {
+ public static MarkupTextLiteralSyntax MergeTextLiterals(params MarkupTextLiteralSyntax[] literalSyntaxes)
+ {
+ if (literalSyntaxes == null || literalSyntaxes.Length == 0)
+ {
+ return null;
+ }
+
+ SyntaxNode parent = null;
+ var position = 0;
+ var seenFirstLiteral = false;
+ var builder = Syntax.InternalSyntax.SyntaxListBuilder.Create();
+
+ foreach (var syntax in literalSyntaxes)
+ {
+ if (syntax == null)
+ {
+ continue;
+ }
+ else if (!seenFirstLiteral)
+ {
+ // Set the parent and position of the merged literal to the value of the first non-null literal.
+ parent = syntax.Parent;
+ position = syntax.Position;
+ seenFirstLiteral = true;
+ }
+
+ foreach (var token in syntax.LiteralTokens)
+ {
+ builder.Add(token.Green);
+ }
+ }
+
+ var mergedLiteralSyntax = Syntax.InternalSyntax.SyntaxFactory.MarkupTextLiteral(
+ builder.ToList());
+
+ return (MarkupTextLiteralSyntax)mergedLiteralSyntax.CreateRed(parent, position);
+ }
+ }
+}