1133 lines
43 KiB
C#
1133 lines
43 KiB
C#
// 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.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using Microsoft.AspNet.Razor.Editor;
|
|
using Microsoft.AspNet.Razor.Chunks.Generators;
|
|
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
|
|
using Microsoft.AspNet.Razor.Text;
|
|
using Microsoft.AspNet.Razor.Tokenizer.Symbols;
|
|
|
|
namespace Microsoft.AspNet.Razor.Parser
|
|
{
|
|
public partial class HtmlMarkupParser
|
|
{
|
|
private const string ScriptTagName = "script";
|
|
|
|
private SourceLocation _lastTagStart = SourceLocation.Zero;
|
|
private HtmlSymbol _bufferedOpenAngle;
|
|
|
|
public override void ParseBlock()
|
|
{
|
|
if (Context == null)
|
|
{
|
|
throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
|
|
}
|
|
|
|
using (PushSpanConfig(DefaultMarkupSpan))
|
|
{
|
|
using (Context.StartBlock(BlockType.Markup))
|
|
{
|
|
if (!NextToken())
|
|
{
|
|
return;
|
|
}
|
|
|
|
AcceptWhile(IsSpacingToken(includeNewLines: true));
|
|
|
|
if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle)
|
|
{
|
|
// "<" => Implicit Tag Block
|
|
TagBlock(new Stack<Tuple<HtmlSymbol, SourceLocation>>());
|
|
}
|
|
else if (CurrentSymbol.Type == HtmlSymbolType.Transition)
|
|
{
|
|
// "@" => Explicit Tag/Single Line Block OR Template
|
|
Output(SpanKind.Markup);
|
|
|
|
// Definitely have a transition span
|
|
Assert(HtmlSymbolType.Transition);
|
|
AcceptAndMoveNext();
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKind.Transition);
|
|
if (At(HtmlSymbolType.Transition))
|
|
{
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
AcceptAndMoveNext();
|
|
Output(SpanKind.MetaCode);
|
|
}
|
|
AfterTransition();
|
|
}
|
|
else
|
|
{
|
|
Context.OnError(
|
|
CurrentSymbol.Start,
|
|
RazorResources.ParseError_MarkupBlock_Must_Start_With_Tag,
|
|
CurrentSymbol.Content.Length);
|
|
}
|
|
Output(SpanKind.Markup);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DefaultMarkupSpan(SpanBuilder span)
|
|
{
|
|
span.ChunkGenerator = new MarkupChunkGenerator();
|
|
span.EditHandler = new SpanEditHandler(Language.TokenizeString, AcceptedCharacters.Any);
|
|
}
|
|
|
|
private void AfterTransition()
|
|
{
|
|
// "@:" => Explicit Single Line Block
|
|
if (CurrentSymbol.Type == HtmlSymbolType.Text && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == ':')
|
|
{
|
|
// Split the token
|
|
Tuple<HtmlSymbol, HtmlSymbol> split = Language.SplitSymbol(CurrentSymbol, 1, HtmlSymbolType.Colon);
|
|
|
|
// The first part (left) is added to this span and we return a MetaCode span
|
|
Accept(split.Item1);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKind.MetaCode);
|
|
if (split.Item2 != null)
|
|
{
|
|
Accept(split.Item2);
|
|
}
|
|
NextToken();
|
|
SingleLineMarkup();
|
|
}
|
|
else if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle)
|
|
{
|
|
TagBlock(new Stack<Tuple<HtmlSymbol, SourceLocation>>());
|
|
}
|
|
}
|
|
|
|
private void SingleLineMarkup()
|
|
{
|
|
// Parse until a newline, it's that simple!
|
|
// First, signal to code parser that whitespace is significant to us.
|
|
var old = Context.WhiteSpaceIsSignificantToAncestorBlock;
|
|
Context.WhiteSpaceIsSignificantToAncestorBlock = true;
|
|
Span.EditHandler = new SingleLineMarkupEditHandler(Language.TokenizeString);
|
|
SkipToAndParseCode(HtmlSymbolType.NewLine);
|
|
if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine)
|
|
{
|
|
AcceptAndMoveNext();
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
PutCurrentBack();
|
|
Context.WhiteSpaceIsSignificantToAncestorBlock = old;
|
|
Output(SpanKind.Markup);
|
|
}
|
|
|
|
private void TagBlock(Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
|
|
{
|
|
// Skip Whitespace and Text
|
|
var complete = false;
|
|
do
|
|
{
|
|
SkipToAndParseCode(HtmlSymbolType.OpenAngle);
|
|
|
|
// Output everything prior to the OpenAngle into a markup span
|
|
Output(SpanKind.Markup);
|
|
|
|
// Do not want to start a new tag block if we're at the end of the file.
|
|
IDisposable tagBlockWrapper = null;
|
|
try
|
|
{
|
|
var atSpecialTag = AtSpecialTag;
|
|
|
|
if (!EndOfFile && !atSpecialTag)
|
|
{
|
|
// Start a Block tag. This is used to wrap things like <p> or <a class="btn"> etc.
|
|
tagBlockWrapper = Context.StartBlock(BlockType.Tag);
|
|
}
|
|
|
|
if (EndOfFile)
|
|
{
|
|
EndTagBlock(tags, complete: true);
|
|
}
|
|
else
|
|
{
|
|
_bufferedOpenAngle = null;
|
|
_lastTagStart = CurrentLocation;
|
|
Assert(HtmlSymbolType.OpenAngle);
|
|
_bufferedOpenAngle = CurrentSymbol;
|
|
var tagStart = CurrentLocation;
|
|
if (!NextToken())
|
|
{
|
|
Accept(_bufferedOpenAngle);
|
|
EndTagBlock(tags, complete: false);
|
|
}
|
|
else
|
|
{
|
|
complete = AfterTagStart(tagStart, tags, atSpecialTag, tagBlockWrapper);
|
|
}
|
|
}
|
|
|
|
if (complete)
|
|
{
|
|
// Completed tags have no accepted characters inside of blocks.
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
// Output the contents of the tag into its own markup span.
|
|
Output(SpanKind.Markup);
|
|
}
|
|
finally
|
|
{
|
|
// Will be null if we were at end of file or special tag when initially created.
|
|
if (tagBlockWrapper != null)
|
|
{
|
|
// End tag block
|
|
tagBlockWrapper.Dispose();
|
|
}
|
|
}
|
|
}
|
|
while (tags.Count > 0);
|
|
|
|
EndTagBlock(tags, complete);
|
|
}
|
|
|
|
private bool AfterTagStart(SourceLocation tagStart,
|
|
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
|
|
bool atSpecialTag,
|
|
IDisposable tagBlockWrapper)
|
|
{
|
|
if (!EndOfFile)
|
|
{
|
|
switch (CurrentSymbol.Type)
|
|
{
|
|
case HtmlSymbolType.ForwardSlash:
|
|
// End Tag
|
|
return EndTag(tagStart, tags, tagBlockWrapper);
|
|
case HtmlSymbolType.Bang:
|
|
// Comment, CDATA, DOCTYPE, or a parser-escaped HTML tag.
|
|
if (atSpecialTag)
|
|
{
|
|
Accept(_bufferedOpenAngle);
|
|
return BangTag();
|
|
}
|
|
else
|
|
{
|
|
goto default;
|
|
}
|
|
case HtmlSymbolType.QuestionMark:
|
|
// XML PI
|
|
Accept(_bufferedOpenAngle);
|
|
return XmlPI();
|
|
default:
|
|
// Start Tag
|
|
return StartTag(tags, tagBlockWrapper);
|
|
}
|
|
}
|
|
if (tags.Count == 0)
|
|
{
|
|
Context.OnError(
|
|
CurrentLocation,
|
|
RazorResources.ParseError_OuterTagMissingName,
|
|
length: 1 /* end of file */);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private bool XmlPI()
|
|
{
|
|
// Accept "?"
|
|
Assert(HtmlSymbolType.QuestionMark);
|
|
AcceptAndMoveNext();
|
|
return AcceptUntilAll(HtmlSymbolType.QuestionMark, HtmlSymbolType.CloseAngle);
|
|
}
|
|
|
|
private bool BangTag()
|
|
{
|
|
// Accept "!"
|
|
Assert(HtmlSymbolType.Bang);
|
|
|
|
if (AcceptAndMoveNext())
|
|
{
|
|
if (CurrentSymbol.Type == HtmlSymbolType.DoubleHyphen)
|
|
{
|
|
AcceptAndMoveNext();
|
|
return AcceptUntilAll(HtmlSymbolType.DoubleHyphen, HtmlSymbolType.CloseAngle);
|
|
}
|
|
else if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket)
|
|
{
|
|
if (AcceptAndMoveNext())
|
|
{
|
|
return CData();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AcceptAndMoveNext();
|
|
return AcceptUntilAll(HtmlSymbolType.CloseAngle);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool CData()
|
|
{
|
|
if (CurrentSymbol.Type == HtmlSymbolType.Text && string.Equals(CurrentSymbol.Content, "cdata", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (AcceptAndMoveNext())
|
|
{
|
|
if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket)
|
|
{
|
|
return AcceptUntilAll(HtmlSymbolType.RightBracket, HtmlSymbolType.RightBracket, HtmlSymbolType.CloseAngle);
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool EndTag(SourceLocation tagStart,
|
|
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
|
|
IDisposable tagBlockWrapper)
|
|
{
|
|
// Accept "/" and move next
|
|
Assert(HtmlSymbolType.ForwardSlash);
|
|
var forwardSlash = CurrentSymbol;
|
|
if (!NextToken())
|
|
{
|
|
Accept(_bufferedOpenAngle);
|
|
Accept(forwardSlash);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
var tagName = string.Empty;
|
|
HtmlSymbol bangSymbol = null;
|
|
|
|
if (At(HtmlSymbolType.Bang))
|
|
{
|
|
bangSymbol = CurrentSymbol;
|
|
|
|
var nextSymbol = Lookahead(count: 1);
|
|
|
|
if (nextSymbol != null && nextSymbol.Type == HtmlSymbolType.Text)
|
|
{
|
|
tagName = "!" + nextSymbol.Content;
|
|
}
|
|
}
|
|
else if (At(HtmlSymbolType.Text))
|
|
{
|
|
tagName = CurrentSymbol.Content;
|
|
}
|
|
|
|
var matched = RemoveTag(tags, tagName, tagStart);
|
|
|
|
if (tags.Count == 0 &&
|
|
// Note tagName may contain a '!' escape character. This ensures </!text> doesn't match here.
|
|
// </!text> tags are treated like any other escaped HTML end tag.
|
|
string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) &&
|
|
matched)
|
|
{
|
|
return EndTextTag(forwardSlash, tagBlockWrapper);
|
|
}
|
|
Accept(_bufferedOpenAngle);
|
|
Accept(forwardSlash);
|
|
|
|
OptionalBangEscape();
|
|
|
|
AcceptUntil(HtmlSymbolType.CloseAngle);
|
|
|
|
// Accept the ">"
|
|
return Optional(HtmlSymbolType.CloseAngle);
|
|
}
|
|
}
|
|
|
|
private void RecoverTextTag()
|
|
{
|
|
// We don't want to skip-to and parse because there shouldn't be anything in the body of text tags.
|
|
AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.NewLine);
|
|
|
|
// Include the close angle in the text tag block if it's there, otherwise just move on
|
|
Optional(HtmlSymbolType.CloseAngle);
|
|
}
|
|
|
|
private bool EndTextTag(HtmlSymbol solidus, IDisposable tagBlockWrapper)
|
|
{
|
|
Accept(_bufferedOpenAngle);
|
|
Accept(solidus);
|
|
|
|
var textLocation = CurrentLocation;
|
|
Assert(HtmlSymbolType.Text);
|
|
AcceptAndMoveNext();
|
|
|
|
var seenCloseAngle = Optional(HtmlSymbolType.CloseAngle);
|
|
|
|
if (!seenCloseAngle)
|
|
{
|
|
Context.OnError(
|
|
textLocation,
|
|
RazorResources.ParseError_TextTagCannotContainAttributes,
|
|
length: 4 /* text */);
|
|
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
RecoverTextTag();
|
|
}
|
|
else
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
|
|
CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKind.Transition);
|
|
|
|
return seenCloseAngle;
|
|
}
|
|
|
|
// Special tags include <!--, <!DOCTYPE, <![CDATA and <? tags
|
|
private bool AtSpecialTag
|
|
{
|
|
get
|
|
{
|
|
if (At(HtmlSymbolType.OpenAngle))
|
|
{
|
|
if (NextIs(HtmlSymbolType.Bang))
|
|
{
|
|
return !IsBangEscape(lookahead: 1);
|
|
}
|
|
|
|
return NextIs(HtmlSymbolType.QuestionMark);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool IsTagRecoveryStopPoint(HtmlSymbol sym)
|
|
{
|
|
return sym.Type == HtmlSymbolType.CloseAngle ||
|
|
sym.Type == HtmlSymbolType.ForwardSlash ||
|
|
sym.Type == HtmlSymbolType.OpenAngle ||
|
|
sym.Type == HtmlSymbolType.SingleQuote ||
|
|
sym.Type == HtmlSymbolType.DoubleQuote;
|
|
}
|
|
|
|
private void TagContent()
|
|
{
|
|
if (!At(HtmlSymbolType.WhiteSpace) && !At(HtmlSymbolType.NewLine))
|
|
{
|
|
// We should be right after the tag name, so if there's no whitespace or new line, something is wrong
|
|
RecoverToEndOfTag();
|
|
}
|
|
else
|
|
{
|
|
// We are here ($): <tag$ foo="bar" biz="~/Baz" />
|
|
while (!EndOfFile && !IsEndOfTag())
|
|
{
|
|
BeforeAttribute();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsEndOfTag()
|
|
{
|
|
if (At(HtmlSymbolType.ForwardSlash))
|
|
{
|
|
if (NextIs(HtmlSymbolType.CloseAngle))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
}
|
|
return At(HtmlSymbolType.CloseAngle) || At(HtmlSymbolType.OpenAngle);
|
|
}
|
|
|
|
private void BeforeAttribute()
|
|
{
|
|
// http://dev.w3.org/html5/spec/tokenization.html#before-attribute-name-state
|
|
// Capture whitespace
|
|
var whitespace = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
|
|
|
|
if (At(HtmlSymbolType.Transition))
|
|
{
|
|
// Transition outside of attribute value => Switch to recovery mode
|
|
Accept(whitespace);
|
|
RecoverToEndOfTag();
|
|
return;
|
|
}
|
|
|
|
// http://dev.w3.org/html5/spec/tokenization.html#attribute-name-state
|
|
// Read the 'name' (i.e. read until the '=' or whitespace/newline)
|
|
var name = Enumerable.Empty<HtmlSymbol>();
|
|
var whitespaceAfterAttributeName = Enumerable.Empty<HtmlSymbol>();
|
|
if (At(HtmlSymbolType.Text))
|
|
{
|
|
name = ReadWhile(sym =>
|
|
sym.Type != HtmlSymbolType.WhiteSpace &&
|
|
sym.Type != HtmlSymbolType.NewLine &&
|
|
sym.Type != HtmlSymbolType.Equals &&
|
|
sym.Type != HtmlSymbolType.CloseAngle &&
|
|
sym.Type != HtmlSymbolType.OpenAngle &&
|
|
(sym.Type != HtmlSymbolType.ForwardSlash || !NextIs(HtmlSymbolType.CloseAngle)));
|
|
|
|
// capture whitespace after attribute name (if any)
|
|
whitespaceAfterAttributeName = ReadWhile(
|
|
sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
|
|
}
|
|
else
|
|
{
|
|
// Unexpected character in tag, enter recovery
|
|
Accept(whitespace);
|
|
RecoverToEndOfTag();
|
|
return;
|
|
}
|
|
|
|
if (!At(HtmlSymbolType.Equals))
|
|
{
|
|
// Minimized attribute
|
|
|
|
// We are at the prefix of the next attribute or the end of tag. Put it back so it is parsed later.
|
|
PutCurrentBack();
|
|
PutBack(whitespaceAfterAttributeName);
|
|
|
|
// Output anything prior to the attribute, in most cases this will be the tag name:
|
|
// |<input| checked />. If in-between other attributes this will noop or output malformed attribute
|
|
// content (if the previous attribute was malformed).
|
|
Output(SpanKind.Markup);
|
|
|
|
using (Context.StartBlock(BlockType.Markup))
|
|
{
|
|
Accept(whitespace);
|
|
Accept(name);
|
|
Output(SpanKind.Markup);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Not a minimized attribute, parse as if it were well-formed (if attribute turns out to be malformed we
|
|
// will go into recovery).
|
|
Output(SpanKind.Markup);
|
|
|
|
// Start a new markup block for the attribute
|
|
using (Context.StartBlock(BlockType.Markup))
|
|
{
|
|
AttributePrefix(whitespace, name, whitespaceAfterAttributeName);
|
|
}
|
|
}
|
|
|
|
private void AttributePrefix(
|
|
IEnumerable<HtmlSymbol> whitespace,
|
|
IEnumerable<HtmlSymbol> nameSymbols,
|
|
IEnumerable<HtmlSymbol> whitespaceAfterAttributeName)
|
|
{
|
|
// First, determine if this is a 'data-' attribute (since those can't use conditional attributes)
|
|
var name = nameSymbols.GetContent(Span.Start);
|
|
var attributeCanBeConditional = !name.Value.StartsWith("data-", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// Accept the whitespace and name
|
|
Accept(whitespace);
|
|
Accept(nameSymbols);
|
|
|
|
// Since this is not a minimized attribute, the whitespace after attribute name belongs to this attribute.
|
|
Accept(whitespaceAfterAttributeName);
|
|
Assert(HtmlSymbolType.Equals); // We should be at "="
|
|
AcceptAndMoveNext();
|
|
|
|
var whitespaceAfterEquals = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
|
|
var quote = HtmlSymbolType.Unknown;
|
|
if (At(HtmlSymbolType.SingleQuote) || At(HtmlSymbolType.DoubleQuote))
|
|
{
|
|
// Found a quote, the whitespace belongs to this attribute.
|
|
Accept(whitespaceAfterEquals);
|
|
quote = CurrentSymbol.Type;
|
|
AcceptAndMoveNext();
|
|
}
|
|
else if (whitespaceAfterEquals.Any())
|
|
{
|
|
// No quotes found after the whitespace. Put it back so that it can be parsed later.
|
|
PutCurrentBack();
|
|
PutBack(whitespaceAfterEquals);
|
|
}
|
|
|
|
// We now have the prefix: (i.e. ' foo="')
|
|
var prefix = Span.GetContent();
|
|
|
|
if (attributeCanBeConditional)
|
|
{
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null; // The block chunk generator will render the prefix
|
|
Output(SpanKind.Markup);
|
|
|
|
// Read the attribute value only if the value is quoted
|
|
// or if there is no whitespace between '=' and the unquoted value.
|
|
if (quote != HtmlSymbolType.Unknown || !whitespaceAfterEquals.Any())
|
|
{
|
|
// Read the attribute value.
|
|
while (!EndOfFile && !IsEndOfAttributeValue(quote, CurrentSymbol))
|
|
{
|
|
AttributeValue(quote);
|
|
}
|
|
}
|
|
|
|
// Capture the suffix
|
|
var suffix = new LocationTagged<string>(string.Empty, CurrentLocation);
|
|
if (quote != HtmlSymbolType.Unknown && At(quote))
|
|
{
|
|
suffix = CurrentSymbol.GetContent();
|
|
AcceptAndMoveNext();
|
|
}
|
|
|
|
if (Span.Symbols.Count > 0)
|
|
{
|
|
// Again, block chunk generator will render the suffix
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
Output(SpanKind.Markup);
|
|
}
|
|
|
|
// Create the block chunk generator
|
|
Context.CurrentBlock.ChunkGenerator = new AttributeBlockChunkGenerator(
|
|
name, prefix, suffix);
|
|
}
|
|
else
|
|
{
|
|
// Output the attribute name, the equals and optional quote. Ex: foo="
|
|
Output(SpanKind.Markup);
|
|
|
|
if (quote == HtmlSymbolType.Unknown && whitespaceAfterEquals.Any())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Not a "conditional" attribute, so just read the value
|
|
SkipToAndParseCode(sym => IsEndOfAttributeValue(quote, sym));
|
|
|
|
// Output the attribute value (will include everything in-between the attribute's quotes).
|
|
Output(SpanKind.Markup);
|
|
|
|
if (quote != HtmlSymbolType.Unknown)
|
|
{
|
|
Optional(quote);
|
|
}
|
|
Output(SpanKind.Markup);
|
|
}
|
|
}
|
|
|
|
private void AttributeValue(HtmlSymbolType quote)
|
|
{
|
|
var prefixStart = CurrentLocation;
|
|
var prefix = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine);
|
|
|
|
if (At(HtmlSymbolType.Transition))
|
|
{
|
|
if (NextIs(HtmlSymbolType.Transition))
|
|
{
|
|
// Wrapping this in a block so that the ConditionalAttributeCollapser doesn't rewrite it.
|
|
using (Context.StartBlock(BlockType.Markup))
|
|
{
|
|
Accept(prefix);
|
|
|
|
// Render a single "@" in place of "@@".
|
|
Span.ChunkGenerator = new LiteralAttributeChunkGenerator(
|
|
prefix.GetContent(prefixStart),
|
|
new LocationTagged<string>(CurrentSymbol.GetContent(), CurrentLocation));
|
|
AcceptAndMoveNext();
|
|
Output(SpanKind.Markup, AcceptedCharacters.None);
|
|
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
AcceptAndMoveNext();
|
|
Output(SpanKind.Markup, AcceptedCharacters.None);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Accept(prefix);
|
|
var valueStart = CurrentLocation;
|
|
PutCurrentBack();
|
|
|
|
// Output the prefix but as a null-span. DynamicAttributeBlockChunkGenerator will render it
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
|
|
// Dynamic value, start a new block and set the chunk generator
|
|
using (Context.StartBlock(BlockType.Markup))
|
|
{
|
|
Context.CurrentBlock.ChunkGenerator =
|
|
new DynamicAttributeBlockChunkGenerator(prefix.GetContent(prefixStart), valueStart);
|
|
|
|
OtherParserBlock();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Accept(prefix);
|
|
|
|
// Literal value
|
|
// 'quote' should be "Unknown" if not quoted and symbols coming from the tokenizer should never have
|
|
// "Unknown" type.
|
|
var value = ReadWhile(sym =>
|
|
// These three conditions find separators which break the attribute value into portions
|
|
sym.Type != HtmlSymbolType.WhiteSpace &&
|
|
sym.Type != HtmlSymbolType.NewLine &&
|
|
sym.Type != HtmlSymbolType.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, sym));
|
|
Accept(value);
|
|
Span.ChunkGenerator = new LiteralAttributeChunkGenerator(
|
|
prefix.GetContent(prefixStart),
|
|
value.GetContent(prefixStart));
|
|
}
|
|
Output(SpanKind.Markup);
|
|
}
|
|
|
|
private bool IsEndOfAttributeValue(HtmlSymbolType quote, HtmlSymbol sym)
|
|
{
|
|
return EndOfFile || sym == null ||
|
|
(quote != HtmlSymbolType.Unknown
|
|
? sym.Type == quote // If quoted, just wait for the quote
|
|
: IsUnquotedEndOfAttributeValue(sym));
|
|
}
|
|
|
|
private bool IsUnquotedEndOfAttributeValue(HtmlSymbol sym)
|
|
{
|
|
// 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 sym.Type == HtmlSymbolType.DoubleQuote ||
|
|
sym.Type == HtmlSymbolType.SingleQuote ||
|
|
sym.Type == HtmlSymbolType.OpenAngle ||
|
|
sym.Type == HtmlSymbolType.Equals ||
|
|
(sym.Type == HtmlSymbolType.ForwardSlash && NextIs(HtmlSymbolType.CloseAngle)) ||
|
|
sym.Type == HtmlSymbolType.CloseAngle ||
|
|
sym.Type == HtmlSymbolType.WhiteSpace ||
|
|
sym.Type == HtmlSymbolType.NewLine;
|
|
}
|
|
|
|
private void RecoverToEndOfTag()
|
|
{
|
|
// Accept until ">", "/" or "<", but parse code
|
|
while (!EndOfFile)
|
|
{
|
|
SkipToAndParseCode(IsTagRecoveryStopPoint);
|
|
if (!EndOfFile)
|
|
{
|
|
EnsureCurrent();
|
|
switch (CurrentSymbol.Type)
|
|
{
|
|
case HtmlSymbolType.SingleQuote:
|
|
case HtmlSymbolType.DoubleQuote:
|
|
ParseQuoted();
|
|
break;
|
|
case HtmlSymbolType.OpenAngle:
|
|
// Another "<" means this tag is invalid.
|
|
case HtmlSymbolType.ForwardSlash:
|
|
// Empty tag
|
|
case HtmlSymbolType.CloseAngle:
|
|
// End of tag
|
|
return;
|
|
default:
|
|
AcceptAndMoveNext();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ParseQuoted()
|
|
{
|
|
var type = CurrentSymbol.Type;
|
|
AcceptAndMoveNext();
|
|
ParseQuoted(type);
|
|
}
|
|
|
|
private void ParseQuoted(HtmlSymbolType type)
|
|
{
|
|
SkipToAndParseCode(type);
|
|
if (!EndOfFile)
|
|
{
|
|
Assert(type);
|
|
AcceptAndMoveNext();
|
|
}
|
|
}
|
|
|
|
private bool StartTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, IDisposable tagBlockWrapper)
|
|
{
|
|
HtmlSymbol bangSymbol = null;
|
|
HtmlSymbol potentialTagNameSymbol;
|
|
|
|
if (At(HtmlSymbolType.Bang))
|
|
{
|
|
bangSymbol = CurrentSymbol;
|
|
|
|
potentialTagNameSymbol = Lookahead(count: 1);
|
|
}
|
|
else
|
|
{
|
|
potentialTagNameSymbol = CurrentSymbol;
|
|
}
|
|
|
|
HtmlSymbol tagName;
|
|
|
|
if (potentialTagNameSymbol == null || potentialTagNameSymbol.Type != HtmlSymbolType.Text)
|
|
{
|
|
tagName = new HtmlSymbol(potentialTagNameSymbol.Start, string.Empty, HtmlSymbolType.Unknown);
|
|
}
|
|
else if (bangSymbol != null)
|
|
{
|
|
tagName = new HtmlSymbol(bangSymbol.Start, "!" + potentialTagNameSymbol.Content, HtmlSymbolType.Text);
|
|
}
|
|
else
|
|
{
|
|
tagName = potentialTagNameSymbol;
|
|
}
|
|
|
|
Tuple<HtmlSymbol, SourceLocation> tag = Tuple.Create(tagName, _lastTagStart);
|
|
|
|
if (tags.Count == 0 &&
|
|
// Note tagName may contain a '!' escape character. This ensures <!text> doesn't match here.
|
|
// <!text> tags are treated like any other escaped HTML start tag.
|
|
string.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Output(SpanKind.Markup);
|
|
Span.ChunkGenerator = SpanChunkGenerator.Null;
|
|
|
|
Accept(_bufferedOpenAngle);
|
|
var textLocation = CurrentLocation;
|
|
Assert(HtmlSymbolType.Text);
|
|
|
|
AcceptAndMoveNext();
|
|
|
|
var bookmark = CurrentLocation.AbsoluteIndex;
|
|
IEnumerable<HtmlSymbol> tokens = ReadWhile(IsSpacingToken(includeNewLines: true));
|
|
var empty = At(HtmlSymbolType.ForwardSlash);
|
|
if (empty)
|
|
{
|
|
Accept(tokens);
|
|
Assert(HtmlSymbolType.ForwardSlash);
|
|
AcceptAndMoveNext();
|
|
bookmark = CurrentLocation.AbsoluteIndex;
|
|
tokens = ReadWhile(IsSpacingToken(includeNewLines: true));
|
|
}
|
|
|
|
if (!Optional(HtmlSymbolType.CloseAngle))
|
|
{
|
|
Context.Source.Position = bookmark;
|
|
NextToken();
|
|
Context.OnError(
|
|
textLocation,
|
|
RazorResources.ParseError_TextTagCannotContainAttributes,
|
|
length: 4 /* text */);
|
|
|
|
RecoverTextTag();
|
|
}
|
|
else
|
|
{
|
|
Accept(tokens);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
if (!empty)
|
|
{
|
|
tags.Push(tag);
|
|
}
|
|
|
|
CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKind.Transition);
|
|
|
|
return true;
|
|
}
|
|
|
|
Accept(_bufferedOpenAngle);
|
|
OptionalBangEscape();
|
|
Optional(HtmlSymbolType.Text);
|
|
return RestOfTag(tag, tags, tagBlockWrapper);
|
|
}
|
|
|
|
private bool RestOfTag(Tuple<HtmlSymbol, SourceLocation> tag,
|
|
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
|
|
IDisposable tagBlockWrapper)
|
|
{
|
|
TagContent();
|
|
|
|
// We are now at a possible end of the tag
|
|
// Found '<', so we just abort this tag.
|
|
if (At(HtmlSymbolType.OpenAngle))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var isEmpty = At(HtmlSymbolType.ForwardSlash);
|
|
// Found a solidus, so don't accept it but DON'T push the tag to the stack
|
|
if (isEmpty)
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
|
|
// Check for the '>' to determine if the tag is finished
|
|
var seenClose = Optional(HtmlSymbolType.CloseAngle);
|
|
if (!seenClose)
|
|
{
|
|
Context.OnError(
|
|
SourceLocation.Advance(tag.Item2, "<"),
|
|
RazorResources.FormatParseError_UnfinishedTag(tag.Item1.Content),
|
|
Math.Max(tag.Item1.Content.Length, 1));
|
|
}
|
|
else
|
|
{
|
|
if (!isEmpty)
|
|
{
|
|
// Is this a void element?
|
|
var tagName = tag.Item1.Content.Trim();
|
|
if (VoidElements.Contains(tagName))
|
|
{
|
|
CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup);
|
|
|
|
// 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. If we see "<", "/", tag name, accept it and the ">" following it
|
|
// Place a bookmark
|
|
var bookmark = CurrentLocation.AbsoluteIndex;
|
|
|
|
// Skip whitespace
|
|
IEnumerable<HtmlSymbol> whiteSpace = ReadWhile(IsSpacingToken(includeNewLines: true));
|
|
|
|
// Open Angle
|
|
if (At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.ForwardSlash))
|
|
{
|
|
var openAngle = CurrentSymbol;
|
|
NextToken();
|
|
Assert(HtmlSymbolType.ForwardSlash);
|
|
var solidus = CurrentSymbol;
|
|
NextToken();
|
|
if (At(HtmlSymbolType.Text) && string.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Accept up to here
|
|
Accept(whiteSpace);
|
|
Output(SpanKind.Markup); // Output the whitespace
|
|
|
|
using (Context.StartBlock(BlockType.Tag))
|
|
{
|
|
Accept(openAngle);
|
|
Accept(solidus);
|
|
AcceptAndMoveNext();
|
|
|
|
// Accept to '>', '<' or EOF
|
|
AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.OpenAngle);
|
|
// Accept the '>' if we saw it. And if we do see it, we're complete
|
|
var complete = Optional(HtmlSymbolType.CloseAngle);
|
|
|
|
if (complete)
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
|
|
// Output the closing void element
|
|
Output(SpanKind.Markup);
|
|
|
|
return complete;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Go back to the bookmark and just finish this tag at the close angle
|
|
Context.Source.Position = bookmark;
|
|
NextToken();
|
|
}
|
|
else if (string.Equals(tagName, ScriptTagName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup);
|
|
|
|
SkipToEndScriptAndParseCode(endTagAcceptedCharacters: AcceptedCharacters.None);
|
|
}
|
|
else
|
|
{
|
|
// Push the tag on to the stack
|
|
tags.Push(tag);
|
|
}
|
|
}
|
|
}
|
|
return seenClose;
|
|
}
|
|
|
|
private void SkipToEndScriptAndParseCode(AcceptedCharacters endTagAcceptedCharacters = AcceptedCharacters.Any)
|
|
{
|
|
// Special case for <script>: Skip to end of script tag and parse code
|
|
var seenEndScript = false;
|
|
|
|
while (!seenEndScript && !EndOfFile)
|
|
{
|
|
SkipToAndParseCode(HtmlSymbolType.OpenAngle);
|
|
var tagStart = CurrentLocation;
|
|
|
|
if (NextIs(HtmlSymbolType.ForwardSlash))
|
|
{
|
|
var openAngle = CurrentSymbol;
|
|
NextToken(); // Skip over '<', current is '/'
|
|
var solidus = CurrentSymbol;
|
|
NextToken(); // Skip over '/', current should be text
|
|
|
|
if (At(HtmlSymbolType.Text) &&
|
|
string.Equals(CurrentSymbol.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 CurrentSymbol to an open angle bracket.
|
|
NextToken();
|
|
}
|
|
|
|
if (seenEndScript)
|
|
{
|
|
Output(SpanKind.Markup);
|
|
|
|
using (Context.StartBlock(BlockType.Tag))
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = endTagAcceptedCharacters;
|
|
|
|
AcceptAndMoveNext(); // '<'
|
|
AcceptAndMoveNext(); // '/'
|
|
SkipToAndParseCode(HtmlSymbolType.CloseAngle);
|
|
if (!Optional(HtmlSymbolType.CloseAngle))
|
|
{
|
|
Context.OnError(
|
|
SourceLocation.Advance(tagStart, "</"),
|
|
RazorResources.FormatParseError_UnfinishedTag(ScriptTagName),
|
|
ScriptTagName.Length);
|
|
}
|
|
Output(SpanKind.Markup);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AcceptAndMoveNext(); // Accept '<' (not the closing script tag's open angle)
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CompleteTagBlockWithSpan(IDisposable tagBlockWrapper,
|
|
AcceptedCharacters acceptedCharacters,
|
|
SpanKind spanKind)
|
|
{
|
|
Debug.Assert(tagBlockWrapper != null,
|
|
"Tag block wrapper should not be null when attempting to complete a block");
|
|
|
|
Span.EditHandler.AcceptedCharacters = acceptedCharacters;
|
|
// Write out the current span into the block before closing it.
|
|
Output(spanKind);
|
|
// Finish the tag block
|
|
tagBlockWrapper.Dispose();
|
|
}
|
|
|
|
private bool AcceptUntilAll(params HtmlSymbolType[] endSequence)
|
|
{
|
|
while (!EndOfFile)
|
|
{
|
|
SkipToAndParseCode(endSequence[0]);
|
|
if (AcceptAll(endSequence))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
Debug.Assert(EndOfFile);
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
|
|
return false;
|
|
}
|
|
|
|
private bool RemoveTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, string tagName, SourceLocation tagStart)
|
|
{
|
|
Tuple<HtmlSymbol, SourceLocation> currentTag = null;
|
|
while (tags.Count > 0)
|
|
{
|
|
currentTag = tags.Pop();
|
|
if (string.Equals(tagName, currentTag.Item1.Content, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Matched the tag
|
|
return true;
|
|
}
|
|
}
|
|
if (currentTag != null)
|
|
{
|
|
Context.OnError(
|
|
SourceLocation.Advance(currentTag.Item2, "<"),
|
|
RazorResources.FormatParseError_MissingEndTag(currentTag.Item1.Content),
|
|
currentTag.Item1.Content.Length);
|
|
}
|
|
else
|
|
{
|
|
Context.OnError(
|
|
SourceLocation.Advance(tagStart, "</"),
|
|
RazorResources.FormatParseError_UnexpectedEndTag(tagName),
|
|
tagName.Length);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void EndTagBlock(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, bool complete)
|
|
{
|
|
if (tags.Count > 0)
|
|
{
|
|
// Ended because of EOF, not matching close tag. Throw error for last tag
|
|
while (tags.Count > 1)
|
|
{
|
|
tags.Pop();
|
|
}
|
|
var tag = tags.Pop();
|
|
Context.OnError(
|
|
SourceLocation.Advance(tag.Item2, "<"),
|
|
RazorResources.FormatParseError_MissingEndTag(tag.Item1.Content),
|
|
tag.Item1.Content.Length);
|
|
}
|
|
else if (complete)
|
|
{
|
|
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
|
|
}
|
|
tags.Clear();
|
|
if (!Context.DesignTimeMode)
|
|
{
|
|
if (At(HtmlSymbolType.WhiteSpace))
|
|
{
|
|
if (Context.LastSpan.Kind == SpanKind.Transition)
|
|
{
|
|
// Output current span content as markup.
|
|
Output(SpanKind.Markup);
|
|
|
|
// Accept and mark the whitespace at the end of a <text> tag as code.
|
|
AcceptWhile(HtmlSymbolType.WhiteSpace);
|
|
Span.ChunkGenerator = new StatementChunkGenerator();
|
|
Output(SpanKind.Code);
|
|
}
|
|
else
|
|
{
|
|
AcceptWhile(HtmlSymbolType.WhiteSpace);
|
|
}
|
|
}
|
|
|
|
if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine)
|
|
{
|
|
AcceptAndMoveNext();
|
|
}
|
|
}
|
|
else if (Span.EditHandler.AcceptedCharacters == AcceptedCharacters.Any)
|
|
{
|
|
AcceptWhile(HtmlSymbolType.WhiteSpace);
|
|
Optional(HtmlSymbolType.NewLine);
|
|
}
|
|
PutCurrentBack();
|
|
|
|
if (!complete)
|
|
{
|
|
AddMarkerSymbolIfNecessary();
|
|
}
|
|
Output(SpanKind.Markup);
|
|
}
|
|
}
|
|
}
|