|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
namespace Microsoft.AspNetCore.Razor.Language.Legacy;
internal class HtmlMarkupParser : TokenizerBackedParser<HtmlTokenizer>
{
private const string ScriptTagName = "script";
private static readonly SyntaxList<RazorSyntaxNode> EmptySyntaxList = new SyntaxListBuilder<RazorSyntaxNode>(0).ToList();
private static readonly char[] ValidAfterTypeAttributeNameCharacters = { ' ', '\t', '\r', '\n', '\f', '=' };
private static readonly SyntaxToken[] nonAllowedHtmlCommentEnding = new[]
{
SyntaxFactory.Token(SyntaxKind.Text, "-"),
SyntaxFactory.Token(SyntaxKind.Bang, "!"),
SyntaxFactory.Token(SyntaxKind.OpenAngle, "<"),
};
private Stack<TagTracker> _tagTracker = new Stack<TagTracker>();
public HtmlMarkupParser(ParserContext context)
: base(context.Options.ParseLeadingDirectives ? FirstDirectiveHtmlLanguageCharacteristics.Instance : HtmlLanguageCharacteristics.Instance, context)
{
}
private TagTracker? CurrentTracker => _tagTracker.Count > 0 ? _tagTracker.Peek() : null;
private string? CurrentStartTagName => CurrentTracker?.TagName;
private CSharpCodeParser? _codeParser;
public CSharpCodeParser CodeParser
{
get
{
// Note: Circular reference with HtmlMarkupParser means we can't set this in the constructor
Debug.Assert(_codeParser != null, "CodeParser should have been set during initialization");
return _codeParser!;
}
set => _codeParser = value;
}
private bool CaseSensitive { get; set; }
private StringComparison Comparison
{
get { return CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; }
}
//
// This is the main entry point into the Razor parser. This will be called only once.
// Anything outside of code blocks like @{} are parsed here.
// This calls into the code parser whenever a '@' transition is encountered.
// In this mode, we group markup elements with the appropriate Start tag, End tag and body
// but we don't perform any validation on the structure. We don't produce errors for cases like missing end tags etc.
// We let the editor take care of that.
//
public RazorDocumentSyntax ParseDocument()
{
if (Context == null)
{
throw new InvalidOperationException(Resources.Parser_Context_Not_Set);
}
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
using (PushSpanContextConfig(DefaultMarkupSpanContext))
{
var builder = pooledResult.Builder;
NextToken();
ParseMarkupNodes(builder, ParseMode.Markup);
AcceptMarkerTokenIfNecessary();
builder.Add(OutputAsMarkupLiteral());
// If we are still tracking any unclosed start tags, we need to close them.
while (_tagTracker.Count > 0)
{
var tracker = _tagTracker.Pop();
if (IsVoidElement(tracker.TagName))
{
// We were tracking a void element but we reached the end of the document without finding a matching end tag.
// So, close that element and move its content to its parent.
var children = builder.Consume();
var voidElement = SyntaxFactory.MarkupElement(tracker.StartTag, EmptySyntaxList, markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(voidElement);
builder.AddRange(children);
}
else
{
var element = SyntaxFactory.MarkupElement(tracker.StartTag, builder.Consume(), markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(element);
}
}
var markup = SyntaxFactory.MarkupBlock(builder.ToList());
return SyntaxFactory.RazorDocument(markup, SyntaxFactory.Token(SyntaxKind.EndOfFile, "", Array.Empty<RazorDiagnostic>()));
}
}
//
// This will be called by the code parser whenever any markup is encountered inside a code block @{}.
// It can either be a single line markup like @: or a tag. In this case, we want to keep track of tag nesting
// and add appropriate errors for any malformed cases.
// In addition to parsing regular tags, we also understand special "text" tags. These tags are not rendered to the output
// but used to render a block of text it encloses as markup. They are a multiline alternative to the single line markup syntax @:
// One caveat is that the tags in single markup as parsed as plain text.
//
// The tag stack inside a code block is different from the stack outside the block.
// E.g, `<div> @{ </div> }` will be parsed as two separate elements with missing end and start tags respectively.
//
public MarkupBlockSyntax? ParseBlock()
{
CancellationToken.ThrowIfCancellationRequested();
if (Context == null)
{
throw new InvalidOperationException(Resources.Parser_Context_Not_Set);
}
var oldTagTracker = _tagTracker;
try
{
// This is the start of a new block. We don't want the current tag stack to mix with the tags in this block.
// Initialize a new stack.
_tagTracker = new Stack<TagTracker>();
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
using (PushSpanContextConfig(DefaultMarkupSpanContext))
{
var builder = pooledResult.Builder;
if (!NextToken())
{
return null;
}
AcceptWhile(IsSpacingTokenIncludingNewLines);
builder.Add(OutputAsMarkupLiteral());
if (At(SyntaxKind.OpenAngle))
{
ParseMarkupInCodeBlock(builder);
}
else if (At(SyntaxKind.Transition))
{
ParseMarkupTransition(builder);
}
else
{
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_MarkupBlockMustStartWithTag(
new SourceSpan(CurrentStart, CurrentToken.Content.Length)));
}
// Add any remaining tokens to the builder.
builder.Add(OutputAsMarkupLiteral());
var markupBlock = builder.ToList();
return SyntaxFactory.MarkupBlock(markupBlock);
}
}
finally
{
_tagTracker = oldTagTracker;
}
}
//
// This is called when the body of a Razor block directive needs to be parsed. E.g @section |{ ... }|
// This parses markup in 'document' mode, which means we don't add any errors for malformed tags.
// Since, a razor block can also have several code blocks @{} within it, we need to keep track of the block nesting level
// to make sure we exit when we reach the final '}'.
//
// Similar to ParseBlock, the tag stack inside a razor block is different from the stack outside the block.
// E.g, `@section Foo { </div> } <div>` will be parsed as two separate elements.
//
public MarkupBlockSyntax ParseRazorBlock(Tuple<string, string> nestingSequences, bool caseSensitive)
{
CancellationToken.ThrowIfCancellationRequested();
if (Context == null)
{
throw new InvalidOperationException(Resources.Parser_Context_Not_Set);
}
var oldTagTracker = _tagTracker;
try
{
// This is the start of a new block. We don't want the current tag stack to mix with the tags in this block.
// Initialize a new stack.
_tagTracker = new Stack<TagTracker>();
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
using (PushSpanContextConfig(DefaultMarkupSpanContext))
{
var builder = pooledResult.Builder;
NextToken();
CaseSensitive = caseSensitive;
NestingBlock(builder, nestingSequences);
AcceptMarkerTokenIfNecessary();
builder.Add(OutputAsMarkupLiteral());
return SyntaxFactory.MarkupBlock(builder.ToList());
}
}
finally
{
_tagTracker = oldTagTracker;
}
}
private void ParseMarkupNodes(
in SyntaxListBuilder<RazorSyntaxNode> builder,
ParseMode mode,
Func<SyntaxToken, bool>? stopCondition = null)
{
CancellationToken.ThrowIfCancellationRequested();
stopCondition = stopCondition ?? (token => false);
while (!EndOfFile && !stopCondition(CurrentToken))
{
ParseMarkupNode(builder, mode);
}
}
private void ParseMarkupNode(in SyntaxListBuilder<RazorSyntaxNode> builder, ParseMode mode)
{
CancellationToken.ThrowIfCancellationRequested();
switch (GetParserState(mode))
{
case ParserState.MarkupText:
ParseMarkupText(builder);
break;
case ParserState.Tag:
ParseMarkupElement(builder, mode);
break;
case ParserState.SpecialTag:
ParseSpecialTag(builder);
break;
case ParserState.XmlPI:
ParseXmlPI(builder);
break;
case ParserState.CData:
ParseCData(builder);
break;
case ParserState.MarkupComment:
ParseMarkupComment(builder);
break;
case ParserState.RazorComment:
ParseRazorCommentWithLeadingAndTrailingWhitespace(builder);
break;
case ParserState.DoubleTransition:
ParseDoubleTransition(builder);
break;
case ParserState.CodeTransition:
ParseCodeTransition(builder);
break;
case ParserState.Misc:
ParseMisc(builder);
break;
case ParserState.Unknown:
AcceptAndMoveNext();
break;
case ParserState.EOF:
builder.Add(OutputAsMarkupLiteral());
break;
}
}
private void ParseMarkupText(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
AcceptAndMoveNext();
}
private void ParseMarkupInCodeBlock(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
do
{
CancellationToken.ThrowIfCancellationRequested();
switch (GetParserState(ParseMode.MarkupInCodeBlock))
{
case ParserState.EOF:
break;
case ParserState.Tag:
ParseMarkupElement(builder, ParseMode.MarkupInCodeBlock);
break;
case ParserState.SpecialTag:
case ParserState.XmlPI:
case ParserState.MarkupComment:
case ParserState.CData:
ParseMarkupNode(builder, ParseMode.MarkupInCodeBlock);
SetAcceptedCharacters(AcceptedCharactersInternal.None);
builder.Add(OutputAsMarkupLiteral());
break;
default:
ParseMarkupNode(builder, ParseMode.Text);
break;
}
}
while (!EndOfFile && _tagTracker.Count > 0);
CompleteMarkupInCodeBlock(builder);
}
private void CompleteMarkupInCodeBlock(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
// Output anything we've accepted so far.
builder.Add(OutputAsMarkupLiteral());
var isOuterTagWellFormed = true;
while (_tagTracker.Count > 0)
{
var tracker = _tagTracker.Pop();
var element = SyntaxFactory.MarkupElement(tracker.StartTag, builder.Consume(), markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(element);
if (_tagTracker.Count == 0)
{
isOuterTagWellFormed = tracker.IsWellFormed;
if (isOuterTagWellFormed)
{
// We're at the outermost start tag. Add an error.
// We don't want to add this error if the tag is unfinished. A different error would have already been added.
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_MissingEndTag(
new SourceSpan(
SourceLocationTracker.Advance(tracker.TagLocation, "<"),
tracker.TagName.Length),
tracker.TagName));
}
}
}
if (!Context.DesignTimeMode)
{
// We want to accept the whitespace and newline at the end of the markup.
// E.g,
// @{
// <div>Foo</div>|
// |}
// Except in two cases,
// 1. Design time
// 2. Text tags
//
var shouldAcceptWhitespaceAndNewLine = true;
// Check if the previous span was a transition.
var previousSpan = builder.Count > 0 ? GetLastSpan(builder[builder.Count - 1]) : null;
if (previousSpan != null &&
((previousSpan is MarkupStartTagSyntax startTag && startTag.IsMarkupTransition) ||
(previousSpan is MarkupEndTagSyntax endTag && endTag.IsMarkupTransition)))
{
using var tokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static f => (f.Kind == SyntaxKind.Whitespace) || (f.Kind == SyntaxKind.NewLine),
ref tokens.AsRef());
// Make sure the current token is not markup, which can be html start tag or @:
if (!(At(SyntaxKind.OpenAngle) ||
(At(SyntaxKind.Transition) && Lookahead(count: 1).Content.StartsWith(":", StringComparison.Ordinal))))
{
// Don't accept whitespace as markup if the end text tag is followed by csharp.
shouldAcceptWhitespaceAndNewLine = false;
}
PutCurrentBack();
PutBack(in tokens);
EnsureCurrent();
}
if (shouldAcceptWhitespaceAndNewLine)
{
// Accept whitespace and a single newline if present
AcceptWhile(SyntaxKind.Whitespace);
TryAccept(SyntaxKind.NewLine);
if (isOuterTagWellFormed)
{
// Completed tags have no accepted characters inside blocks.
SetAcceptedCharacters(AcceptedCharactersInternal.None);
}
}
}
PutCurrentBack();
if (!isOuterTagWellFormed)
{
AcceptMarkerTokenIfNecessary();
}
builder.Add(OutputAsMarkupLiteral());
}
private void ParseMarkupTransition(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
Assert(SyntaxKind.Transition);
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
chunkGenerator = SpanChunkGenerator.Null;
var transition = SyntaxFactory.MarkupTransition(Output(), chunkGenerator, GetEditHandler());
builder.Add(transition);
// "@:" => Explicit Single Line Block
if (CurrentToken.Kind == SyntaxKind.Text && CurrentToken.Content.Length > 0 && CurrentToken.Content[0] == ':')
{
// Split the token
var split = Language.SplitToken(CurrentToken, 1, SyntaxKind.Colon);
// The first part (left) is output as MetaCode
Accept(split.Item1);
chunkGenerator = SpanChunkGenerator.Null;
builder.Add(OutputAsMetaCode(Output(), AcceptedCharactersInternal.Any));
if (split.Item2 != null)
{
Accept(split.Item2);
}
NextToken();
ParseSingleLineMarkup(builder);
}
else if (CurrentToken.Kind == SyntaxKind.OpenAngle)
{
// Template
// E.g, @<div>Foo</div>
ParseMarkupInCodeBlock(builder);
}
}
private void ParseSingleLineMarkup(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
// Parse until a newline.
// First, signal to code parser that whitespace is significant to us.
var old = Context.WhiteSpaceIsSignificantToAncestorBlock;
Context.WhiteSpaceIsSignificantToAncestorBlock = true;
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
if (editHandlerBuilder != null)
{
editHandlerBuilder.Reset();
editHandlerBuilder.Tokenizer = LanguageTokenizeString;
}
// Now parse until a new line.
do
{
ParseMarkupNodes(builder, ParseMode.Text, token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
if (At(SyntaxKind.Whitespace))
{
AcceptAndMoveNext();
}
} while (!EndOfFile && CurrentToken.Kind != SyntaxKind.NewLine);
// Code block inside single-line markup transition (`@: @{ }`)
// does not swallow trailing whitespace.
Context.NullGenerateWhitespaceAndNewLine = false;
if (!EndOfFile && CurrentToken.Kind == SyntaxKind.NewLine)
{
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
}
PutCurrentBack();
Context.WhiteSpaceIsSignificantToAncestorBlock = old;
builder.Add(OutputAsMarkupLiteral());
}
private void ParseMarkupElement(in SyntaxListBuilder<RazorSyntaxNode> builder, ParseMode mode)
{
CancellationToken.ThrowIfCancellationRequested();
Assert(SyntaxKind.OpenAngle);
// Output already accepted tokens if any.
builder.Add(OutputAsMarkupLiteral());
if (!NextIs(SyntaxKind.ForwardSlash))
{
// Parsing a start tag
var tagStart = CurrentStart;
var startTag = ParseStartTag(mode, tagStart, out var tagName, out var tagMode, out var isWellFormed);
if (tagMode == MarkupTagMode.Script)
{
var acceptedCharacters = mode == ParseMode.MarkupInCodeBlock ? AcceptedCharactersInternal.None : AcceptedCharactersInternal.Any;
ParseJavascriptAndEndScriptTag(builder, startTag, acceptedCharacters);
return;
}
if (tagMode == MarkupTagMode.SelfClosing || tagMode == MarkupTagMode.Invalid || tagMode == MarkupTagMode.Void)
{
// For cases like <foo />, <input> or invalid cases like |<|<p>
var element = SyntaxFactory.MarkupElement(startTag, EmptySyntaxList, markupEndTag: null);
builder.Add(element);
return;
}
else
{
// This is a normal start tag. We need to keep track of it.
var tracker = new TagTracker(tagName, startTag, tagStart, builder.Consume(), isWellFormed);
_tagTracker.Push(tracker);
return;
}
}
else
{
// Parsing an end tag.
var endTagStart = CurrentStart;
var endTag = ParseEndTag(mode, out var endTagName, out _);
if (string.Equals(CurrentStartTagName, endTagName, StringComparison.OrdinalIgnoreCase))
{
// Happy path. Found a matching start tag. Create the element and reset the builder.
var tracker = _tagTracker.Pop();
var element = SyntaxFactory.MarkupElement(tracker.StartTag, builder.Consume(), endTag);
builder.AddRange(tracker.PreviousNodes);
builder.Add(element);
return;
}
else
{
// Current tag scope does not match the end tag. Attempt to recover the start tag
// by looking up the previous tag scopes for a matching start tag.
if (!TryRecoverStartTag(builder, endTagName, endTag))
{
// Could not recover.
var element = SyntaxFactory.MarkupElement(markupStartTag: null, body: EmptySyntaxList, markupEndTag: endTag);
builder.Add(element);
if (mode == ParseMode.MarkupInCodeBlock)
{
CompleteEndTag(builder, endTagName, endTagStart, endTag);
}
}
}
}
}
private void CompleteEndTag(
in SyntaxListBuilder<RazorSyntaxNode> builder,
string endTagName,
SourceLocation endTagStartLocation,
MarkupEndTagSyntax endTag)
{
// At this point we already know we don't have a matching start tag. Just build whatever is left.
if (_tagTracker.Count == 0)
{
// We can't possibly have a matching start tag.
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_UnexpectedEndTag(
new SourceSpan(SourceLocationTracker.Advance(endTagStartLocation, "</"), Math.Max(endTagName.Length, 1)), endTagName));
return;
}
while (_tagTracker.Count > 0)
{
var tracker = _tagTracker.Pop();
var unclosedElement = SyntaxFactory.MarkupElement(tracker.StartTag, builder.Consume(), markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(unclosedElement);
if (_tagTracker.Count == 0)
{
// This means we couldn't find a match and we're at the outermost start tag. Add an error.
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_MissingEndTag(
new SourceSpan(
SourceLocationTracker.Advance(tracker.TagLocation, "<"),
tracker.TagName.Length),
tracker.TagName));
}
}
}
private bool TryRecoverStartTag(in SyntaxListBuilder<RazorSyntaxNode> builder, string endTagName, MarkupEndTagSyntax endTag)
{
// First check if the tag we're tracking is a void tag. If so, we need to close it out before moving on.
while (_tagTracker.Count > 0 &&
!string.Equals(CurrentStartTagName, endTagName, StringComparison.OrdinalIgnoreCase) &&
IsVoidElement(CurrentStartTagName))
{
var tracker = _tagTracker.Pop();
var children = builder.Consume();
var voidElement = SyntaxFactory.MarkupElement(tracker.StartTag, EmptySyntaxList, markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(voidElement);
builder.AddRange(children);
}
var malformedTagCount = 0;
foreach (var tag in _tagTracker)
{
if (string.Equals(tag.TagName, endTagName, StringComparison.OrdinalIgnoreCase))
{
break;
}
malformedTagCount++;
}
if (malformedTagCount != _tagTracker.Count)
{
// This means we found a matching tag.
for (var i = 0; i < malformedTagCount; i++)
{
var tracker = _tagTracker.Pop();
var malformedElement = SyntaxFactory.MarkupElement(tracker.StartTag, builder.Consume(), markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(malformedElement);
}
// Now complete our target tag which is not malformed.
var tagTracker = _tagTracker.Pop();
var element = SyntaxFactory.MarkupElement(tagTracker.StartTag, builder.Consume(), endTag);
builder.AddRange(tagTracker.PreviousNodes);
builder.Add(element);
return true;
}
return false;
}
private MarkupStartTagSyntax ParseStartTag(
ParseMode mode,
SourceLocation tagStartLocation,
out string tagName,
out MarkupTagMode tagMode,
out bool isWellFormed)
{
Assert(SyntaxKind.OpenAngle);
tagName = string.Empty;
tagMode = MarkupTagMode.Normal;
isWellFormed = false;
var openAngleToken = EatCurrentToken(); // Accept '<'
var isBangEscape = TryParseBangEscape(out var bangToken);
if (At(SyntaxKind.Text))
{
tagName = CurrentToken.Content;
if (isBangEscape)
{
// We don't want to group <p> and </!p> together.
tagName = "!" + tagName;
}
}
if (mode == ParseMode.MarkupInCodeBlock &&
_tagTracker.Count == 0 &&
string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
{
// "<text>" tag is special only if it is the outermost tag.
return ParseStartTextTag(openAngleToken, out tagMode, out isWellFormed);
}
var tagNameToken = At(SyntaxKind.Text) ? EatCurrentToken() : SyntaxFactory.MissingToken(SyntaxKind.Text);
var attributes = EmptySyntaxList;
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var attributeBuilder = pooledResult.Builder;
// Parse the contents of a tag like attributes.
ParseAttributes(attributeBuilder);
attributes = attributeBuilder.ToList();
}
SyntaxToken? forwardSlashToken = null;
if (At(SyntaxKind.ForwardSlash))
{
// This is a self closing tag.
tagMode = MarkupTagMode.SelfClosing;
forwardSlashToken = EatCurrentToken();
}
var closeAngleToken = SyntaxFactory.MissingToken(SyntaxKind.CloseAngle);
if (mode == ParseMode.MarkupInCodeBlock)
{
if (EndOfFile || !At(SyntaxKind.CloseAngle))
{
// Unfinished tag
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_UnfinishedTag(
new SourceSpan(
tagName.Length == 0 ? tagStartLocation : SourceLocationTracker.Advance(tagStartLocation, "<"),
Math.Max(tagName.Length, 1)),
tagName));
}
else
{
if (At(SyntaxKind.CloseAngle))
{
isWellFormed = true;
closeAngleToken = EatCurrentToken();
}
// Completed tags in code blocks have no accepted characters.
SetAcceptedCharacters(AcceptedCharactersInternal.None);
if (tagMode != MarkupTagMode.SelfClosing && IsVoidElement(tagName))
{
// This is a void element.
// Technically, void elements like "meta" are not allowed to have end tags. Just in case they do,
// we need to look ahead at the next set of tokens.
// Place a bookmark
var bookmark = CurrentStart.AbsoluteIndex;
// Skip whitespace
SkipWhile(IsSpacingTokenIncludingNewLines);
// Open Angle
if (At(SyntaxKind.OpenAngle) && NextIs(SyntaxKind.ForwardSlash))
{
NextToken();
Assert(SyntaxKind.ForwardSlash);
NextToken();
if (!At(SyntaxKind.Text) || !string.Equals(CurrentToken.Content, tagName, StringComparison.OrdinalIgnoreCase))
{
// There is no matching end void tag.
tagMode = MarkupTagMode.Void;
}
}
else
{
// There is no matching end void tag.
tagMode = MarkupTagMode.Void;
}
// Go back to the bookmark and just finish this tag at the close angle
Context.Source.Position = bookmark;
NextToken();
}
}
}
else if (At(SyntaxKind.CloseAngle))
{
isWellFormed = true;
closeAngleToken = EatCurrentToken();
}
// End tag block
var startTag = SyntaxFactory.MarkupStartTag(
openAngleToken,
bangToken,
tagNameToken,
attributes,
forwardSlashToken,
closeAngleToken,
isMarkupTransition: false,
chunkGenerator,
GetEditHandler());
if (string.Equals(tagName, ScriptTagName, StringComparison.OrdinalIgnoreCase))
{
// If the script tag expects javascript content then we should do minimal parsing until we reach
// the end script tag. Don't want to incorrectly parse a "var tag = '<input />';" as an HTML tag.
if (!ScriptTagExpectsHtml(startTag))
{
tagMode = MarkupTagMode.Script;
}
}
if (tagNameToken.IsMissing && closeAngleToken.IsMissing)
{
// We want to consider tags with no name and no closing angle as invalid.
// E.g,
// <, < @DateTime.Now are all invalid tags
// <>, < @DateTime.Now>, <strong are all still valid.
tagMode = MarkupTagMode.Invalid;
}
return startTag;
}
private MarkupStartTagSyntax ParseStartTextTag(SyntaxToken openAngleToken, out MarkupTagMode tagMode, out bool isWellFormed)
{
// At this point, we should have already accepted the open angle. We won't get here if the tag is escaped.
tagMode = MarkupTagMode.Normal;
var textLocation = CurrentStart;
Assert(SyntaxKind.Text);
var tagNameToken = EatCurrentToken();
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var miscAttributeContentBuilder = pooledResult.Builder;
SyntaxToken? forwardSlashToken = null;
SyntaxToken? closeAngleToken = null;
AcceptWhile(IsSpacingToken);
miscAttributeContentBuilder.Add(OutputAsMarkupLiteral());
if (At(SyntaxKind.CloseAngle) ||
(At(SyntaxKind.ForwardSlash) && NextIs(SyntaxKind.CloseAngle)))
{
if (At(SyntaxKind.ForwardSlash))
{
tagMode = MarkupTagMode.SelfClosing;
forwardSlashToken = EatCurrentToken();
}
closeAngleToken = EatCurrentToken();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
}
else
{
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_TextTagCannotContainAttributes(
new SourceSpan(textLocation, contentLength: 4 /* text */)));
RecoverTextTag(out var miscContent, out closeAngleToken);
miscAttributeContentBuilder.Add(miscContent);
}
isWellFormed = true;
chunkGenerator = SpanChunkGenerator.Null;
return SyntaxFactory.MarkupStartTag(
openAngleToken,
bang: null,
name: tagNameToken,
attributes: miscAttributeContentBuilder.ToList(),
forwardSlash: forwardSlashToken,
closeAngle: closeAngleToken,
isMarkupTransition: true,
chunkGenerator,
GetEditHandler());
}
}
private void RecoverTextTag(out MarkupTextLiteralSyntax? miscContent, out SyntaxToken closeAngleToken)
{
// We don't want to skip-to and parse because there shouldn't be anything in the body of text tags.
AcceptUntil(SyntaxKind.CloseAngle, SyntaxKind.NewLine);
miscContent = OutputAsMarkupLiteral();
// Include the close angle in the text tag block if it's there, otherwise just move on
if (At(SyntaxKind.CloseAngle))
{
closeAngleToken = EatCurrentToken();
}
else
{
closeAngleToken = SyntaxFactory.MissingToken(SyntaxKind.CloseAngle);
}
}
private MarkupEndTagSyntax ParseEndTag(ParseMode mode, out string tagName, out bool isWellFormed)
{
// This section can accept things like: '</p >' or '</p>' etc.
Assert(SyntaxKind.OpenAngle);
tagName = string.Empty;
SyntaxToken tagNameToken;
var openAngleToken = EatCurrentToken(); // Accept '<'
var forwardSlashToken = At(SyntaxKind.ForwardSlash) ? EatCurrentToken() : SyntaxFactory.MissingToken(SyntaxKind.ForwardSlash);
// Whitespace here is invalid (according to the spec)
var isBangEscape = TryParseBangEscape(out var bangToken);
if (At(SyntaxKind.Text))
{
tagName = isBangEscape ? "!" : string.Empty;
tagName += CurrentToken.Content;
if (mode == ParseMode.MarkupInCodeBlock &&
string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
{
// "<text>" tag is special only if it is the outermost tag. We need to figure out if the current end text tag
// matches the outermost start text tag.
var openTextTagCount = 0;
foreach (var tracker in _tagTracker)
{
if (string.Equals(tracker.TagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
{
openTextTagCount++;
}
}
if (openTextTagCount == 1 &&
string.Equals(_tagTracker.Last().TagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
{
// This means there is only one open text tag and it is the outermost tag.
return ParseEndTextTag(openAngleToken, forwardSlashToken, out isWellFormed);
}
}
tagNameToken = EatCurrentToken();
}
else
{
tagNameToken = SyntaxFactory.MissingToken(SyntaxKind.Text);
}
SyntaxToken closeAngleToken;
MarkupMiscAttributeContentSyntax? miscAttributeContent = null;
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var miscAttributeBuilder = pooledResult.Builder;
AcceptWhile(SyntaxKind.Whitespace);
miscAttributeBuilder.Add(OutputAsMarkupLiteral());
if (mode == ParseMode.MarkupInCodeBlock)
{
// We want to accept malformed end tags as content.
AcceptUntil(SyntaxKind.CloseAngle, SyntaxKind.OpenAngle);
miscAttributeBuilder.Add(OutputAsMarkupLiteral());
if (At(SyntaxKind.CloseAngle))
{
// Completed tags in code blocks have no accepted characters.
SetAcceptedCharacters(AcceptedCharactersInternal.None);
}
}
if (miscAttributeBuilder.Count > 0)
{
miscAttributeContent = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeBuilder.ToList());
}
}
if (At(SyntaxKind.CloseAngle))
{
isWellFormed = true;
closeAngleToken = EatCurrentToken();
}
else
{
isWellFormed = false;
closeAngleToken = SyntaxFactory.MissingToken(SyntaxKind.CloseAngle);
}
// End tag block
return SyntaxFactory.MarkupEndTag(
openAngleToken,
forwardSlashToken,
bangToken,
tagNameToken,
miscAttributeContent,
closeAngleToken,
isMarkupTransition: false,
chunkGenerator,
GetEditHandler());
}
private MarkupEndTagSyntax ParseEndTextTag(SyntaxToken openAngleToken, SyntaxToken forwardSlashToken, out bool isWellFormed)
{
// At this point, we should have already accepted the open angle and forward slash. We won't get here if the tag is escaped.
var textLocation = CurrentStart;
Assert(SyntaxKind.Text);
var tagNameToken = EatCurrentToken();
MarkupMiscAttributeContentSyntax? miscAttributeContent = null;
SyntaxToken? closeAngleToken = null;
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var miscAttributeBuilder = pooledResult.Builder;
isWellFormed = At(SyntaxKind.CloseAngle);
if (!isWellFormed)
{
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_TextTagCannotContainAttributes(
new SourceSpan(textLocation, contentLength: 4 /* text */)));
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
RecoverTextTag(out var miscContent, out closeAngleToken);
miscAttributeBuilder.Add(miscContent);
}
else
{
SetAcceptedCharacters(AcceptedCharactersInternal.None);
closeAngleToken = EatCurrentToken();
}
if (miscAttributeBuilder.Count > 0)
{
miscAttributeContent = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeBuilder.ToList());
}
}
chunkGenerator = SpanChunkGenerator.Null;
return SyntaxFactory.MarkupEndTag(
openAngleToken,
forwardSlashToken,
bang: null,
name: tagNameToken,
miscAttributeContent: miscAttributeContent,
closeAngle: closeAngleToken,
isMarkupTransition: true,
chunkGenerator,
GetEditHandler());
}
private void ParseAttributes(in SyntaxListBuilder<RazorSyntaxNode> 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 ($): <tag$ foo="bar" biz="~/Baz" />
while (!EndOfFile && !IsEndOfTag())
{
if (At(SyntaxKind.ForwardSlash))
{
// This means we're at a '/' but it's not considered end of tag. E.g. <p / class=foo>
// 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<RazorSyntaxNode> builder)
{
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
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<RazorSyntaxNode> builder)
{
// Output anything prior to the attribute, in most cases this will be any invalid content after the tag name or a previous attribute:
// <input| /| checked />. If there is nothing in-between other attributes this will noop.
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var miscAttributeContentBuilder = pooledResult.Builder;
miscAttributeContentBuilder.Add(OutputAsMarkupLiteral());
if (miscAttributeContentBuilder.Count > 0)
{
var invalidAttributeBlock = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeContentBuilder.ToList());
builder.Add(invalidAttributeBlock);
}
}
// https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state
// Capture whitespace
using var attributePrefixWhitespace = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine,
ref attributePrefixWhitespace.AsRef());
// https://html.spec.whatwg.org/multipage/parsing.html#attribute-name-state
// Read the 'name' (i.e. read until the '=' or whitespace/newline)
using var nameTokens = new PooledArrayBuilder<SyntaxToken>();
var nameParsingResult = TryParseAttributeName(out SyntaxToken? ephemeralToken, ref nameTokens.AsRef());
switch (nameParsingResult)
{
// Parse C# and return to attribute parsing afterwards.
case AttributeNameParsingResult.CSharp:
{
Accept(in attributePrefixWhitespace);
PutCurrentBack();
using var pooledResult = Pool.Allocate<RazorSyntaxNode>();
var dynamicAttributeValueBuilder = pooledResult.Builder;
OtherParserBlock(dynamicAttributeValueBuilder);
var value = SyntaxFactory.MarkupMiscAttributeContent(dynamicAttributeValueBuilder.ToList());
builder.Add(value);
return;
}
// Parse razor comment and return to attribute parsing afterwards.
case AttributeNameParsingResult.RazorComment:
{
Accept(in attributePrefixWhitespace);
PutCurrentBack();
ParseRazorCommentWithLeadingAndTrailingWhitespace(builder);
return;
}
// Unexpected character in tag, enter recovery
case AttributeNameParsingResult.Other:
{
Accept(in attributePrefixWhitespace);
ParseMiscAttribute(builder);
return;
}
}
Debug.Assert(nameParsingResult is AttributeNameParsingResult.Success);
Accept(in attributePrefixWhitespace); // Whitespace before attribute name
var namePrefix = OutputAsMarkupLiteral();
if (ephemeralToken is not null)
{
builder.Add(namePrefix);
Accept(ephemeralToken);
builder.Add(OutputAsMarkupEphemeralLiteral());
namePrefix = null;
}
Accept(in nameTokens); // Attribute name
var name = OutputAsMarkupLiteralRequired();
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 enum AttributeNameParsingResult
{
Success,
Other,
CSharp,
RazorComment,
}
private AttributeNameParsingResult TryParseAttributeName(out SyntaxToken? ephemeralToken, ref PooledArrayBuilder<SyntaxToken> nameTokens)
{
ephemeralToken = null;
//
// We are currently here <input |name="..." />
// If we encounter a transition (@) here, it can be parsed as CSharp or Markup depending on the feature flag.
// For example, in Components, we want to parse it as Markup so we can support directive attributes.
//
if (Context.Options.AllowCSharpInMarkupAttributeArea)
{
if (At(SyntaxKind.Transition))
{
if (NextIs(SyntaxKind.Transition))
{
// The attribute name is escaped (@@), skip the first @ sign.
ephemeralToken = CurrentToken;
NextToken();
// And accept the second @ sign.
Debug.Assert(nameTokens.Count == 0);
nameTokens.Add(CurrentToken);
NextToken();
}
else
{
// There is CSharp in the attribute area. Don't try to parse the name.
return AttributeNameParsingResult.CSharp;
}
}
else if (At(SyntaxKind.RazorCommentTransition))
{
// There is razor comment in the attribute area. Don't try to parse the name.
return AttributeNameParsingResult.RazorComment;
}
}
if (ephemeralToken is not null || IsValidAttributeNameToken(CurrentToken))
{
ReadWhile(
static (token, self) =>
token.Kind != SyntaxKind.Whitespace &&
token.Kind != SyntaxKind.NewLine &&
token.Kind != SyntaxKind.Equals &&
token.Kind != SyntaxKind.CloseAngle &&
token.Kind != SyntaxKind.OpenAngle &&
(token.Kind != SyntaxKind.Transition || !self.Context.Options.AllowCSharpInMarkupAttributeArea) &&
(token.Kind != SyntaxKind.ForwardSlash || !self.NextIs(SyntaxKind.CloseAngle)),
this,
ref nameTokens,
expectsEmptyBuilder: ephemeralToken is null);
return AttributeNameParsingResult.Success;
}
return AttributeNameParsingResult.Other;
}
private MarkupAttributeBlockSyntax ParseRemainingAttribute(MarkupTextLiteralSyntax? namePrefix, MarkupTextLiteralSyntax name)
{
// Since this is not a minimized attribute, the whitespace after attribute name belongs to this attribute.
AcceptWhile(static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine);
var nameSuffix = OutputAsMarkupLiteral();
Assert(SyntaxKind.Equals); // We should be at "="
var equalsToken = EatCurrentToken();
using var whitespaceAfterEquals = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine,
ref whitespaceAfterEquals.AsRef());
var quote = SyntaxKind.Marker;
if (At(SyntaxKind.SingleQuote) || At(SyntaxKind.DoubleQuote))
{
// Found a quote, the whitespace belongs to this attribute.
Accept(in 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(in 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)
string nameContent;
using (StringBuilderPool.GetPooledObject(out var builder))
{
foreach (var node in name.LiteralTokens.Nodes)
{
builder.Append(node.Content);
}
nameContent = builder.ToString();
}
if (IsConditionalAttributeName(nameContent))
{
// 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<RazorSyntaxNode>())
{
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();
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<RazorSyntaxNode>())
{
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<RazorSyntaxNode> builder, SyntaxKind quote)
{
var prefixStart = CurrentStart;
using var prefixTokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static token => token.Kind == SyntaxKind.Whitespace || token.Kind == SyntaxKind.NewLine,
ref prefixTokens.AsRef());
if (At(SyntaxKind.Transition))
{
if (NextIs(SyntaxKind.Transition))
{
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var markupBuilder = pooledResult.Builder;
Accept(in prefixTokens);
// Render a single "@" in place of "@@".
string prefixContent;
using (var pooledBuilder = StringBuilderPool.GetPooledObject())
{
var prefixBuilder = pooledBuilder.Object;
foreach (var prefixToken in prefixTokens)
{
prefixBuilder.Append(prefixToken.Content);
}
prefixContent = prefixBuilder.ToString();
}
chunkGenerator = new LiteralAttributeChunkGenerator(
new LocationTagged<string>(prefixContent, prefixStart),
new LocationTagged<string>(CurrentToken.Content, CurrentStart));
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
markupBuilder.Add(OutputAsMarkupLiteral());
chunkGenerator = SpanChunkGenerator.Null;
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
markupBuilder.Add(OutputAsMarkupEphemeralLiteral());
var markupBlock = SyntaxFactory.MarkupBlock(markupBuilder.ToList());
builder.Add(markupBlock);
}
}
else
{
Accept(in prefixTokens);
var valueStart = CurrentStart;
PutCurrentBack();
var prefix = OutputAsMarkupLiteral();
// Dynamic value, start a new block and set the chunk generator
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var dynamicAttributeValueBuilder = pooledResult.Builder;
OtherParserBlock(dynamicAttributeValueBuilder);
var value = SyntaxFactory.MarkupDynamicAttributeValue(prefix, SyntaxFactory.GenericBlock(dynamicAttributeValueBuilder.ToList()));
builder.Add(value);
}
}
}
else
{
Accept(in prefixTokens);
var prefix = OutputAsMarkupLiteral();
// Literal value
// 'quote' should be "Unknown" if not quoted and tokens coming from the tokenizer should never have
// "Unknown" type.
using var valueTokens = new PooledArrayBuilder<SyntaxToken>();
ReadWhile(
static (token, arg) =>
// 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)
!arg.self.IsEndOfAttributeValue(arg.quote, token),
(self: this, quote),
ref valueTokens.AsRef());
Accept(in 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:
// https://html.spec.whatwg.org/multipage/parsing.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<RazorSyntaxNode> builder, MarkupStartTagSyntax startTag, AcceptedCharactersInternal endTagAcceptedCharacters = AcceptedCharactersInternal.Any)
{
var previousNodes = builder.Consume();
// Special case for <script>: Skip to end of script tag and parse code
var seenEndScript = false;
while (!seenEndScript && !EndOfFile)
{
ParseMarkupNodes(builder, ParseMode.Text, token => token.Kind == SyntaxKind.OpenAngle);
var tagStart = CurrentStart;
if (NextIs(SyntaxKind.ForwardSlash))
{
var openAngle = CurrentToken;
NextToken(); // Skip over '<', current is '/'
var solidus = CurrentToken;
NextToken(); // Skip over '/', current should be text
if (At(SyntaxKind.Text) &&
string.Equals(CurrentToken.Content, ScriptTagName, StringComparison.OrdinalIgnoreCase))
{
seenEndScript = true;
}
// We put everything back because we just wanted to look ahead to see if the current end tag that we're parsing is
// the script tag. If so we'll generate correct code to encompass it.
PutCurrentBack(); // Put back whatever was after the solidus
PutBack(solidus); // Put back '/'
PutBack(openAngle); // Put back '<'
// We just looked ahead, this NextToken will set CurrentToken to an open angle bracket.
NextToken();
}
if (!seenEndScript)
{
AcceptAndMoveNext(); // Accept '<' (not the closing script tag's open angle)
}
}
MarkupEndTagSyntax? endTag = null;
if (seenEndScript)
{
var tagStart = CurrentStart;
builder.Add(OutputAsMarkupLiteral());
var openAngleToken = EatCurrentToken(); // '<'
var forwardSlashToken = EatCurrentToken(); // '/'
var tagNameToken = EatCurrentToken(); // 'script'
MarkupMiscAttributeContentSyntax? miscContent = null;
SyntaxToken? closeAngleToken = null;
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var miscAttributeBuilder = pooledResult.Builder;
// We want to accept malformed end tags as content.
AcceptUntil(SyntaxKind.CloseAngle, SyntaxKind.OpenAngle);
miscAttributeBuilder.Add(OutputAsMarkupLiteral());
if (miscAttributeBuilder.Count > 0)
{
miscContent = SyntaxFactory.MarkupMiscAttributeContent(miscAttributeBuilder.ToList());
}
if (!At(SyntaxKind.CloseAngle))
{
Context.ErrorSink.OnError(
RazorDiagnosticFactory.CreateParsing_UnfinishedTag(
new SourceSpan(SourceLocationTracker.Advance(tagStart, "</"), ScriptTagName.Length),
ScriptTagName));
closeAngleToken = SyntaxFactory.MissingToken(SyntaxKind.CloseAngle);
}
else
{
closeAngleToken = EatCurrentToken();
}
}
SetAcceptedCharacters(endTagAcceptedCharacters);
endTag = SyntaxFactory.MarkupEndTag(
openAngleToken,
forwardSlashToken,
bang: null,
name: tagNameToken,
miscAttributeContent: miscContent,
closeAngle: closeAngleToken,
isMarkupTransition: false,
chunkGenerator,
GetEditHandler());
}
var element = SyntaxFactory.MarkupElement(startTag, builder.Consume(), endTag);
builder.AddRange(previousNodes);
builder.Add(element);
}
private bool ParseSpecialTag(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
// Clear the current token builder.
builder.Add(OutputAsMarkupLiteral());
return AcceptTokenUntilAll(builder, SyntaxKind.CloseAngle);
}
private bool ParseXmlPI(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
Assert(SyntaxKind.OpenAngle);
AcceptAndMoveNext();
Assert(SyntaxKind.QuestionMark);
AcceptAndMoveNext();
return AcceptTokenUntilAll(builder, SyntaxKind.QuestionMark, SyntaxKind.CloseAngle);
}
private bool ParseCData(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
// <![CDATA[...]]>
Assert(SyntaxKind.OpenAngle);
AcceptAndMoveNext(); // '<'
AcceptAndMoveNext(); // '!'
AcceptAndMoveNext(); // '['
Debug.Assert(CurrentToken.Kind == SyntaxKind.Text && string.Equals(CurrentToken.Content, "cdata", StringComparison.OrdinalIgnoreCase));
AcceptAndMoveNext();
Assert(SyntaxKind.LeftBracket);
return AcceptTokenUntilAll(builder, SyntaxKind.RightBracket, SyntaxKind.RightBracket, SyntaxKind.CloseAngle);
}
private void ParseDoubleTransition(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
AcceptWhile(IsSpacingTokenIncludingNewLines);
builder.Add(OutputAsMarkupLiteral());
// First transition
Assert(SyntaxKind.Transition);
AcceptAndMoveNext();
chunkGenerator = SpanChunkGenerator.Null;
builder.Add(OutputAsMarkupEphemeralLiteral());
// Second transition
AcceptAndMoveNext();
}
private void ParseCodeTransition(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
NullGenerateWhitespaceAndNewLine(in builder);
var lastWhitespace = AcceptWhitespaceInLines();
if (lastWhitespace != null)
{
if (Context.DesignTimeMode || !Context.StartOfLine)
{
// Markup owns whitespace in design time mode.
Accept(lastWhitespace);
lastWhitespace = null;
}
}
PutCurrentBack();
if (lastWhitespace != null)
{
PutBack(lastWhitespace);
}
OtherParserBlock(builder);
}
private void ParseMarkupComment(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
Assert(SyntaxKind.OpenAngle);
// Clear the current token builder.
builder.Add(OutputAsMarkupLiteral());
using (var pooledResult = Pool.Allocate<RazorSyntaxNode>())
{
var htmlCommentBuilder = pooledResult.Builder;
// Accept the '<', '!' and double-hyphen token at the beginning of the comment block.
AcceptAndMoveNext();
AcceptAndMoveNext();
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
htmlCommentBuilder.Add(OutputAsMarkupLiteral());
SetAcceptedCharacters(AcceptedCharactersInternal.Whitespace);
while (!EndOfFile)
{
ParseMarkupNodes(htmlCommentBuilder, ParseMode.Text, t => t.Kind == SyntaxKind.DoubleHyphen);
var lastDoubleHyphen = AcceptAllButLastDoubleHyphens();
if (At(SyntaxKind.CloseAngle))
{
// Output the content in the comment block as a separate markup
SetAcceptedCharacters(AcceptedCharactersInternal.Whitespace);
htmlCommentBuilder.Add(OutputAsMarkupLiteral());
// This is the end of a comment block
Accept(lastDoubleHyphen);
AcceptAndMoveNext();
SetAcceptedCharacters(AcceptedCharactersInternal.None);
htmlCommentBuilder.Add(OutputAsMarkupLiteral());
var commentBlock = SyntaxFactory.MarkupCommentBlock(htmlCommentBuilder.ToList());
builder.Add(commentBlock);
return;
}
else if (lastDoubleHyphen != null)
{
Accept(lastDoubleHyphen);
}
}
builder.Add(OutputAsMarkupLiteral());
}
}
private void ParseRazorCommentWithLeadingAndTrailingWhitespace(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
NullGenerateWhitespaceAndNewLine(in builder);
var shouldRenderWhitespace = true;
var lastWhitespace = AcceptWhitespaceInLines();
var startOfLine = Context.StartOfLine;
if (lastWhitespace != null)
{
// Don't render the whitespace between the start of the line and the razor comment.
if (startOfLine)
{
AcceptMarkerTokenIfNecessary();
// Output the tokens that may have been accepted prior to the whitespace.
builder.Add(OutputAsMarkupLiteral());
chunkGenerator = SpanChunkGenerator.Null;
shouldRenderWhitespace = false;
}
Accept(lastWhitespace);
lastWhitespace = null;
}
AcceptMarkerTokenIfNecessary();
if (shouldRenderWhitespace)
{
builder.Add(OutputAsMarkupLiteral());
}
else
{
builder.Add(OutputAsMarkupEphemeralLiteral());
}
var comment = ParseRazorComment();
builder.Add(comment);
// Handle the whitespace and newline at the end of a razor comment.
if (startOfLine &&
(At(SyntaxKind.NewLine) ||
(At(SyntaxKind.Whitespace) && NextIs(SyntaxKind.NewLine))))
{
AcceptWhile(IsSpacingToken);
AcceptAndMoveNext();
chunkGenerator = SpanChunkGenerator.Null;
builder.Add(OutputAsMarkupEphemeralLiteral());
}
}
private void ParseMisc(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
NullGenerateWhitespaceAndNewLine(in builder);
AcceptWhile(IsSpacingTokenIncludingNewLines);
}
private void NullGenerateWhitespaceAndNewLine(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
if (Context.NullGenerateWhitespaceAndNewLine)
{
// Usually this is set to true when a Code block ends and there is whitespace left after it.
// We don't want to write it to output.
if (TokenBuilder.Count != 0)
{
// There should be no unprocessed tokens, only whitespace between the code block end and here.
Debug.Assert(false, "Unexpected tokens after NullGenerateWhitespaceAndNewLine");
}
else
{
Context.NullGenerateWhitespaceAndNewLine = false;
chunkGenerator = SpanChunkGenerator.Null;
AcceptWhile(IsSpacingToken);
if (At(SyntaxKind.NewLine))
{
AcceptAndMoveNext();
}
builder.Add(OutputAsMarkupEphemeralLiteral());
}
}
}
private bool ScriptTagExpectsHtml(MarkupStartTagSyntax tagBlock)
{
MarkupAttributeBlockSyntax? typeAttribute = null;
for (var i = 0; i < tagBlock.Attributes.Count; i++)
{
var node = tagBlock.Attributes[i];
if (node is MarkupAttributeBlockSyntax attributeBlock &&
attributeBlock.Value != null &&
attributeBlock.Value.Children.Count > 0 &&
IsTypeAttribute(attributeBlock))
{
typeAttribute = attributeBlock;
break;
}
}
if (typeAttribute != null)
{
var contentValues = typeAttribute.Value.CreateRed().DescendantTokens();
var scriptType = string.Concat(contentValues.Select(t => t.Content)).Trim();
// Does not allow charset parameter (or any other parameters).
return string.Equals(scriptType, "text/html", StringComparison.OrdinalIgnoreCase);
}
return false;
}
private static bool IsTypeAttribute(MarkupAttributeBlockSyntax attributeBlock)
{
if (attributeBlock.Name.LiteralTokens.Count == 0)
{
return false;
}
var trimmedStartContent = attributeBlock.Name.ToString().TrimStart();
if (trimmedStartContent.StartsWith("type", StringComparison.OrdinalIgnoreCase) &&
(trimmedStartContent.Length == 4 ||
ValidAfterTypeAttributeNameCharacters.Contains(trimmedStartContent[4])))
{
return true;
}
return false;
}
// Internal for testing.
internal SyntaxToken? AcceptAllButLastDoubleHyphens()
{
var lastDoubleHyphen = CurrentToken;
AcceptWhile(s =>
{
if (NextIs(SyntaxKind.DoubleHyphen))
{
lastDoubleHyphen = s;
return true;
}
return false;
});
NextToken();
if (At(SyntaxKind.Text) && IsHyphen(CurrentToken))
{
// Doing this here to maintain the order of tokens
if (!NextIs(SyntaxKind.CloseAngle))
{
Accept(lastDoubleHyphen);
lastDoubleHyphen = null;
}
AcceptAndMoveNext();
}
return lastDoubleHyphen;
}
private bool AcceptTokenUntilAll(in SyntaxListBuilder<RazorSyntaxNode> builder, params SyntaxKind[] endSequence)
{
while (!EndOfFile)
{
ParseMarkupNodes(builder, ParseMode.Text, t => t.Kind == endSequence[0]);
if (AcceptAll(endSequence))
{
return true;
}
}
Debug.Assert(EndOfFile);
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
return false;
}
private void FastReadWhitespaceAndNewLines(ref PooledArrayBuilder<SyntaxToken> whitespaceTokens)
{
Debug.Assert(whitespaceTokens.Count == 0, "Expected empty builder.");
if (EnsureCurrent() && (CurrentToken.Kind == SyntaxKind.Whitespace || CurrentToken.Kind == SyntaxKind.NewLine))
{
whitespaceTokens.Add(CurrentToken);
NextToken();
while (EnsureCurrent() && (CurrentToken.Kind == SyntaxKind.Whitespace || CurrentToken.Kind == SyntaxKind.NewLine))
{
whitespaceTokens.Add(CurrentToken);
NextToken();
}
}
}
private ParserState GetParserState(ParseMode mode)
{
using var whitespace = new PooledArrayBuilder<SyntaxToken>();
FastReadWhitespaceAndNewLines(ref whitespace.AsRef());
try
{
if (whitespace.Count == 0 && EndOfFile)
{
return ParserState.EOF;
}
else if (At(SyntaxKind.RazorCommentTransition))
{
// Let the comment parser handle the preceding whitespace.
return ParserState.RazorComment;
}
else if (At(SyntaxKind.Transition))
{
if (NextIs(SyntaxKind.Transition))
{
return ParserState.DoubleTransition;
}
// Let the transition parser handle the preceding whitespace.
return ParserState.CodeTransition;
}
else if (whitespace.Count > 0)
{
// This whitespace isn't sensitive to what comes after it.
return ParserState.Misc;
}
else if (mode == ParseMode.Text)
{
// We don't want to parse as tags in Text mode. We do this for cases like script tags or <!-- -->.
return ParserState.MarkupText;
}
else if (At(SyntaxKind.OpenAngle))
{
if (NextIs(SyntaxKind.Bang))
{
// Checking to see if we meet the conditions of a special '!' tag: <!DOCTYPE, <![CDATA[, <!--.
if (!IsBangEscape(lookahead: 1))
{
if (IsHtmlCommentAhead())
{
return ParserState.MarkupComment;
}
else if (Lookahead(2)?.Kind == SyntaxKind.LeftBracket &&
Lookahead(3) is SyntaxToken tagName &&
string.Equals(tagName.Content, "cdata", StringComparison.OrdinalIgnoreCase) &&
Lookahead(4)?.Kind == SyntaxKind.LeftBracket)
{
return ParserState.CData;
}
else
{
// E.g. <!DOCTYPE ...
return ParserState.SpecialTag;
}
}
}
else if (NextIs(SyntaxKind.QuestionMark))
{
return ParserState.XmlPI;
}
// Regular tag
return ParserState.Tag;
}
else
{
return ParserState.Unknown;
}
}
finally
{
if (whitespace.Count > 0)
{
PutCurrentBack();
PutBack(in whitespace);
EnsureCurrent();
}
}
}
private bool TryParseBangEscape([NotNullWhen(true)] out SyntaxToken? bangToken)
{
bangToken = null;
if (IsBangEscape(lookahead: 0))
{
// Accept the parser escape character '!'.
Assert(SyntaxKind.Bang);
bangToken = EatCurrentToken();
return true;
}
return false;
}
private bool IsBangEscape(int lookahead)
{
var potentialBang = Lookahead(lookahead);
if (potentialBang != null &&
potentialBang.Kind == SyntaxKind.Bang)
{
var afterBang = Lookahead(lookahead + 1);
return afterBang != null &&
afterBang.Kind == SyntaxKind.Text &&
!string.Equals(afterBang.Content, "DOCTYPE", StringComparison.OrdinalIgnoreCase);
}
return false;
}
// Internal for testing
internal bool IsHtmlCommentAhead()
{
// From HTML5 Specification, available at https://html.spec.whatwg.org/multipage/syntax.html#comments
// Comments must have the following format:
// 1. The string "<!--"
// 2. Optionally, text, with the additional restriction that the text
// 2.1 must not start with the string ">" nor start with the string "->"
// 2.2 nor contain the strings
// 2.2.1 "<!--"
// 2.2.2 "-->" As we will be treating this as a comment ending, there is no need to handle this case at all.
// 2.2.3 "--!>"
// 2.3 nor end with the string "<!-".
// 3. The string "-->"
if (!(At(SyntaxKind.OpenAngle) && NextIs(SyntaxKind.Bang)))
{
return false;
}
// Consume '<' and '!'
var openAngle = EatCurrentToken();
var bangToken = EatCurrentToken();
try
{
if (EndOfFile || CurrentToken.Kind != SyntaxKind.DoubleHyphen)
{
return false;
}
// Check condition 2.1
if (NextIs(SyntaxKind.CloseAngle) || NextIs(next => IsHyphen(next) && NextIs(SyntaxKind.CloseAngle)))
{
return false;
}
// Check condition 2.2
var isValidComment = false;
LookaheadUntil((token, ref readonly prevTokens) =>
{
if (token.Kind == SyntaxKind.DoubleHyphen)
{
if (NextIs(SyntaxKind.CloseAngle))
{
// Check condition 2.3: We're at the end of a comment. Check to make sure the text ending is allowed.
isValidComment = !IsCommentContentEndingInvalid(in prevTokens);
return true;
}
else if (NextIs(ns => IsHyphen(ns) && NextIs(SyntaxKind.CloseAngle)))
{
// Check condition 2.3: we're at the end of a comment, which has an extra dash.
// Need to treat the dash as part of the content and check the ending.
// However, that case would have already been checked as part of check from 2.2.1 which
// would already fail this iteration and we wouldn't get here
isValidComment = true;
return true;
}
else if (NextIs(ns => ns.Kind == SyntaxKind.Bang && NextIs(SyntaxKind.CloseAngle)))
{
// This is condition 2.2.3
isValidComment = false;
return true;
}
}
else if (token.Kind == SyntaxKind.OpenAngle)
{
// Checking condition 2.2.1
if (NextIs(ns => ns.Kind == SyntaxKind.Bang && NextIs(SyntaxKind.DoubleHyphen)))
{
isValidComment = false;
return true;
}
}
return false;
});
return isValidComment;
}
finally
{
// Put back the consumed tokens for later parsing.
PutCurrentBack();
PutBack(bangToken);
PutBack(openAngle);
EnsureCurrent();
}
}
private bool IsConditionalAttributeName(string name)
{
if (Context.Options.AllowConditionalDataDashAttributes)
{
return true;
}
if (!name.StartsWith("data-", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
private void NestingBlock(in SyntaxListBuilder<RazorSyntaxNode> builder, Tuple<string, string> nestingSequences)
{
var nesting = 1;
while (nesting > 0 && !EndOfFile)
{
ParseMarkupNodes(builder, ParseMode.Text, token =>
token.Kind == SyntaxKind.Text ||
token.Kind == SyntaxKind.OpenAngle);
if (At(SyntaxKind.Text))
{
// We need to inspect this text token to figure out if this could be the end of the Razor block
// or if it is the start of a new block in which case we need to keep track of the nesting level.
nesting += ProcessTextToken(builder, nestingSequences, nesting);
if (CurrentToken != null)
{
// This was just some regular text. Accept and move on.
// If we were at the end of a block, we would have already accepted it and CurrentToken will be null.
AcceptAndMoveNext();
}
else if (nesting > 0)
{
// This was the start of a new block. We've already consumed the text. Move on.
NextToken();
}
}
else
{
// We're at a tag. Parse it and continue.
ParseMarkupNode(builder, ParseMode.Markup);
}
}
// If we are still tracking any unclosed start tags, we need to close them.
while (_tagTracker.Count > 0)
{
var tracker = _tagTracker.Pop();
var element = SyntaxFactory.MarkupElement(tracker.StartTag, builder.Consume(), markupEndTag: null);
builder.AddRange(tracker.PreviousNodes);
builder.Add(element);
}
}
private int ProcessTextToken(in SyntaxListBuilder<RazorSyntaxNode> builder, Tuple<string, string> nestingSequences, int currentNesting)
{
for (var i = 0; i < CurrentToken.Content.Length; i++)
{
var nestingDelta = HandleNestingSequence(builder, nestingSequences.Item1, i, currentNesting, 1);
if (nestingDelta == 0)
{
nestingDelta = HandleNestingSequence(builder, nestingSequences.Item2, i, currentNesting, -1);
}
if (nestingDelta != 0)
{
return nestingDelta;
}
}
return 0;
}
private int HandleNestingSequence(in SyntaxListBuilder<RazorSyntaxNode> builder, string sequence, int position, int currentNesting, int retIfMatched)
{
if (sequence != null &&
CurrentToken.Content[position] == sequence[0] &&
position + sequence.Length <= CurrentToken.Content.Length)
{
var possibleStart = CurrentToken.Content.AsSpan(position, sequence.Length);
if (possibleStart.Equals(sequence.AsSpan(), Comparison))
{
// Capture the current token and "put it back" (really we just want to clear CurrentToken)
var bookmark = CurrentStart;
var token = CurrentToken;
PutCurrentBack();
// Carve up the token
var (preSequence, right) = Language.SplitToken(token, position, SyntaxKind.Text);
Debug.Assert(right != null);
var (sequenceToken, _) = Language.SplitToken(right, sequence.Length, SyntaxKind.Text);
var postSequenceBookmark = bookmark.AbsoluteIndex + preSequence.Content.Length + sequenceToken.Content.Length;
// Accept the first chunk (up to the nesting sequence we just saw)
if (!string.IsNullOrEmpty(preSequence.Content))
{
Accept(preSequence);
}
if (currentNesting + retIfMatched == 0)
{
// This is 'popping' the final entry on the stack of nesting sequences
// A caller higher in the parsing stack will accept the sequence token, so advance
// to it
Context.Source.Position = bookmark.AbsoluteIndex + preSequence.Content.Length;
}
else
{
// This isn't the end of the last nesting sequence, accept the token and keep going
Accept(sequenceToken);
// Position at the start of the postSequence token, which might be null.
Context.Source.Position = postSequenceBookmark;
}
// Return the value we were asked to return if matched, since we found a nesting sequence
return retIfMatched;
}
}
return 0;
}
private void OtherParserBlock(in SyntaxListBuilder<RazorSyntaxNode> builder)
{
AcceptMarkerTokenIfNecessary();
builder.Add(OutputAsMarkupLiteral());
RazorSyntaxNode? codeBlock;
using (PushSpanContextConfig())
{
codeBlock = CodeParser.ParseBlock();
}
builder.Add(codeBlock);
InitializeContext();
NextToken();
}
/// <summary>
/// Verifies, that the sequence doesn't end with the "<!-" HtmlTokens. Note, the first token is an opening bracket token
/// </summary>
internal static bool IsCommentContentEndingInvalid(ref readonly PooledArrayBuilder<SyntaxToken> tokens)
{
var index = 0;
for (var i = tokens.Count - 1; i >= 0; i--)
{
if (!tokens[i].IsEquivalentTo(nonAllowedHtmlCommentEnding[index++]))
{
return false;
}
if (index == nonAllowedHtmlCommentEnding.Length)
{
return true;
}
}
return false;
}
internal static bool IsHyphen(SyntaxToken token)
{
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: https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// 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.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(SpanEditHandlerBuilder? editHandlerBuilder, ref ISpanChunkGenerator? generator)
{
generator = MarkupChunkGenerator.Instance;
SetAcceptedCharacters(AcceptedCharactersInternal.Any);
if (editHandlerBuilder == null)
{
return;
}
editHandlerBuilder.Reset();
editHandlerBuilder.Tokenizer = LanguageTokenizeString;
}
private static Syntax.GreenNode? GetLastSpan(RazorSyntaxNode node)
{
if (node == null)
{
return null;
}
// Find the last token of this node and return its immediate non-list parent.
var red = node.CreateRed();
var last = red.GetLastToken();
if (last.Kind == SyntaxKind.None)
{
return null;
}
return last.Parent?.Green;
}
private static bool IsVoidElement(string? tagName)
{
if (string.IsNullOrEmpty(tagName))
{
return false;
}
if (tagName!.StartsWith("!", StringComparison.Ordinal))
{
tagName = tagName.Substring(1);
}
return ParserHelpers.VoidElements.Contains(tagName);
}
private enum ParseMode
{
Markup,
MarkupInCodeBlock,
Text,
}
private enum MarkupTagMode
{
Normal,
Void,
SelfClosing,
Script,
Invalid,
}
private class TagTracker
{
public TagTracker(
string tagName,
MarkupStartTagSyntax startTag,
SourceLocation tagLocation,
SyntaxList<RazorSyntaxNode> previousNodes,
bool isWellFormed)
{
TagName = tagName;
StartTag = startTag;
TagLocation = tagLocation;
PreviousNodes = previousNodes;
IsWellFormed = isWellFormed;
}
public string TagName { get; }
public MarkupStartTagSyntax StartTag { get; }
public SourceLocation TagLocation { get; }
public SyntaxList<RazorSyntaxNode> PreviousNodes { get; }
public bool IsWellFormed { get; }
}
}
|