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 static LookupInfo GetLookupInfo( TagHelperDirectiveDescriptor directiveDescriptor, ErrorSink errorSink) { var lookupText = directiveDescriptor.DirectiveText; var lookupStrings = lookupText?.Split(new[] { ',' }); // Ensure that we have valid lookupStrings to work with. The valid format is "typeName, assemblyName" if (lookupStrings == null || lookupStrings.Any(string.IsNullOrWhiteSpace) || lookupStrings.Length != 2) { errorSink.OnError( directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText), GetErrorLength(lookupText)); return(null); } var trimmedAssemblyName = lookupStrings[1].Trim(); // + 1 is for the comma separator in the lookup text. var assemblyNameIndex = lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName, StringComparison.Ordinal); var assemblyNamePrefix = directiveDescriptor.DirectiveText.Substring(0, assemblyNameIndex); var assemblyNameLocation = SourceLocation.Advance(directiveDescriptor.Location, assemblyNamePrefix); return(new LookupInfo { TypePattern = lookupStrings[0].Trim(), AssemblyName = trimmedAssemblyName, AssemblyNameLocation = assemblyNameLocation, }); }
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 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 && At(HtmlSymbolType.NewLine) && Context.LastSpan.Kind != SpanKind.Transition) { AcceptAndMoveNext(); } } else if (Span.EditHandler.AcceptedCharacters == AcceptedCharacters.Any) { AcceptWhile(HtmlSymbolType.WhiteSpace); Optional(HtmlSymbolType.NewLine); } PutCurrentBack(); if (!complete) { AddMarkerSymbolIfNecessary(); } Output(SpanKind.Markup); }
public void SymbolBoundAttributes_Whitespace(string attributeName) { // Arrange var attributeNameLength = attributeName.Length; var newlineLength = Environment.NewLine.Length; var prefixLocation1 = new SourceLocation( absoluteIndex: 2, lineIndex: 0, characterIndex: 2); var suffixLocation1 = new SourceLocation( absoluteIndex: 10 + newlineLength + attributeNameLength, lineIndex: 1, characterIndex: 5 + attributeNameLength + newlineLength); var valueLocation1 = new SourceLocation( absoluteIndex: 7 + attributeNameLength + newlineLength, lineIndex: 1, characterIndex: 4 + attributeNameLength); var prefixLocation2 = SourceLocation.Advance(suffixLocation1, "'"); var suffixLocation2 = new SourceLocation( absoluteIndex: 17 + attributeNameLength * 2 + newlineLength * 2, lineIndex: 2, characterIndex: 5 + attributeNameLength); var valueLocation2 = new SourceLocation( absoluteIndex: 14 + attributeNameLength * 2 + newlineLength * 2, lineIndex: 2, characterIndex: 2 + attributeNameLength); // Act & Assert ParseBlockTest( $"<a {Environment.NewLine} {attributeName}='Foo'\t{Environment.NewLine}{attributeName}='Bar' />", new MarkupBlock( new MarkupTagBlock( Factory.Markup("<a"), new MarkupBlock( new AttributeBlockChunkGenerator( attributeName, prefix: new LocationTagged <string>( $" {Environment.NewLine} {attributeName}='", prefixLocation1), suffix: new LocationTagged <string>("'", suffixLocation1)), Factory.Markup($" {Environment.NewLine} {attributeName}='").With(SpanChunkGenerator.Null), Factory.Markup("Foo").With( new LiteralAttributeChunkGenerator( prefix: new LocationTagged <string>(string.Empty, valueLocation1), value: new LocationTagged <string>("Foo", valueLocation1))), Factory.Markup("'").With(SpanChunkGenerator.Null)), new MarkupBlock( new AttributeBlockChunkGenerator( attributeName, prefix: new LocationTagged <string>( $"\t{Environment.NewLine}{attributeName}='", prefixLocation2), suffix: new LocationTagged <string>("'", suffixLocation2)), Factory.Markup($"\t{Environment.NewLine}{attributeName}='").With(SpanChunkGenerator.Null), Factory.Markup("Bar").With( new LiteralAttributeChunkGenerator( prefix: new LocationTagged <string>(string.Empty, valueLocation2), value: new LocationTagged <string>("Bar", valueLocation2))), Factory.Markup("'").With(SpanChunkGenerator.Null)), Factory.Markup(" />").Accepts(AcceptedCharacters.None)))); }
private static SourceLocation GetSubTextSourceLocation(Span span, string text) { var startOffset = span.Content.IndexOf(text); var offsetContent = span.Content.Substring(0, startOffset); var offsetTextLocation = SourceLocation.Advance(span.Start, offsetContent); return(offsetTextLocation); }
public override void VisitSpan(Span span) { if (span == null) { throw new ArgumentNullException(nameof(span)); } string directiveText; TagHelperDirectiveType directiveType; var addTagHelperChunkGenerator = span.ChunkGenerator as AddTagHelperChunkGenerator; var removeTagHelperChunkGenerator = span.ChunkGenerator as RemoveTagHelperChunkGenerator; var tagHelperPrefixChunkGenerator = span.ChunkGenerator as TagHelperPrefixDirectiveChunkGenerator; if (addTagHelperChunkGenerator != null) { directiveType = TagHelperDirectiveType.AddTagHelper; directiveText = addTagHelperChunkGenerator.LookupText; } else if (removeTagHelperChunkGenerator != null) { directiveType = TagHelperDirectiveType.RemoveTagHelper; directiveText = removeTagHelperChunkGenerator.LookupText; } else if (tagHelperPrefixChunkGenerator != null) { directiveType = TagHelperDirectiveType.TagHelperPrefix; directiveText = tagHelperPrefixChunkGenerator.Prefix; } else { // Not a chunk generator that we're interested in. return; } directiveText = directiveText.Trim(); var startOffset = span.Content.IndexOf(directiveText, StringComparison.Ordinal); var offsetContent = span.Content.Substring(0, startOffset); var offsetTextLocation = SourceLocation.Advance(span.Start, offsetContent); var directiveDescriptor = new TagHelperDirectiveDescriptor { DirectiveText = directiveText, Location = offsetTextLocation, DirectiveType = directiveType }; _directiveDescriptors.Add(directiveDescriptor); }
private void ValidateParentAllowsContent(Span child, ErrorSink errorSink) { if (HasAllowedChildren()) { var content = child.Content; if (!string.IsNullOrWhiteSpace(content)) { var trimmedStart = content.TrimStart(); var whitespace = content.Substring(0, content.Length - trimmedStart.Length); var errorStart = SourceLocation.Advance(child.Start, whitespace); var length = trimmedStart.TrimEnd().Length; var allowedChildren = _currentTagHelperTracker.AllowedChildren; var allowedChildrenString = string.Join(", ", allowedChildren); errorSink.OnError( errorStart, RazorResources.FormatTagHelperParseTreeRewriter_CannotHaveNonTagContent( _currentTagHelperTracker.TagName, allowedChildrenString), length); } } }
private void BuildMalformedTagHelpers(int count, RewritingContext context) { for (var i = 0; i < count; i++) { var tracker = _trackerStack.Peek(); // Skip all non-TagHelper entries. Non TagHelper trackers do not need to represent well-formed HTML. if (!tracker.IsTagHelper) { PopTrackerStack(); continue; } var malformedTagHelper = ((TagHelperBlockTracker)tracker).Builder; context.ErrorSink.OnError( SourceLocation.Advance(malformedTagHelper.Start, "<"), RazorResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper( malformedTagHelper.TagName), malformedTagHelper.TagName.Length); BuildCurrentlyTrackedTagHelperBlock(endTag: null); } }
// This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. private static TryParseResult TryParseSpan( Span span, IEnumerable <TagHelperDescriptor> descriptors, ErrorSink errorSink) { var afterEquals = false; var builder = new SpanBuilder { ChunkGenerator = span.ChunkGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; // Will contain symbols that represent a single attribute value: <input| class="btn"| /> var htmlSymbols = span.Symbols.OfType <HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; // Default to DoubleQuotes. We purposefully do not persist NoQuotes ValueStyle to stay consistent with the // TryParseBlock() variation of attribute parsing. var attributeValueStyle = HtmlAttributeValueStyle.DoubleQuotes; // The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for // the attribute value the symbolOffset is adjusted accordingly. var symbolOffset = 0; string name = null; // Iterate down through the symbols to find the name and the start of the value. // We subtract the symbolOffset so we don't accept an ending quote of a span. for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++) { var symbol = htmlSymbols[i]; if (afterEquals) { // We've captured all leading whitespace, the attribute name, and an equals with an optional // quote/double quote. We're now at: " asp-for='|...'" or " asp-for=|..." // The goal here is to capture all symbols until the end of the attribute. Note this will not // consume an ending quote due to the symbolOffset. // When symbols are accepted into SpanBuilders, their locations get altered to be offset by the // parent which is why we need to mark our start location prior to adding the symbol. // This is needed to know the location of the attribute value start within the document. if (!capturedAttributeValueStart) { capturedAttributeValueStart = true; attributeValueStartLocation = span.Start + symbol.Start; } builder.Accept(symbol); } else if (name == null && HtmlMarkupParser.IsValidAttributeNameSymbol(symbol)) { // We've captured all leading whitespace prior to the attribute name. // We're now at: " |asp-for='...'" or " |asp-for=..." // The goal here is to capture the attribute name. var symbolContents = htmlSymbols .Skip(i) // Skip prefix .TakeWhile(nameSymbol => HtmlMarkupParser.IsValidAttributeNameSymbol(nameSymbol)) .Select(nameSymbol => nameSymbol.Content); // Move the indexer past the attribute name symbols. i += symbolContents.Count() - 1; name = string.Concat(symbolContents); attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." // The goal here is to consume the equal sign and the optional single/double-quote. // The coming symbols will either be a quote or value (in the case that the value is unquoted). SourceLocation symbolStartLocation; // Skip the whitespace preceding the start of the attribute value. do { i++; // Start from the symbol after '='. } while (i < htmlSymbols.Length && (htmlSymbols[i].Type == HtmlSymbolType.WhiteSpace || htmlSymbols[i].Type == HtmlSymbolType.NewLine)); // Check for attribute start values, aka single or double quote if (i < htmlSymbols.Length && IsQuote(htmlSymbols[i])) { if (htmlSymbols[i].Type == HtmlSymbolType.SingleQuote) { attributeValueStyle = HtmlAttributeValueStyle.SingleQuotes; } symbolStartLocation = htmlSymbols[i].Start; // If there's a start quote then there must be an end quote to be valid, skip it. symbolOffset = 1; } else { // We are at the symbol after equals. Go back to equals to ensure we don't skip past that symbol. i--; symbolStartLocation = symbol.Start; } attributeValueStartLocation = span.Start + symbolStartLocation + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } else if (symbol.Type == HtmlSymbolType.WhiteSpace) { // We're at the start of the attribute, this branch may be hit on the first iterations of // the loop since the parser separates attributes with their spaces included as symbols. // We're at: "| asp-for='...'" or "| asp-for=..." // Note: This will not be hit even for situations like asp-for ="..." because the core Razor // parser currently does not know how to handle attributes in that format. This will be addressed // by https://github.com/aspnet/Razor/issues/123. attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content); } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify each symbol's Start location. builder.Start = attributeValueStartLocation; if (name == null) { // We couldn't find a name, if the original span content was whitespace it ultimately means the tag // that owns this "attribute" is malformed and is expecting a user to type a new attribute. // ex: <myTH class="btn"| | if (!string.IsNullOrWhiteSpace(span.Content)) { errorSink.OnError( span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributeListMustBeWellFormed, span.Content.Length); } return(null); } var result = CreateTryParseResult(name, descriptors); // If we're not after an equal then we should treat the value as if it were a minimized attribute. Span attributeValue = null; if (afterEquals) { attributeValue = CreateMarkupAttribute(builder, result.IsBoundNonStringAttribute); } else { attributeValueStyle = HtmlAttributeValueStyle.Minimized; } result.AttributeValueNode = attributeValue; result.AttributeValueStyle = attributeValueStyle; return(result); }
// This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. private static bool TryParseSpan( Span span, IReadOnlyDictionary <string, string> attributeValueTypes, ParserErrorSink errorSink, out KeyValuePair <string, SyntaxTreeNode> attribute) { var afterEquals = false; var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; // Will contain symbols that represent a single attribute value: <input| class="btn"| /> var htmlSymbols = span.Symbols.OfType <HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; // The symbolOffset is initialized to 0 to expect worst case: "class=". If a quote is found later on for // the attribute value the symbolOffset is adjusted accordingly. var symbolOffset = 0; string name = null; // Iterate down through the symbols to find the name and the start of the value. // We subtract the symbolOffset so we don't accept an ending quote of a span. for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++) { var symbol = htmlSymbols[i]; if (afterEquals) { // We've captured all leading whitespace, the attribute name, and an equals with an optional // quote/double quote. We're now at: " asp-for='|...'" or " asp-for=|..." // The goal here is to capture all symbols until the end of the attribute. Note this will not // consume an ending quote due to the symbolOffset. // When symbols are accepted into SpanBuilders, their locations get altered to be offset by the // parent which is why we need to mark our start location prior to adding the symbol. // This is needed to know the location of the attribute value start within the document. if (!capturedAttributeValueStart) { capturedAttributeValueStart = true; attributeValueStartLocation = span.Start + symbol.Start; } builder.Accept(symbol); } else if (name == null && symbol.Type == HtmlSymbolType.Text) { // We've captured all leading whitespace prior to the attribute name. // We're now at: " |asp-for='...'" or " |asp-for=..." // The goal here is to capture the attribute name. name = symbol.Content; attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, name); } else if (symbol.Type == HtmlSymbolType.Equals) { Debug.Assert( name != null, "Name should never be null here. The parser should guaruntee an attribute has a name."); // We've captured all leading whitespace and the attribute name. // We're now at: " asp-for|='...'" or " asp-for|=..." // The goal here is to consume the equal sign and the optional single/double-quote. // The coming symbols will either be a quote or value (in the case that the value is unquoted). // Spaces after/before the equal symbol are not yet supported: // https://github.com/aspnet/Razor/issues/123 // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. // https://github.com/aspnet/Razor/issues/104 SourceLocation symbolStartLocation; // Check for attribute start values, aka single or double quote if ((i + 1) < htmlSymbols.Length && IsQuote(htmlSymbols[i + 1])) { // Move past the attribute start so we can accept the true value. i++; symbolStartLocation = htmlSymbols[i].Start; // If there's a start quote then there must be an end quote to be valid, skip it. symbolOffset = 1; } else { symbolStartLocation = symbol.Start; } attributeValueStartLocation = span.Start + symbolStartLocation + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } else if (symbol.Type == HtmlSymbolType.WhiteSpace) { // We're at the start of the attribute, this branch may be hit on the first iterations of // the loop since the parser separates attributes with their spaces included as symbols. // We're at: "| asp-for='...'" or "| asp-for=..." // Note: This will not be hit even for situations like asp-for ="..." because the core Razor // parser currently does not know how to handle attributes in that format. This will be addressed // by https://github.com/aspnet/Razor/issues/123. attributeValueStartLocation = SourceLocation.Advance(attributeValueStartLocation, symbol.Content); } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify each symbol's Start location. builder.Start = attributeValueStartLocation; if (name == null) { // We couldn't find a name, if the original span content was whitespace it ultimately means the tag // that owns this "attribute" is malformed and is expecting a user to type a new attribute. // ex: <myTH class="btn"| | if (!string.IsNullOrWhiteSpace(span.Content)) { errorSink.OnError( span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed, span.Content.Length); } attribute = default(KeyValuePair <string, SyntaxTreeNode>); return(false); } attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); return(true); }
// This method handles cases when the attribute is a simple span attribute such as // class="something moresomething". This does not handle complex attributes such as // class="@myclass". Therefore the span.Content is equivalent to the entire attribute. private static bool TryParseSpan( Span span, IReadOnlyDictionary <string, string> attributeValueTypes, ParserErrorSink errorSink, out KeyValuePair <string, SyntaxTreeNode> attribute) { var afterEquals = false; var builder = new SpanBuilder { CodeGenerator = span.CodeGenerator, EditHandler = span.EditHandler, Kind = span.Kind }; var htmlSymbols = span.Symbols.OfType <HtmlSymbol>().ToArray(); var capturedAttributeValueStart = false; var attributeValueStartLocation = span.Start; var symbolOffset = 1; string name = null; // Iterate down through the symbols to find the name and the start of the value. // We subtract the symbolOffset so we don't accept an ending quote of a span. for (var i = 0; i < htmlSymbols.Length - symbolOffset; i++) { var symbol = htmlSymbols[i]; if (afterEquals) { // When symbols are accepted into SpanBuilders, their locations get altered to be offset by the // parent which is why we need to mark our start location prior to adding the symbol. // This is needed to know the location of the attribute value start within the document. if (!capturedAttributeValueStart) { capturedAttributeValueStart = true; attributeValueStartLocation = span.Start + symbol.Start; } builder.Accept(symbol); } else if (name == null && symbol.Type == HtmlSymbolType.Text) { name = symbol.Content; attributeValueStartLocation = SourceLocation.Advance(span.Start, name); } else if (symbol.Type == HtmlSymbolType.Equals) { // We've found an '=' symbol, this means that the coming symbols will either be a quote // or value (in the case that the value is unquoted). // Spaces after/before the equal symbol are not yet supported: // https://github.com/aspnet/Razor/issues/123 // TODO: Handle malformed tags, if there's an '=' then there MUST be a value. // https://github.com/aspnet/Razor/issues/104 SourceLocation symbolStartLocation; // Check for attribute start values, aka single or double quote if (IsQuote(htmlSymbols[i + 1])) { // Move past the attribute start so we can accept the true value. i++; symbolStartLocation = htmlSymbols[i + 1].Start; } else { symbolStartLocation = symbol.Start; // Set the symbol offset to 0 so we don't attempt to skip an end quote that doesn't exist. symbolOffset = 0; } attributeValueStartLocation = symbolStartLocation + span.Start + new SourceLocation(absoluteIndex: 1, lineIndex: 0, characterIndex: 1); afterEquals = true; } } // After all symbols have been added we need to set the builders start position so we do not indirectly // modify each symbol's Start location. builder.Start = attributeValueStartLocation; if (name == null) { errorSink.OnError(span.Start, RazorResources.TagHelperBlockRewriter_TagHelperAttributesMustBeWelformed, span.Content.Length); attribute = default(KeyValuePair <string, SyntaxTreeNode>); return(false); } attribute = CreateMarkupAttribute(name, builder, attributeValueTypes); return(true); }
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)) { if (!CurrentScriptTagExpectsHtml()) { CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup); SkipToEndScriptAndParseCode(endTagAcceptedCharacters: AcceptedCharacters.None); } else { // Push the script tag onto the tag stack, it should be treated like all other HTML tags. tags.Push(tag); } } else { // Push the tag on to the stack tags.Push(tag); } } } return(seenClose); }
private static SourceLocation GetTagDeclarationErrorStart(Block tagBlock) { var advanceBy = IsEndTag(tagBlock) ? "</" : "<"; return(SourceLocation.Advance(tagBlock.Start, advanceBy)); }
private bool TryRewriteTagHelper(Block tagBlock, RewritingContext context) { // Get tag name of the current block (doesn't matter if it's an end or start tag) var tagName = GetTagName(tagBlock); // Could not determine tag name, it can't be a TagHelper, continue on and track the element. if (tagName == null) { return(false); } var descriptors = Enumerable.Empty <TagHelperDescriptor>(); if (!IsPotentialTagHelper(tagName, tagBlock)) { return(false); } var tracker = _currentTagHelperTracker; var tagNameScope = tracker?.TagName ?? string.Empty; if (!IsEndTag(tagBlock)) { // We're now in a start tag block, we first need to see if the tag block is a tag helper. var providedAttributes = GetAttributeNameValuePairs(tagBlock); descriptors = _provider.GetDescriptors(tagName, providedAttributes, _currentParentTagName); // If there aren't any TagHelperDescriptors registered then we aren't a TagHelper if (!descriptors.Any()) { // If the current tag matches the current TagHelper scope it means the parent TagHelper matched // all the required attributes but the current one did not; therefore, we need to increment the // OpenMatchingTags counter for current the TagHelperBlock so we don't end it too early. // ex: <myth req="..."><myth></myth></myth> We don't want the first myth to close on the inside // tag. if (string.Equals(tagNameScope, tagName, StringComparison.OrdinalIgnoreCase)) { tracker.OpenMatchingTags++; } return(false); } ValidateParentAllowsTagHelper(tagName, tagBlock, context.ErrorSink); ValidateDescriptors(descriptors, tagName, tagBlock, context.ErrorSink); // We're in a start TagHelper block. var validTagStructure = ValidateTagSyntax(tagName, tagBlock, context); var builder = TagHelperBlockRewriter.Rewrite( tagName, validTagStructure, tagBlock, descriptors, context.ErrorSink); // Track the original start tag so the editor knows where each piece of the TagHelperBlock lies // for formatting. builder.SourceStartTag = tagBlock; // Found a new tag helper block TrackTagHelperBlock(builder); // If it's a non-content expecting block then we don't have to worry about nested children within the // tag. Complete it. if (builder.TagMode == TagMode.SelfClosing || builder.TagMode == TagMode.StartTagOnly) { BuildCurrentlyTrackedTagHelperBlock(endTag: null); } } else { // Validate that our end tag matches the currently scoped tag, if not we may need to error. if (tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase)) { // If there are additional end tags required before we can build our block it means we're in a // situation like this: <myth req="..."><myth></myth></myth> where we're at the inside </myth>. if (tracker.OpenMatchingTags > 0) { tracker.OpenMatchingTags--; return(false); } ValidateTagSyntax(tagName, tagBlock, context); BuildCurrentlyTrackedTagHelperBlock(tagBlock); } else { descriptors = _provider.GetDescriptors( tagName, attributes: Enumerable.Empty <KeyValuePair <string, string> >(), parentTagName: _currentParentTagName); // If there are not TagHelperDescriptors associated with the end tag block that also have no // required attributes then it means we can't be a TagHelper, bail out. if (!descriptors.Any()) { return(false); } var invalidDescriptor = descriptors.FirstOrDefault( descriptor => descriptor.TagStructure == TagStructure.WithoutEndTag); if (invalidDescriptor != null) { // End tag TagHelper that states it shouldn't have an end tag. context.ErrorSink.OnError( SourceLocation.Advance(tagBlock.Start, "</"), RazorResources.FormatTagHelperParseTreeRewriter_EndTagTagHelperMustNotHaveAnEndTag( tagName, invalidDescriptor.TypeName, invalidDescriptor.TagStructure), tagName.Length); return(false); } // Current tag helper scope does not match the end tag. Attempt to recover the tag // helper by looking up the previous tag helper scopes for a matching tag. If we // can't recover it means there was no corresponding tag helper start tag. if (TryRecoverTagHelper(tagName, tagBlock, context)) { ValidateParentAllowsTagHelper(tagName, tagBlock, context.ErrorSink); ValidateTagSyntax(tagName, tagBlock, context); // Successfully recovered, move onto the next element. } else { // Could not recover, the end tag helper has no corresponding start tag, create // an error based on the current childBlock. context.ErrorSink.OnError( SourceLocation.Advance(tagBlock.Start, "</"), RazorResources.FormatTagHelpersParseTreeRewriter_FoundMalformedTagHelper(tagName), tagName.Length); return(false); } } } return(true); }
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) { var shouldAcceptWhitespaceAndNewLine = true; if (Context.LastSpan.Kind == SpanKind.Transition) { var symbols = ReadWhile( f => (f.Type == HtmlSymbolType.WhiteSpace) || (f.Type == HtmlSymbolType.NewLine)); // Make sure the current symbol is not markup, which can be html start tag or @: if (!(At(HtmlSymbolType.OpenAngle) || (At(HtmlSymbolType.Transition) && Lookahead(count: 1).Content.StartsWith(":")))) { // Don't accept whitespace as markup if the end text tag is followed by csharp. shouldAcceptWhitespaceAndNewLine = false; } PutCurrentBack(); PutBack(symbols); EnsureCurrent(); } if (shouldAcceptWhitespaceAndNewLine) { // Accept whitespace and a single newline if present AcceptWhile(HtmlSymbolType.WhiteSpace); Optional(HtmlSymbolType.NewLine); } } else if (Span.EditHandler.AcceptedCharacters == AcceptedCharacters.Any) { AcceptWhile(HtmlSymbolType.WhiteSpace); Optional(HtmlSymbolType.NewLine); } PutCurrentBack(); if (!complete) { AddMarkerSymbolIfNecessary(); } Output(SpanKind.Markup); }