Modify parser to group html begin/end elements.

- Added a "Tag" block type.
- Wrapped all begin/end elements in a "Tag" Markup block.

#75
This commit is contained in:
N. Taylor Mullen 2014-08-13 17:34:25 -07:00
parent ff944e5948
commit 6114d5d269
8 changed files with 251 additions and 95 deletions

View File

@ -38,7 +38,7 @@ namespace Microsoft.AspNet.Razor.Parser
return "<";
case HtmlSymbolType.Bang:
return "!";
case HtmlSymbolType.Solidus:
case HtmlSymbolType.ForwardSlash:
return "/";
case HtmlSymbolType.QuestionMark:
return "?";

View File

@ -125,25 +125,58 @@ namespace Microsoft.AspNet.Razor.Parser
do
{
SkipToAndParseCode(HtmlSymbolType.OpenAngle);
if (EndOfFile)
// 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
{
EndTagBlock(tags, complete: true);
}
else
{
_bufferedOpenAngle = null;
_lastTagStart = CurrentLocation;
Assert(HtmlSymbolType.OpenAngle);
_bufferedOpenAngle = CurrentSymbol;
SourceLocation tagStart = CurrentLocation;
if (!NextToken())
if (!EndOfFile && !AtSpecialTag)
{
Accept(_bufferedOpenAngle);
EndTagBlock(tags, complete: false);
// 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
{
complete = AfterTagStart(tagStart, tags);
_bufferedOpenAngle = null;
_lastTagStart = CurrentLocation;
Assert(HtmlSymbolType.OpenAngle);
_bufferedOpenAngle = CurrentSymbol;
SourceLocation tagStart = CurrentLocation;
if (!NextToken())
{
Accept(_bufferedOpenAngle);
EndTagBlock(tags, complete: false);
}
else
{
complete = AfterTagStart(tagStart, tags, 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();
}
}
}
@ -152,15 +185,17 @@ namespace Microsoft.AspNet.Razor.Parser
EndTagBlock(tags, complete);
}
private bool AfterTagStart(SourceLocation tagStart, Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
private bool AfterTagStart(SourceLocation tagStart,
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
IDisposable tagBlockWrapper)
{
if (!EndOfFile)
{
switch (CurrentSymbol.Type)
{
case HtmlSymbolType.Solidus:
case HtmlSymbolType.ForwardSlash:
// End Tag
return EndTag(tagStart, tags);
return EndTag(tagStart, tags, tagBlockWrapper);
case HtmlSymbolType.Bang:
// Comment
Accept(_bufferedOpenAngle);
@ -171,7 +206,7 @@ namespace Microsoft.AspNet.Razor.Parser
return XmlPI();
default:
// Start Tag
return StartTag(tags);
return StartTag(tags, tagBlockWrapper);
}
}
if (tags.Count == 0)
@ -234,10 +269,12 @@ namespace Microsoft.AspNet.Razor.Parser
return false;
}
private bool EndTag(SourceLocation tagStart, Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
private bool EndTag(SourceLocation tagStart,
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
IDisposable tagBlockWrapper)
{
// Accept "/" and move next
Assert(HtmlSymbolType.Solidus);
Assert(HtmlSymbolType.ForwardSlash);
HtmlSymbol solidus = CurrentSymbol;
if (!NextToken())
{
@ -258,8 +295,7 @@ namespace Microsoft.AspNet.Razor.Parser
String.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) &&
matched)
{
Output(SpanKind.Markup);
return EndTextTag(solidus);
return EndTextTag(solidus, tagBlockWrapper);
}
Accept(_bufferedOpenAngle);
Accept(solidus);
@ -271,7 +307,16 @@ namespace Microsoft.AspNet.Razor.Parser
}
}
private bool EndTextTag(HtmlSymbol solidus)
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)
{
SourceLocation start = _bufferedOpenAngle.Start;
@ -286,6 +331,9 @@ namespace Microsoft.AspNet.Razor.Parser
if (!seenCloseAngle)
{
Context.OnError(start, RazorResources.ParseError_TextTagCannotContainAttributes);
Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
RecoverTextTag();
}
else
{
@ -293,14 +341,27 @@ namespace Microsoft.AspNet.Razor.Parser
}
Span.CodeGenerator = SpanCodeGenerator.Null;
Output(SpanKind.Transition);
CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKind.Transition);
return seenCloseAngle;
}
// Special tags include <! and <? tags
private bool AtSpecialTag
{
get
{
return (At(HtmlSymbolType.OpenAngle) &&
(NextIs(HtmlSymbolType.Bang) ||
NextIs(HtmlSymbolType.QuestionMark)));
}
}
private bool IsTagRecoveryStopPoint(HtmlSymbol sym)
{
return sym.Type == HtmlSymbolType.CloseAngle ||
sym.Type == HtmlSymbolType.Solidus ||
sym.Type == HtmlSymbolType.ForwardSlash ||
sym.Type == HtmlSymbolType.OpenAngle ||
sym.Type == HtmlSymbolType.SingleQuote ||
sym.Type == HtmlSymbolType.DoubleQuote;
@ -325,7 +386,7 @@ namespace Microsoft.AspNet.Razor.Parser
private bool IsEndOfTag()
{
if (At(HtmlSymbolType.Solidus))
if (At(HtmlSymbolType.ForwardSlash))
{
if (NextIs(HtmlSymbolType.CloseAngle))
{
@ -364,7 +425,7 @@ namespace Microsoft.AspNet.Razor.Parser
sym.Type != HtmlSymbolType.Equals &&
sym.Type != HtmlSymbolType.CloseAngle &&
sym.Type != HtmlSymbolType.OpenAngle &&
(sym.Type != HtmlSymbolType.Solidus || !NextIs(HtmlSymbolType.CloseAngle)));
(sym.Type != HtmlSymbolType.ForwardSlash || !NextIs(HtmlSymbolType.CloseAngle)));
}
else
{
@ -470,12 +531,16 @@ namespace Microsoft.AspNet.Razor.Parser
// Dynamic value, start a new block and set the code generator
using (Context.StartBlock(BlockType.Markup))
{
Context.CurrentBlock.CodeGenerator = new DynamicAttributeBlockCodeGenerator(prefix.GetContent(prefixStart), valueStart);
Context.CurrentBlock.CodeGenerator =
new DynamicAttributeBlockCodeGenerator(prefix.GetContent(prefixStart), valueStart);
OtherParserBlock();
}
}
else if (At(HtmlSymbolType.Text) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == '~' && NextIs(HtmlSymbolType.Solidus))
else if (At(HtmlSymbolType.Text) &&
CurrentSymbol.Content.Length > 0 &&
CurrentSymbol.Content[0] == '~' &&
NextIs(HtmlSymbolType.ForwardSlash))
{
// Virtual Path value
SourceLocation valueStart = CurrentLocation;
@ -489,11 +554,12 @@ namespace Microsoft.AspNet.Razor.Parser
// 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
// 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)
// 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.CodeGenerator = new LiteralAttributeCodeGenerator(prefix.GetContent(prefixStart), value.GetContent(prefixStart));
@ -518,7 +584,7 @@ namespace Microsoft.AspNet.Razor.Parser
sym.Type == HtmlSymbolType.SingleQuote ||
sym.Type == HtmlSymbolType.OpenAngle ||
sym.Type == HtmlSymbolType.Equals ||
(sym.Type == HtmlSymbolType.Solidus && NextIs(HtmlSymbolType.CloseAngle)) ||
(sym.Type == HtmlSymbolType.ForwardSlash && NextIs(HtmlSymbolType.CloseAngle)) ||
sym.Type == HtmlSymbolType.CloseAngle ||
sym.Type == HtmlSymbolType.WhiteSpace ||
sym.Type == HtmlSymbolType.NewLine;
@ -553,7 +619,7 @@ namespace Microsoft.AspNet.Razor.Parser
break;
case HtmlSymbolType.OpenAngle:
// Another "<" means this tag is invalid.
case HtmlSymbolType.Solidus:
case HtmlSymbolType.ForwardSlash:
// Empty tag
case HtmlSymbolType.CloseAngle:
// End of tag
@ -583,7 +649,7 @@ namespace Microsoft.AspNet.Razor.Parser
}
}
private bool StartTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
private bool StartTag(Stack<Tuple<HtmlSymbol, SourceLocation>> tags, IDisposable tagBlockWrapper)
{
// If we're at text, it's the name, otherwise the name is ""
HtmlSymbol tagName;
@ -610,11 +676,11 @@ namespace Microsoft.AspNet.Razor.Parser
int bookmark = CurrentLocation.AbsoluteIndex;
IEnumerable<HtmlSymbol> tokens = ReadWhile(IsSpacingToken(includeNewLines: true));
bool empty = At(HtmlSymbolType.Solidus);
bool empty = At(HtmlSymbolType.ForwardSlash);
if (empty)
{
Accept(tokens);
Assert(HtmlSymbolType.Solidus);
Assert(HtmlSymbolType.ForwardSlash);
AcceptAndMoveNext();
bookmark = CurrentLocation.AbsoluteIndex;
tokens = ReadWhile(IsSpacingToken(includeNewLines: true));
@ -625,6 +691,8 @@ namespace Microsoft.AspNet.Razor.Parser
Context.Source.Position = bookmark;
NextToken();
Context.OnError(tag.Item2, RazorResources.ParseError_TextTagCannotContainAttributes);
RecoverTextTag();
}
else
{
@ -636,15 +704,19 @@ namespace Microsoft.AspNet.Razor.Parser
{
tags.Push(tag);
}
Output(SpanKind.Transition);
CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKind.Transition);
return true;
}
Accept(_bufferedOpenAngle);
Optional(HtmlSymbolType.Text);
return RestOfTag(tag, tags);
return RestOfTag(tag, tags, tagBlockWrapper);
}
private bool RestOfTag(Tuple<HtmlSymbol, SourceLocation> tag, Stack<Tuple<HtmlSymbol, SourceLocation>> tags)
private bool RestOfTag(Tuple<HtmlSymbol, SourceLocation> tag,
Stack<Tuple<HtmlSymbol, SourceLocation>> tags,
IDisposable tagBlockWrapper)
{
TagContent();
@ -655,7 +727,7 @@ namespace Microsoft.AspNet.Razor.Parser
return false;
}
bool isEmpty = At(HtmlSymbolType.Solidus);
bool isEmpty = At(HtmlSymbolType.ForwardSlash);
// Found a solidus, so don't accept it but DON'T push the tag to the stack
if (isEmpty)
{
@ -676,44 +748,63 @@ namespace Microsoft.AspNet.Razor.Parser
string 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
int bookmark = CurrentLocation.AbsoluteIndex;
// Skip whitespace
IEnumerable<HtmlSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true));
IEnumerable<HtmlSymbol> whiteSpace = ReadWhile(IsSpacingToken(includeNewLines: true));
// Open Angle
if (At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.Solidus))
if (At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.ForwardSlash))
{
HtmlSymbol openAngle = CurrentSymbol;
NextToken();
Assert(HtmlSymbolType.Solidus);
Assert(HtmlSymbolType.ForwardSlash);
HtmlSymbol solidus = CurrentSymbol;
NextToken();
if (At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase))
if (At(HtmlSymbolType.Text) && string.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase))
{
// Accept up to here
Accept(ws);
Accept(openAngle);
Accept(solidus);
AcceptAndMoveNext();
Accept(whiteSpace);
Output(SpanKind.Markup); // Output the whitespace
// Accept to '>', '<' or EOF
AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.OpenAngle);
// Accept the '>' if we saw it. And if we do see it, we're complete
return Optional(HtmlSymbolType.CloseAngle);
} // At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase)
} // At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.Solidus)
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, "script", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(tagName, "script", StringComparison.OrdinalIgnoreCase))
{
SkipToEndScriptAndParseCode();
CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup);
SkipToEndScriptAndParseCode(endTagAcceptedCharacters: AcceptedCharacters.None);
}
else
{
@ -725,33 +816,74 @@ namespace Microsoft.AspNet.Razor.Parser
return seenClose;
}
private void SkipToEndScriptAndParseCode()
private void SkipToEndScriptAndParseCode(AcceptedCharacters endTagAcceptedCharacters = AcceptedCharacters.Any)
{
// Special case for <script>: Skip to end of script tag and parse code
bool seenEndScript = false;
while (!seenEndScript && !EndOfFile)
{
SkipToAndParseCode(HtmlSymbolType.OpenAngle);
SourceLocation tagStart = CurrentLocation;
AcceptAndMoveNext();
AcceptWhile(HtmlSymbolType.WhiteSpace);
if (Optional(HtmlSymbolType.Solidus))
if (NextIs(HtmlSymbolType.ForwardSlash))
{
AcceptWhile(HtmlSymbolType.WhiteSpace);
if (At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, "script", StringComparison.OrdinalIgnoreCase))
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, "script", StringComparison.OrdinalIgnoreCase))
{
// </script!
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 '<'
}
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(tagStart, RazorResources.FormatParseError_UnfinishedTag("script"));
}
seenEndScript = true;
Output(SpanKind.Markup);
}
}
else
{
AcceptAndMoveNext(); // '<' (not the closing script tags 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)

View File

@ -36,37 +36,65 @@ namespace Microsoft.AspNet.Razor.Parser
/// Reads the content of a tag (if present) in the MarkupDocument (or MarkupSection) context,
/// where we don't care about maintaining a stack of tags.
/// </summary>
/// <returns>A boolean indicating if we scanned at least one tag.</returns>
private bool ScanTagInDocumentContext()
private void ScanTagInDocumentContext()
{
if (Optional(HtmlSymbolType.OpenAngle))
if (At(HtmlSymbolType.OpenAngle))
{
if (At(HtmlSymbolType.Bang))
if (NextIs(HtmlSymbolType.Bang))
{
AcceptAndMoveNext(); // Accept '<'
BangTag();
return true;
}
else if (At(HtmlSymbolType.QuestionMark))
else if (NextIs(HtmlSymbolType.QuestionMark))
{
AcceptAndMoveNext(); // Accept '<'
XmlPI();
return true;
}
else if (!At(HtmlSymbolType.Solidus))
else
{
bool scriptTag = At(HtmlSymbolType.Text) &&
String.Equals(CurrentSymbol.Content, "script", StringComparison.OrdinalIgnoreCase);
Optional(HtmlSymbolType.Text);
TagContent(); // Parse the tag, don't care about the content
Optional(HtmlSymbolType.Solidus);
Optional(HtmlSymbolType.CloseAngle);
if (scriptTag)
Output(SpanKind.Markup);
// Start tag block
var tagBlock = Context.StartBlock(BlockType.Tag);
AcceptAndMoveNext(); // Accept '<'
if (!At(HtmlSymbolType.ForwardSlash))
{
SkipToEndScriptAndParseCode();
// Parsing a start tag
var scriptTag = At(HtmlSymbolType.Text) &&
string.Equals(CurrentSymbol.Content, "script", StringComparison.OrdinalIgnoreCase);
Optional(HtmlSymbolType.Text);
TagContent(); // Parse the tag, don't care about the content
Optional(HtmlSymbolType.ForwardSlash);
Optional(HtmlSymbolType.CloseAngle);
if (scriptTag)
{
Output(SpanKind.Markup);
tagBlock.Dispose();
SkipToEndScriptAndParseCode();
return;
}
}
return true;
else
{
// Parsing an end tag
// This section can accept things like: '</p >' or '</p>' etc.
Optional(HtmlSymbolType.ForwardSlash);
// Whitespace here is invalid (according to the spec)
Optional(HtmlSymbolType.Text);
AcceptAll(HtmlSymbolType.WhiteSpace);
Optional(HtmlSymbolType.CloseAngle);
}
Output(SpanKind.Markup);
// End tag block
tagBlock.Dispose();
}
}
return false;
}
}
}

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.Parser.SyntaxTree
Template,
// Special
Comment
Comment,
Tag
}
}

