// 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>()); } 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 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>()); } } 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> 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

or 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> 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> 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 doesn't match here. // 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