Пример #1
0
        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,
            });
        }
Пример #3
0
        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)
                }
            }
        }
Пример #4
0
        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);
        }
Пример #5
0
        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))));
        }
Пример #6
0
        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);
        }
Пример #7
0
        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);
        }
Пример #11
0
        // 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);
        }
Пример #12
0
        // 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);
        }
Пример #13
0
        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);
        }
Пример #16
0
        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);
        }