View File

@ -133,7 +133,7 @@ namespace Microsoft.AspNet.Razor.Tokenizer
case '!':
return EndSymbol(HtmlSymbolType.Bang);
case '/':
return EndSymbol(HtmlSymbolType.Solidus);
return EndSymbol(HtmlSymbolType.ForwardSlash);
case '?':
return EndSymbol(HtmlSymbolType.QuestionMark);
case '[':

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNet.Razor.Tokenizer.Symbols
NewLine, // Newline
OpenAngle, // <
Bang, // !
Solidus, // /
ForwardSlash, // /
QuestionMark, // ?
DoubleHyphen, // --
LeftBracket, // [

View File

@ -8,6 +8,7 @@ namespace Microsoft.AspNet.Razor.Utils
internal class DisposableAction : IDisposable
{
private Action _action;
private bool _invoked;
public DisposableAction(Action action)
{
@ -20,16 +21,10 @@ namespace Microsoft.AspNet.Razor.Utils
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
// If we were disposed by the finalizer it's because the user didn't use a "using" block, so don't do anything!
if (disposing)
if (!_invoked)
{
_action();
_invoked = true;
}
}
}

View File

@ -66,7 +66,7 @@ namespace Microsoft.AspNet.Razor.Test.Tokenizer
[Fact]
public void Solidus_Is_Recognized()
{
TestSingleToken("/", HtmlSymbolType.Solidus);
TestSingleToken("/", HtmlSymbolType.ForwardSlash);
}
[Fact]