diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs index d8ffdcc661..a2290e42e6 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs @@ -149,7 +149,7 @@ namespace Microsoft.AspNet.Razor.Parser var current = CurrentSymbol; if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter) { - Tuple split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition); + var split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition); current = split.Item1; Context.Source.Position = split.Item2.Start.AbsoluteIndex; NextToken(); diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs index 6df8333bf7..351db46641 100644 --- a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs +++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs @@ -316,7 +316,7 @@ namespace Microsoft.AspNet.Razor.Parser var matched = RemoveTag(tags, tagName, tagStart); if (tags.Count == 0 && - // Note tagName may contain a '!' escape character. This ensures doesn't match here. + // 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) @@ -491,7 +491,7 @@ namespace Microsoft.AspNet.Razor.Parser private void AttributePrefix(IEnumerable whitespace, IEnumerable nameSymbols) { // First, determine if this is a 'data-' attribute (since those can't use conditional attributes) - LocationTagged name = nameSymbols.GetContent(Span.Start); + var name = nameSymbols.GetContent(Span.Start); var attributeCanBeConditional = !name.Value.StartsWith("data-", StringComparison.OrdinalIgnoreCase); // Accept the whitespace and name @@ -507,7 +507,7 @@ namespace Microsoft.AspNet.Razor.Parser } // We now have the prefix: (i.e. ' foo="') - LocationTagged prefix = Span.GetContent(); + var prefix = Span.GetContent(); if (attributeCanBeConditional) { @@ -521,7 +521,7 @@ namespace Microsoft.AspNet.Razor.Parser } // Capture the suffix - LocationTagged suffix = new LocationTagged(string.Empty, CurrentLocation); + var suffix = new LocationTagged(string.Empty, CurrentLocation); if (quote != HtmlSymbolType.Unknown && At(quote)) { suffix = CurrentSymbol.GetContent(); @@ -554,23 +554,45 @@ namespace Microsoft.AspNet.Razor.Parser { var prefixStart = CurrentLocation; var prefix = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine); - Accept(prefix); if (At(HtmlSymbolType.Transition)) { - var valueStart = CurrentLocation; - PutCurrentBack(); - - // Output the prefix but as a null-span. DynamicAttributeBlockCodeGenerator will render it - Span.CodeGenerator = SpanCodeGenerator.Null; - - // Dynamic value, start a new block and set the code generator - using (Context.StartBlock(BlockType.Markup)) + if (NextIs(HtmlSymbolType.Transition)) { - Context.CurrentBlock.CodeGenerator = - new DynamicAttributeBlockCodeGenerator(prefix.GetContent(prefixStart), valueStart); + // Wrapping this in a block so that the ConditionalAttributeCollapser doesn't rewrite it. + using (Context.StartBlock(BlockType.Markup)) + { + Accept(prefix); - OtherParserBlock(); + // Render a single "@" in place of "@@". + Span.CodeGenerator = new LiteralAttributeCodeGenerator( + prefix.GetContent(prefixStart), + new LocationTagged(CurrentSymbol.GetContent(), CurrentLocation)); + AcceptAndMoveNext(); + Output(SpanKind.Markup, AcceptedCharacters.None); + + Span.CodeGenerator = SpanCodeGenerator.Null; + AcceptAndMoveNext(); + Output(SpanKind.Markup, AcceptedCharacters.None); + } + } + else + { + Accept(prefix); + var valueStart = CurrentLocation; + PutCurrentBack(); + + // Output the prefix but as a null-span. DynamicAttributeBlockCodeGenerator will render it + Span.CodeGenerator = SpanCodeGenerator.Null; + + // Dynamic value, start a new block and set the code generator + using (Context.StartBlock(BlockType.Markup)) + { + Context.CurrentBlock.CodeGenerator = + new DynamicAttributeBlockCodeGenerator(prefix.GetContent(prefixStart), valueStart); + + OtherParserBlock(); + } } } else if (At(HtmlSymbolType.Text) && @@ -578,6 +600,8 @@ namespace Microsoft.AspNet.Razor.Parser CurrentSymbol.Content[0] == '~' && NextIs(HtmlSymbolType.ForwardSlash)) { + Accept(prefix); + // Virtual Path value var valueStart = CurrentLocation; VirtualPath(); @@ -587,6 +611,8 @@ namespace Microsoft.AspNet.Razor.Parser } else { + Accept(prefix); + // Literal value // 'quote' should be "Unknown" if not quoted and symbols coming from the tokenizer should never have "Unknown" type. var value = ReadWhile(sym => @@ -719,7 +745,7 @@ namespace Microsoft.AspNet.Razor.Parser Tuple tag = Tuple.Create(tagName, _lastTagStart); if (tags.Count == 0 && - // Note tagName may contain a '!' escape character. This ensures doesn't match here. + // Note tagName may contain a '!' escape character. This ensures doesn't match here. // tags are treated like any other escaped HTML start tag. string.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase)) { diff --git a/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs b/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs index 8bf3b498d4..439d8643d2 100644 --- a/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs +++ b/test/Microsoft.AspNet.Razor.Test/Framework/ParserTestBase.cs @@ -277,15 +277,14 @@ namespace Microsoft.AspNet.Razor.Test.Framework // Evaluate the result var collector = new ErrorCollector(); - // Link all the nodes - expectedRoot.LinkNodes(); - if (expectedRoot == null) { Assert.Null(actualRoot); } else { + // Link all the nodes + expectedRoot.LinkNodes(); Assert.NotNull(actualRoot); EvaluateSyntaxTreeNode(collector, actualRoot, expectedRoot); if (collector.Success) @@ -412,7 +411,7 @@ namespace Microsoft.AspNet.Razor.Test.Framework actual.TagName, actual.SelfClosing); } - + var expectedAttributes = expected.Attributes.GetEnumerator(); var actualAttributes = actual.Attributes.GetEnumerator(); diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpBlockTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpBlockTest.cs index 49bf645ef7..054b1a9aca 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpBlockTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpBlockTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Test.Framework; using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; using Microsoft.AspNet.Razor.Tokenizer.Symbols; using Xunit; @@ -704,8 +705,8 @@ catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code); Factory.Markup("(" href=\"", 183 + Environment.NewLine.Length * 5, 5, 30), + "href", + new LocationTagged(" href=\"", 183 + Environment.NewLine.Length * 5, 5, 30), new LocationTagged("\"", 246 + Environment.NewLine.Length * 5, 5, 93)), Factory.Markup(" href=\"").With(SpanCodeGenerator.Null), new MarkupBlock( @@ -736,6 +737,322 @@ catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code); Factory.Code(" }").AsStatement().Accepts(AcceptedCharacters.None))); } + public static TheoryData BlockWithEscapedTransitionData + { + get + { + var factory = CreateDefaultSpanFactory(); + var datetimeBlock = new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)); + + return new TheoryData + { + { + // Double transition in attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 14, 0, 14)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition at the end of attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 17, 0, 17)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc", 12, 0, 12))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged("@", 15, 0, 15))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition at the beginning attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 17, 0, 17)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("def").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 14, 0, 14), new LocationTagged("def", 14, 0, 14))), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition in between attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 22, 0, 22)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc", 12, 0, 12))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 15, 0, 15), new LocationTagged("@", 16, 0, 16))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(" def").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 18, 0, 18), new LocationTagged("def", 19, 0, 19))), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition with expression block + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 14, 0, 14), 14, 0, 14), + factory.EmptyHtml().With(SpanCodeGenerator.Null), + datetimeBlock), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 28, 0, 28)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + datetimeBlock), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 25, 0, 25), new LocationTagged("@", 26, 0, 26))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + datetimeBlock), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 25, 0, 25), new LocationTagged("@", 25, 0, 25))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 33, 0, 33)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None), + factory.Code("2+3").AsExpression(), + factory.MetaCode(")").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 18, 0, 18), new LocationTagged("@", 18, 0, 18))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 20, 0, 20), 20, 0, 20), + factory.EmptyHtml().With(SpanCodeGenerator.Null), + datetimeBlock), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 20, 0, 20)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 14, 0, 14), 14, 0, 14), + factory.EmptyHtml().With(SpanCodeGenerator.Null), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None), + factory.Code("2+3").AsExpression(), + factory.MetaCode(")").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None))), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition with email in attribute value + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 26, 0, 26)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc@def.com").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc@def.com", 12, 0, 12))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 23, 0, 23), new LocationTagged("@", 24, 0, 24))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + "{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 6, 0, 6), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("abc", 12, 0, 12))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 15, 0, 15), new LocationTagged("@", 15, 0, 15))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("def.com").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 17, 0, 17), new LocationTagged("def.com", 17, 0, 17))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 24, 0, 24), new LocationTagged("@", 25, 0, 25))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + { + // Double transition in complex regex in attribute value + @"{}", + CreateStatementBlock( + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo=\"", 6, 0, 6), new LocationTagged("\"", 112, 0, 112)), + factory.Markup(" foo=\"").With(SpanCodeGenerator.Null), + factory.Markup(@"/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged(@"/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+", 12, 0, 12))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 44, 0, 44), new LocationTagged("@", 44, 0, 44))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(@"[a-z0-9]([a-z0-9-]*[a-z0-9])?\.([a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 46, 0, 46), new LocationTagged(@"[a-z0-9]([a-z0-9-]*[a-z0-9])?\.([a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i", 46, 0, 46))), + factory.Markup("\"").With(SpanCodeGenerator.Null)), + factory.Markup(" />").Accepts(AcceptedCharacters.None)))) + }, + }; + } + } + + [Theory] + [MemberData(nameof(BlockWithEscapedTransitionData))] + public void ParseBlock_WithDoubleTransition_DoesNotThrow(string input, Block expected) + { + // Act & Assert + ParseBlockTest(input, expected); + } + + [Fact] + public void ParseBlock_WithDoubleTransition_EndOfFile_Throws() + { + // Arrange + var expected = new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" foo='", 6, 0, 6), new LocationTagged(string.Empty, 14, 0, 14)), + Factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + Factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), new LocationTagged("@", 12, 0, 12))).Accepts(AcceptedCharacters.None), + Factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)))), + Factory.EmptyHtml())); + var expectedErrors = new RazorError[] + { + new RazorError(@"End of file or an unexpected character was reached before the ""span"" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing (""
"") or have matching end tags (""

Hello

""). If you intended to display a ""<"" character, use the ""<"" HTML entity.", new SourceLocation(1, 0, 1)), + new RazorError(@"The code block is missing a closing ""}"" character. Make sure you have a matching ""}"" character for all the ""{"" characters within this block, and that none of the ""}"" characters are being interpreted as markup.", new SourceLocation(0, 0, 0)), + }; + + // Act & Assert + ParseBlockTest("{("'", 15, 0, 15)), + Factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 12, 0, 12), 12, 0, 12), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace))), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(" ", 13, 0, 13), 13, 0, 13), + Factory.Markup(" ").With(SpanCodeGenerator.Null), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace))), + Factory.Markup("'").With(SpanCodeGenerator.Null)), + Factory.Markup(" />").Accepts(AcceptedCharacters.None))), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)); + var expectedErrors = new RazorError[] + { + new RazorError(@"A space or line break was encountered after the ""@"" character. Only valid identifiers, keywords, comments, ""("" and ""{"" are valid at the start of a code block and they must occur immediately following ""@"" with no space in between.", new SourceLocation(13, 0, 13)), + new RazorError(@"""' />}"" is not valid at the start of a code block. Only identifiers, keywords, comments, ""("" and ""{"" are valid.", new SourceLocation(15, 0, 15)), + }; + + // Act & Assert + ParseBlockTest("{}", expected, expectedErrors); + } + private void RunRazorCommentBetweenClausesTest(string preComment, string postComment, AcceptedCharacters acceptedCharacters = AcceptedCharacters.Any) { ParseBlockTest(preComment + "@* Foo *@ @* Bar *@" + postComment, @@ -786,5 +1103,24 @@ catch(bar) { baz(); }", BlockType.Statement, SpanKind.Code); .Accepts(acceptedCharacters)), errors); } + + private static SpanFactory CreateDefaultSpanFactory() + { + return new SpanFactory + { + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), + CodeTokenizerFactory = doc => new CSharpTokenizer(doc) + }; + } + + private static StatementBlock CreateStatementBlock(MarkupBlock block) + { + var factory = CreateDefaultSpanFactory(); + return new StatementBlock( + factory.MetaCode("{").Accepts(AcceptedCharacters.None), + block, + factory.EmptyCSharp().AsStatement(), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)); + } } } diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpSectionTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpSectionTest.cs index 2f98aaf722..3adb784f5f 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpSectionTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpSectionTest.cs @@ -6,6 +6,8 @@ using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Test.Framework; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; using Xunit; namespace Microsoft.AspNet.Razor.Test.Parser.CSharp @@ -443,5 +445,84 @@ namespace Microsoft.AspNet.Razor.Test.Parser.CSharp Factory.MetaCode("}").Accepts(AcceptedCharacters.None)), Factory.EmptyHtml())); } + + public static TheoryData SectionWithEscapedTransitionData + { + get + { + var factory = CreateDefaultSpanFactory(); + + return new TheoryData + { + { + "@section s {}", + new MarkupBlock( + factory.EmptyHtml(), + new SectionBlock(new SectionCodeGenerator("s"), + factory.CodeTransition(), + factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 17, 0, 17), new LocationTagged("'", 25, 0, 25)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 23, 0, 23), new LocationTagged("@", 23, 0, 23))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }, + { + "@section s {}", + new MarkupBlock( + factory.EmptyHtml(), + new SectionBlock(new SectionCodeGenerator("s"), + factory.CodeTransition(), + factory.MetaCode("section s {") + .AutoCompleteWith(null, atEndOfSpan: true), + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 17, 0, 17), new LocationTagged("'", 39, 0, 39)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 23, 0, 23), 23, 0, 23), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 36, 0, 36), new LocationTagged("@", 37, 0, 37))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))), + factory.MetaCode("}").Accepts(AcceptedCharacters.None)), + factory.EmptyHtml()) + }, + }; + } + } + + [Theory] + [MemberData(nameof(SectionWithEscapedTransitionData))] + public void ParseSectionBlock_WithDoubleTransition_DoesNotThrow(string input, Block expected) + { + ParseDocumentTest(input, expected); + } + + private static SpanFactory CreateDefaultSpanFactory() + { + return new SpanFactory + { + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), + CodeTokenizerFactory = doc => new CSharpTokenizer(doc) + }; + } } } diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs index 3b8839ab6f..95d2ed8d98 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/CSharp/CSharpTemplateTest.cs @@ -3,9 +3,11 @@ using System; using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Test.Framework; +using Microsoft.AspNet.Razor.Text; using Microsoft.AspNet.Razor.Tokenizer.Symbols; using Xunit; @@ -272,6 +274,49 @@ namespace Microsoft.AspNet.Razor.Test.Parser.CSharp GetNestedTemplateError(69)); } + [Fact] + public void ParseBlock_WithDoubleTransition_DoesNotThrow() + { + // Arrange + var testTemplateWithDoubleTransitionCode = " @

Foo #@item

"; + var testTemplateWithDoubleTransition = new TemplateBlock( + new MarkupBlock( + Factory.MarkupTransition(), + new MarkupTagBlock( + Factory.Markup("(" foo='", 46, 0, 46), new LocationTagged("'", 54, 0, 54)), + Factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + Factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 52, 0, 52), new LocationTagged("@", 52, 0, 52))).Accepts(AcceptedCharacters.None), + Factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + Factory.Markup("'").With(SpanCodeGenerator.Null)), + Factory.Markup(">").Accepts(AcceptedCharacters.None)), + Factory.Markup("Foo #"), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.Code("item") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace) + ), + new MarkupTagBlock( + Factory.Markup("

").Accepts(AcceptedCharacters.None)) + ) + ); + + var expected = new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharacters.None), + Factory.Code(" var foo = bar; Html.ExecuteTemplate(foo, ") + .AsStatement() + .AutoCompleteWith(autoCompleteString: null), + testTemplateWithDoubleTransition, + Factory.Code("); ").AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharacters.None)); + + // Act & Assert + ParseBlockTest("{ var foo = bar; Html.ExecuteTemplate(foo," + testTemplateWithDoubleTransitionCode + "); }", expected); + } + private static RazorError GetNestedTemplateError(int characterIndex) { return new RazorError(RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested, new SourceLocation(characterIndex, 0, characterIndex)); diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs index 9cc349ae82..5f139cc6ed 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlAttributeTest.cs @@ -206,6 +206,31 @@ namespace Microsoft.AspNet.Razor.Test.Parser.Html Factory.Markup(" />")))); } + [Fact] + public void ConditionalAttributeCollapserDoesNotRewriteEscapedTransitions() + { + // Act + var results = ParseDocument(""); + var rewritingContext = new RewritingContext(results.Document, new ErrorSink()); + new ConditionalAttributeCollapser(new HtmlMarkupParser().BuildSpan).Rewrite(rewritingContext); + var rewritten = rewritingContext.SyntaxTree; + + // Assert + Assert.Equal(0, results.ParserErrors.Count()); + EvaluateParseTree(rewritten, + new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 13, 0, 13)), + Factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + Factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("@", 11, 0, 11))).Accepts(AcceptedCharacters.None), + Factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + Factory.Markup("'").With(SpanCodeGenerator.Null)), + Factory.Markup(" />")))); + } + [Fact] public void ConditionalAttributesDoNotCreateExtraDataForEntirelyLiteralAttribute() { diff --git a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs index 1902dfc5c7..ed797f121b 100644 --- a/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/Parser/Html/HtmlDocumentTest.cs @@ -2,11 +2,13 @@ // 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 Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Test.Framework; using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; using Xunit; namespace Microsoft.AspNet.Razor.Test.Parser.Html @@ -281,5 +283,302 @@ namespace Microsoft.AspNet.Razor.Test.Parser.Html var content = Nested1000.ReadAllText(); ParseDocument(content); } + + public static TheoryData BlockWithEscapedTransitionData + { + get + { + var factory = CreateDefaultSpanFactory(); + var datetimeBlock = new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace)); + + return new TheoryData + { + { + // Double transition in attribute value + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 13, 0, 13)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("@", 11, 0, 11))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + // Double transition at the end of attribute value + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 16, 0, 16)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("abc", 11, 0, 11))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 14, 0, 14), new LocationTagged("@", 14, 0, 14))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + // Double transition at the beginning of attribute value + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 16, 0, 16)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("@", 11, 0, 11))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("def").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 13, 0, 13), new LocationTagged("def", 13, 0, 13))), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + // Double transition in between attribute value + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 21, 0, 21)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("abc", 11, 0, 11))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 14, 0, 14), new LocationTagged("@", 15, 0, 15))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(" def").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 17, 0, 17), new LocationTagged("def", 18, 0, 18))), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + // Double transition with expression block + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 26, 0, 26)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("@", 11, 0, 11))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 13, 0, 13), 13, 0, 13), + factory.EmptyHtml().With(SpanCodeGenerator.Null), + datetimeBlock), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 27, 0, 27)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), 11, 0, 11), + datetimeBlock), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 24, 0, 24), new LocationTagged("@", 25, 0, 25))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 32, 0, 32)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), 11, 0, 11), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None), + factory.Code("2+3").AsExpression(), + factory.MetaCode(")").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 17, 0, 17), new LocationTagged("@", 17, 0, 17))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 19, 0, 19), 19, 0, 19), + factory.EmptyHtml().With(SpanCodeGenerator.Null), + datetimeBlock), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 19, 0, 19)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("@", 11, 0, 11))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 13, 0, 13), 13, 0, 13), + factory.EmptyHtml().With(SpanCodeGenerator.Null), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None), + factory.Code("2+3").AsExpression(), + factory.MetaCode(")").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None))), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 26, 0, 26)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), 11, 0, 11), + datetimeBlock), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 24, 0, 24), new LocationTagged("@", 24, 0, 24))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + // Double transition with email in attribute value + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 25, 0, 25)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc@def.com").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("abc@def.com", 11, 0, 11))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 22, 0, 22), new LocationTagged("@", 23, 0, 23))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + "", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 26, 0, 26)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + factory.Markup("abc").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("abc", 11, 0, 11))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 14, 0, 14), new LocationTagged("@", 14, 0, 14))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("def.com").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 16, 0, 16), new LocationTagged("def.com", 16, 0, 16))), + new MarkupBlock( + factory.Markup(" @").With(new LiteralAttributeCodeGenerator(new LocationTagged(" ", 23, 0, 23), new LocationTagged("@", 24, 0, 24))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("'").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + { + // Double transition before end of file + "(string.Empty, 13, 0, 13)), + factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged("@", 11, 0, 11))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)))), + factory.EmptyHtml()) + }, + { + // Double transition in complex regex in attribute value + @"", + new MarkupBlock( + new MarkupTagBlock( + factory.Markup("(" foo=\"", 5, 0, 5), new LocationTagged("\"", 111, 0, 111)), + factory.Markup(" foo=\"").With(SpanCodeGenerator.Null), + factory.Markup(@"/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), new LocationTagged(@"/^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+", 11, 0, 11))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 43, 0, 43), new LocationTagged("@", 43, 0, 43))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(@"[a-z0-9]([a-z0-9-]*[a-z0-9])?\.([a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 45, 0, 45), new LocationTagged(@"[a-z0-9]([a-z0-9-]*[a-z0-9])?\.([a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i", 45, 0, 45))), + factory.Markup("\"").With(SpanCodeGenerator.Null)), + factory.Markup(" />"))) + }, + }; + } + } + + [Theory] + [MemberData(nameof(BlockWithEscapedTransitionData))] + public void ParseBlock_WithDoubleTransition_DoesNotThrow(string input, Block expected) + { + // Act & Assert + ParseDocumentTest(input, expected); + } + + [Fact] + public void ParseDocument_WithUnexpectedTransitionsInAttributeValue_Throws() + { + // Arrange + var expected = new MarkupBlock( + new MarkupTagBlock( + Factory.Markup("(" foo='", 5, 0, 5), new LocationTagged("'", 14, 0, 14)), + Factory.Markup(" foo='").With(SpanCodeGenerator.Null), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(string.Empty, 11, 0, 11), 11, 0, 11), + new ExpressionBlock( + Factory.CodeTransition(), + Factory.EmptyCSharp().AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace))), + new MarkupBlock( + new DynamicAttributeBlockCodeGenerator(new LocationTagged(" ", 12, 0, 12), 12, 0, 12), + Factory.Markup(" ").With(SpanCodeGenerator.Null), + new ExpressionBlock( + Factory.CodeTransition().Accepts(AcceptedCharacters.None).With(SpanCodeGenerator.Null), + Factory.EmptyCSharp().AsImplicitExpression(CSharpCodeParser.DefaultKeywords).Accepts(AcceptedCharacters.NonWhiteSpace))), + Factory.Markup("'").With(SpanCodeGenerator.Null)), + Factory.Markup(" />"))); + var expectedErrors = new RazorError[] + { + new RazorError(@"A space or line break was encountered after the ""@"" character. Only valid identifiers, keywords, comments, ""("" and ""{"" are valid at the start of a code block and they must occur immediately following ""@"" with no space in between.", new SourceLocation(12, 0, 12)), + new RazorError(@"""' />"" is not valid at the start of a code block. Only identifiers, keywords, comments, ""("" and ""{"" are valid.", new SourceLocation(14, 0, 14)), + }; + + // Act & Assert + ParseDocumentTest("", expected, expectedErrors); + } + + private static SpanFactory CreateDefaultSpanFactory() + { + return new SpanFactory + { + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), + CodeTokenizerFactory = doc => new CSharpTokenizer(doc) + }; + } } } diff --git a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs index f7eb817209..de2254478f 100644 --- a/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNet.Razor.Test/TagHelpers/TagHelperParseTreeRewriterTest.cs @@ -352,6 +352,24 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers }, children: factory.Markup("words and spaces"))) }, + { + "
words and spaces
", + new MarkupBlock( + new MarkupTagHelperBlock( + "div", + attributes: new List> + { + new KeyValuePair("style", new MarkupBlock()), + new KeyValuePair("class", factory.Markup("btn")), + new KeyValuePair("catchAll", + new MarkupBlock( + new MarkupBlock( + factory.Markup("@").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("hi"))), + }, + children: factory.Markup("words and spaces"))) + }, { "
words and " + "spaces
", @@ -1143,6 +1161,32 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers })), availableDescriptorsText }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock( + "PREFIXmyth2", + selfClosing: true, + attributes: new List> + { + { + new KeyValuePair( + "bound", + new MarkupBlock( + new MarkupBlock( + factory.Markup("@").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + factory.EmptyHtml(), + new ExpressionBlock( + factory.CodeTransition(), + factory.Code("DateTime.Now") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharacters.NonWhiteSpace))))) + } + })), + availableDescriptorsText + }, }; } } @@ -2175,6 +2219,41 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers absoluteIndex: 2, lineIndex: 0, columnIndex: 2) } }, + { + "@{ new MarkupBlock( + new MarkupTagBlock( + factory.Markup("<"), + factory.BangEscape(), + factory.Markup("p"), + new MarkupBlock( + new AttributeBlockCodeGenerator( + name: "class", + prefix: new LocationTagged(" class=\"", 5, 0, 5), + suffix: new LocationTagged(string.Empty, 19, 0, 19)), + factory.Markup(" class=\"").With(SpanCodeGenerator.Null), + factory.Markup("btn").With( + new LiteralAttributeCodeGenerator( + prefix: new LocationTagged(string.Empty, 13, 0, 13), + value: new LocationTagged("btn", 13, 0, 13))), + new MarkupBlock( + factory.Markup("@").With(new LiteralAttributeCodeGenerator(new LocationTagged(string.Empty, 16, 0, 16), new LocationTagged("@", 16, 0, 16))).Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("}").With( + new LiteralAttributeCodeGenerator( + prefix: new LocationTagged(string.Empty, 18, 0, 18), + value: new LocationTagged("}", 18, 0, 18))))))), + new [] + { + new RazorError( + errorMatchingBrace, + absoluteIndex: 1, lineIndex: 0, columnIndex: 1), + new RazorError( + string.Format(errorEOFMatchingBrace, "!p"), + absoluteIndex: 2, lineIndex: 0, columnIndex: 2) + } + }, { "@{", + new MarkupBlock( + new MarkupTagHelperBlock("person", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("age", factory.CodeMarkup("12")), + new KeyValuePair( + "birthday", + factory.CodeMarkup("DateTime.Now")), + new KeyValuePair( + "name", + new MarkupBlock( + factory.Markup("Time:"), + new MarkupBlock( + factory.Markup(" @").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + dateTimeNow)) + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + selfClosing: true, + attributes: new List> + { + new KeyValuePair("age", factory.CodeMarkup("12")), + new KeyValuePair( + "birthday", + factory.CodeMarkup("DateTime.Now")), + new KeyValuePair( + "name", + new MarkupBlock( + new MarkupBlock( + factory.Markup("@").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("BoundStringAttribute"))) + })) + }, + { + "", + new MarkupBlock( + new MarkupTagHelperBlock("person", + selfClosing: true, + attributes: new List> + { + new KeyValuePair( + "age", + new MarkupBlock( + new MarkupBlock( + factory.Markup("@").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + new MarkupBlock( + factory.EmptyHtml(), + new ExpressionBlock( + factory.CodeTransition(), + factory.MetaCode("(").Accepts(AcceptedCharacters.None), + factory.Code("11+1").AsExpression(), + factory.MetaCode(")").Accepts(AcceptedCharacters.None))))), + new KeyValuePair( + "birthday", + factory.CodeMarkup("DateTime.Now")), + new KeyValuePair( + "name", + new MarkupBlock(factory.Markup("Time:"), dateTimeNow)) + })) + }, }; } } @@ -4535,6 +4683,25 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers }), factory.Markup(" World"))) }; + yield return new object[] { + "

Hello World

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + factory.Markup("Hello "), + new MarkupTagHelperBlock("script", + new List> + { + new KeyValuePair( + "class", + new MarkupBlock( + new MarkupBlock( + factory.Markup("@").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup("foo@bar.com"))), + new KeyValuePair("style", factory.Markup("color:red;")) + }), + factory.Markup(" World"))) + }; } } @@ -4651,6 +4818,25 @@ namespace Microsoft.AspNet.Razor.Test.TagHelpers }, factory.Markup("Hello World"))) }; + yield return new object[] { + "

Hello World

", + new MarkupBlock( + new MarkupTagHelperBlock("p", + new List> + { + new KeyValuePair("class", factory.Markup("foo")), + new KeyValuePair("dynamic", new MarkupBlock(dateTimeNow)), + new KeyValuePair( + "style", + new MarkupBlock( + factory.Markup("color"), + new MarkupBlock( + factory.Markup("@").Accepts(AcceptedCharacters.None), + factory.Markup("@").With(SpanCodeGenerator.Null).Accepts(AcceptedCharacters.None)), + factory.Markup(":red;"))) + }, + factory.Markup("Hello World"))) + }; yield return new object[] { "

Hello

World

", new MarkupBlock(