diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs index c515770ca7..6c37427ca7 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy // Special Comment = 8, - Tag = 9 + Tag = 9, + HtmlComment = 10 } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs index 4fdee1322f..a23adbb28f 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/HtmlMarkupParser.cs @@ -12,6 +12,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { private const string ScriptTagName = "script"; + private static readonly HtmlSymbol[] nonAllowedHtmlCommentEnding = new[] { HtmlSymbol.Hyphen, new HtmlSymbol("!", HtmlSymbolType.Bang), new HtmlSymbol("<", HtmlSymbolType.OpenAngle) }; + private static readonly HtmlSymbol[] singleHyphenArray = new[] { HtmlSymbol.Hyphen }; + private static readonly char[] ValidAfterTypeAttributeNameCharacters = { ' ', '\t', '\r', '\n', '\f', '=' }; private SourceLocation _lastTagStart = SourceLocation.Zero; private HtmlSymbol _bufferedOpenAngle; @@ -492,33 +495,37 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy if (AcceptAndMoveNext()) { - if (CurrentSymbol.Type == HtmlSymbolType.DoubleHyphen) + if (IsHtmlCommentAhead()) { - AcceptAndMoveNext(); - - Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.Any; - while (!EndOfFile) + using (Context.Builder.StartBlock(BlockKindInternal.HtmlComment)) { - SkipToAndParseCode(HtmlSymbolType.DoubleHyphen); - if (At(HtmlSymbolType.DoubleHyphen)) - { - AcceptWhile(HtmlSymbolType.DoubleHyphen); + // Accept the double-hyphen symbol at the beginning of the comment block. + AcceptAndMoveNext(); + Output(SpanKindInternal.Markup, AcceptedCharactersInternal.None); - if (At(HtmlSymbolType.Text) && - string.Equals(CurrentSymbol.Content, "-", StringComparison.Ordinal)) - { - AcceptAndMoveNext(); - } + Span.EditHandler.AcceptedCharacters = AcceptedCharactersInternal.WhiteSpace; + while (!EndOfFile) + { + SkipToAndParseCode(HtmlSymbolType.DoubleHyphen); + var lastDoubleHyphen = AcceptAllButLastDoubleHyphens(); if (At(HtmlSymbolType.CloseAngle)) { + // Output the content in the comment block as a separate markup + Output(SpanKindInternal.Markup, AcceptedCharactersInternal.WhiteSpace); + + // This is the end of a comment block + Accept(lastDoubleHyphen); AcceptAndMoveNext(); + Output(SpanKindInternal.Markup, AcceptedCharactersInternal.None); return true; } + else if (lastDoubleHyphen != null) + { + Accept(lastDoubleHyphen); + } } } - - return false; } else if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) { @@ -537,6 +544,138 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return false; } + protected HtmlSymbol AcceptAllButLastDoubleHyphens() + { + var lastDoubleHyphen = CurrentSymbol; + AcceptWhile(s => + { + if (NextIs(HtmlSymbolType.DoubleHyphen)) + { + lastDoubleHyphen = s; + return true; + } + + return false; + }); + + NextToken(); + + if (At(HtmlSymbolType.Text) && IsHyphen(CurrentSymbol)) + { + // Doing this here to maintain the order of symbols + if (!NextIs(HtmlSymbolType.CloseAngle)) + { + Accept(lastDoubleHyphen); + lastDoubleHyphen = null; + } + + AcceptAndMoveNext(); + } + + return lastDoubleHyphen; + } + + internal static bool IsHyphen(HtmlSymbol symbol) + { + return symbol.Equals(HtmlSymbol.Hyphen); + } + + protected bool IsHtmlCommentAhead() + { + /* + * From HTML5 Specification, available at http://www.w3.org/TR/html52/syntax.html#comments + * + * Comments must have the following format: + * 1. The string "" // As we will be treating this as a comment ending, there is no need to handle this case at all. + * 2.2.3 "--!>" + * 2.3 nor end with the string "" + * + * */ + + if (CurrentSymbol.Type != HtmlSymbolType.DoubleHyphen) + { + return false; + } + + // Check condition 2.1 + if (NextIs(HtmlSymbolType.CloseAngle) || NextIs(next => IsHyphen(next) && NextIs(HtmlSymbolType.CloseAngle))) + { + return false; + } + + // Check condition 2.2 + var isValidComment = false; + LookaheadUntil((symbol, prevSymbols) => + { + if (symbol.Type == HtmlSymbolType.DoubleHyphen) + { + if (NextIs(HtmlSymbolType.CloseAngle)) + { + // Check condition 2.3: We're at the end of a comment. Check to make sure the text ending is allowed. + isValidComment = !IsCommentContentEndingInvalid(prevSymbols); + return true; + } + else if (NextIs(ns => IsHyphen(ns) && NextIs(HtmlSymbolType.CloseAngle))) + { + // Check condition 2.3: we're at the end of a comment, which has an extra dash. + // Need to treat the dash as part of the content and check the ending. + // However, that case would have already been checked as part of check from 2.2.1 which + // would already fail this iteration and we wouldn't get here + isValidComment = true; + return true; + } + else if (NextIs(ns => ns.Type == HtmlSymbolType.Bang && NextIs(HtmlSymbolType.CloseAngle))) + { + // This is condition 2.2.3 + isValidComment = false; + return true; + } + } + else if (symbol.Type == HtmlSymbolType.OpenAngle) + { + // Checking condition 2.2.1 + if (NextIs(ns => ns.Type == HtmlSymbolType.Bang && NextIs(HtmlSymbolType.DoubleHyphen))) + { + isValidComment = false; + return true; + } + } + + return false; + }); + + return isValidComment; + } + + /// + /// Verifies, that the sequence doesn't end with the "<!-" HtmlSymbols. Note, the first symbol is an opening bracket symbol + /// + internal static bool IsCommentContentEndingInvalid(IEnumerable sequence) + { + var reversedSequence = sequence.Reverse(); + var index = 0; + foreach (var item in reversedSequence) + { + if (!item.Equals(nonAllowedHtmlCommentEnding[index++])) + { + return false; + } + + if (index == nonAllowedHtmlCommentEnding.Length) + { + return true; + } + } + + return false; + } + private bool CData() { if (CurrentSymbol.Type == HtmlSymbolType.Text && string.Equals(CurrentSymbol.Content, "cdata", StringComparison.OrdinalIgnoreCase)) @@ -1474,8 +1613,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy // Checking to see if we meet the conditions of a special '!' tag: { + internal static readonly HtmlSymbol Hyphen = new HtmlSymbol("-", HtmlSymbolType.Text); + public HtmlSymbol(string content, HtmlSymbolType type) : base(content, type, RazorDiagnostic.EmptyArray) { diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs index 2059522263..d5d65c2985 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TagHelperParseTreeRewriter.cs @@ -490,20 +490,29 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { if (HasAllowedChildren()) { - var content = child.Content; - if (!string.IsNullOrWhiteSpace(content)) + var isDisallowedContent = true; + if (_featureFlags.AllowHtmlCommentsInTagHelpers) { - var trimmedStart = content.TrimStart(); - var whitespace = content.Substring(0, content.Length - trimmedStart.Length); - var errorStart = SourceLocationTracker.Advance(child.Start, whitespace); - var length = trimmedStart.TrimEnd().Length; - var allowedChildren = _currentTagHelperTracker.AllowedChildren; - var allowedChildrenString = string.Join(", ", allowedChildren); - errorSink.OnError( - RazorDiagnosticFactory.CreateTagHelper_CannotHaveNonTagContent( - new SourceSpan(errorStart, length), - _currentTagHelperTracker.TagName, - allowedChildrenString)); + isDisallowedContent = !IsComment(child) && child.Kind != SpanKindInternal.Transition && child.Kind != SpanKindInternal.Code; + } + + if (isDisallowedContent) + { + var content = child.Content; + if (!string.IsNullOrWhiteSpace(content)) + { + var trimmedStart = content.TrimStart(); + var whitespace = content.Substring(0, content.Length - trimmedStart.Length); + var errorStart = SourceLocationTracker.Advance(child.Start, whitespace); + var length = trimmedStart.TrimEnd().Length; + var allowedChildren = _currentTagHelperTracker.AllowedChildren; + var allowedChildrenString = string.Join(", ", allowedChildren); + errorSink.OnError( + RazorDiagnosticFactory.CreateTagHelper_CannotHaveNonTagContent( + new SourceSpan(errorStart, length), + _currentTagHelperTracker.TagName, + allowedChildrenString)); + } } } } @@ -817,6 +826,18 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return relevantSymbol.Type == HtmlSymbolType.ForwardSlash; } + internal static bool IsComment(Span span) + { + Block currentBlock = span.Parent; + while (currentBlock != null && currentBlock.Type != BlockKindInternal.Comment && currentBlock.Type != BlockKindInternal.HtmlComment) + { + currentBlock = currentBlock.Parent; + } + + return currentBlock != null; + } + + private static void EnsureTagBlock(Block tagBlock) { Debug.Assert(tagBlock.Type == BlockKindInternal.Tag); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TokenizerBackedParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TokenizerBackedParser.cs index 6f564eb442..01fa0b11e1 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/TokenizerBackedParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/TokenizerBackedParser.cs @@ -109,6 +109,52 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy return symbols[count]; } + /// + /// Looks forward until the specified condition is met. + /// + /// A predicate accepting the symbol being evaluated and the list of symbols which have been looped through. + /// true, if the condition was met. false - if the condition wasn't met and the last symbol has already been processed. + /// The list of previous symbols is passed in the reverse order. So the last processed element will be the first one in the list. + protected bool LookaheadUntil(Func, bool> condition) + { + if (condition == null) + { + throw new ArgumentNullException(nameof(condition)); + } + + var matchFound = false; + + var symbols = new List(); + symbols.Add(CurrentSymbol); + + while (true) + { + if (!NextToken()) + { + break; + } + + symbols.Add(CurrentSymbol); + if (condition(CurrentSymbol, symbols)) + { + matchFound = true; + break; + } + } + + // Restore Tokenizer's location to where it was pointing before the look-ahead. + for (var i = symbols.Count - 1; i >= 0; i--) + { + PutBack(symbols[i]); + } + + // The PutBacks above will set CurrentSymbol to null. EnsureCurrent will set our CurrentSymbol to the + // next symbol. + EnsureCurrent(); + + return matchFound; + } + protected internal bool NextToken() { PreviousSymbol = CurrentSymbol; @@ -254,12 +300,21 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy protected internal bool NextIs(Func condition) { var cur = CurrentSymbol; - NextToken(); - var result = condition(CurrentSymbol); - PutCurrentBack(); - PutBack(cur); - EnsureCurrent(); - return result; + if (NextToken()) + { + var result = condition(CurrentSymbol); + PutCurrentBack(); + PutBack(cur); + EnsureCurrent(); + return result; + } + else + { + PutBack(cur); + EnsureCurrent(); + } + + return false; } protected internal bool Was(TSymbolType type) diff --git a/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs b/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs index 0629eb9af8..409718b640 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/RazorParserFeatureFlags.cs @@ -8,26 +8,33 @@ namespace Microsoft.AspNetCore.Razor.Language public static RazorParserFeatureFlags Create(RazorLanguageVersion version) { var allowMinimizedBooleanTagHelperAttributes = false; + var allowHtmlCommentsInTagHelpers = false; if (version.CompareTo(RazorLanguageVersion.Version_2_1) >= 0) { // Added in 2.1 allowMinimizedBooleanTagHelperAttributes = true; + allowHtmlCommentsInTagHelpers = true; } - return new DefaultRazorParserFeatureFlags(allowMinimizedBooleanTagHelperAttributes); + return new DefaultRazorParserFeatureFlags(allowMinimizedBooleanTagHelperAttributes, allowHtmlCommentsInTagHelpers); } public abstract bool AllowMinimizedBooleanTagHelperAttributes { get; } + public abstract bool AllowHtmlCommentsInTagHelpers { get; } + private class DefaultRazorParserFeatureFlags : RazorParserFeatureFlags { - public DefaultRazorParserFeatureFlags(bool allowMinimizedBooleanTagHelperAttributes) + public DefaultRazorParserFeatureFlags(bool allowMinimizedBooleanTagHelperAttributes, bool allowHtmlCommentsInTagHelpers) { AllowMinimizedBooleanTagHelperAttributes = allowMinimizedBooleanTagHelperAttributes; + AllowHtmlCommentsInTagHelpers = allowHtmlCommentsInTagHelpers; } public override bool AllowMinimizedBooleanTagHelperAttributes { get; } + + public override bool AllowHtmlCommentsInTagHelpers { get; } } } } diff --git a/src/Microsoft.VisualStudio.Editor.Razor/BlockKind.cs b/src/Microsoft.VisualStudio.Editor.Razor/BlockKind.cs index 48ad2e4fb4..2559231329 100644 --- a/src/Microsoft.VisualStudio.Editor.Razor/BlockKind.cs +++ b/src/Microsoft.VisualStudio.Editor.Razor/BlockKind.cs @@ -19,6 +19,8 @@ namespace Microsoft.VisualStudio.Editor.Razor // Special Comment, - Tag + Tag, + + HtmlComment } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs index 3ae95b1994..3e2fbac20f 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpSectionTest.cs @@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { // Act & Assert ParseDocumentTest( - "@section foo " + "@section foo " + Environment.NewLine + Environment.NewLine + Environment.NewLine @@ -606,7 +606,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Factory.Span(SpanKindInternal.Markup, " ", CSharpSymbolType.WhiteSpace).Accepts(AcceptedCharactersInternal.AllWhiteSpace), Factory.MetaCode("{").AutoCompleteWith(null, atEndOfSpan: true).Accepts(AcceptedCharactersInternal.None), new MarkupBlock( - Factory.Markup("")), + BlockFactory.HtmlCommentBlock(" "), + Factory.EmptyHtml()), Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), Factory.EmptyHtml())); } @@ -630,7 +631,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Factory.Span(SpanKindInternal.Markup, " ", CSharpSymbolType.WhiteSpace).Accepts(AcceptedCharactersInternal.AllWhiteSpace), Factory.MetaCode("{").AutoCompleteWith(null, atEndOfSpan: true).Accepts(AcceptedCharactersInternal.None), new MarkupBlock( - Factory.Markup("")), + BlockFactory.HtmlCommentBlock(" > \" '"), + Factory.EmptyHtml()), Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), Factory.EmptyHtml())); } @@ -655,7 +657,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Factory.Markup(Environment.NewLine), new MarkupTagBlock( Factory.Markup(" \" '-->")), + BlockFactory.HtmlCommentBlock(" > \" '"), + Factory.EmptyHtml()), Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), Factory.EmptyHtml())); } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlBlockTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlBlockTest.cs index f54ba50d37..3abd53b105 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlBlockTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlBlockTest.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Factory.Code(Environment.NewLine).AsStatement().AutoCompleteWith(null), new MarkupBlock( Factory.Markup(" "), - Factory.Markup("").Accepts(AcceptedCharactersInternal.None), + BlockFactory.HtmlCommentBlock(" Hello, I'm a comment that shouldn't break razor -"), Factory.Markup(Environment.NewLine).Accepts(AcceptedCharactersInternal.None)), Factory.EmptyCSharp().AsStatement(), Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None)), @@ -333,7 +333,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy [Fact] public void ParseBlockSupportsCommentAsBlock() { - SingleSpanBlockTest("", BlockKindInternal.Markup, SpanKindInternal.Markup, acceptedCharacters: AcceptedCharactersInternal.None); + ParseBlockTest("", new MarkupBlock(BlockFactory.HtmlCommentBlock(" foo "))); + } + + [Fact] + public void ParseBlockSupportsCommentWithExtraDashAsBlock() + { + ParseBlockTest("", new MarkupBlock(BlockFactory.HtmlCommentBlock(" foo -"))); } [Fact] @@ -344,8 +350,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupTagBlock( Factory.Markup("").Accepts(AcceptedCharactersInternal.None)), Factory.Markup("bar"), - Factory.Markup("").Accepts(AcceptedCharactersInternal.None), - Factory.Markup("baz"), + BlockFactory.HtmlCommentBlock(" zoop "), + Factory.Markup("baz").Accepts(AcceptedCharactersInternal.None), new MarkupTagBlock( Factory.Markup("").Accepts(AcceptedCharactersInternal.None)))); } @@ -355,7 +361,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy get { var factory = new SpanFactory(); - + var blockFactory = new BlockFactory(factory); return new TheoryData { { @@ -363,7 +369,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None)), - factory.Markup("").Accepts(AcceptedCharactersInternal.None), + blockFactory.HtmlCommentBlock("- Hello World -"), new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None))) }, @@ -372,7 +378,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None)), - factory.Markup("").Accepts(AcceptedCharactersInternal.None), + blockFactory.HtmlCommentBlock("-- Hello World --"), new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None))) }, @@ -381,7 +387,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None)), - factory.Markup("").Accepts(AcceptedCharactersInternal.None), + blockFactory.HtmlCommentBlock("--- Hello World ---"), new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None))) }, @@ -390,7 +396,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( factory.Markup("
").Accepts(AcceptedCharactersInternal.None)), - factory.Markup("").Accepts(AcceptedCharactersInternal.None), + blockFactory.HtmlCommentBlock("--- Hello < --- > World
---"), new MarkupTagBlock( factory.Markup("").Accepts(AcceptedCharactersInternal.None))) }, @@ -410,19 +416,22 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy [Fact] public void ParseBlockProperlyBalancesCommentStartAndEndTags() { - SingleSpanBlockTest("", BlockKindInternal.Markup, SpanKindInternal.Markup, acceptedCharacters: AcceptedCharactersInternal.None); + ParseBlockTest("", new MarkupBlock(BlockFactory.HtmlCommentBlock(""))); } [Fact] public void ParseBlockTerminatesAtEOFWhenParsingComment() { - SingleSpanBlockTest("", BlockKindInternal.Markup, SpanKindInternal.Markup, acceptedCharacters: AcceptedCharactersInternal.None); + ParseBlockTest("", new MarkupBlock(BlockFactory.HtmlCommentBlock("--"))); } [Fact] @@ -432,8 +441,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( Factory.Markup("").Accepts(AcceptedCharactersInternal.None)), - Factory.Markup("").Accepts(AcceptedCharactersInternal.None), - Factory.Markup("-->"), + BlockFactory.HtmlCommentBlock("").Accepts(AcceptedCharactersInternal.None), new MarkupTagBlock( Factory.Markup("").Accepts(AcceptedCharactersInternal.None)))); } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlDocumentTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlDocumentTest.cs index 0f06fe578a..b7f5eeb87d 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlDocumentTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlDocumentTest.cs @@ -214,7 +214,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy [Fact] public void ParseDocumentReturnsOneMarkupSegmentIfNoCodeBlocksEncountered() { - SingleSpanDocumentTest("Foo BazBarBar"); + + // Act + var symbol = sut.AcceptAllButLastDoubleHyphens(); + + // Assert + Assert.Equal(doubleHyphenSymbol, symbol); + Assert.True(sut.At(HtmlSymbolType.CloseAngle)); + Assert.Equal(doubleHyphenSymbol, sut.PreviousSymbol); + } + + [Fact] + public void AcceptAllButLastDoubleHypens_ReturnsTheDoubleHyphenSymbolAfterAcceptingTheDash() + { + // Arrange + var sut = CreateTestParserForContent("--->"); + + // Act + var symbol = sut.AcceptAllButLastDoubleHyphens(); + + // Assert + Assert.Equal(doubleHyphenSymbol, symbol); + Assert.True(sut.At(HtmlSymbolType.CloseAngle)); + Assert.True(HtmlMarkupParser.IsHyphen(sut.PreviousSymbol)); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsTrueForEmptyCommentTag() + { + // Arrange + var sut = CreateTestParserForContent("---->"); + + // Act & Assert + Assert.True(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsTrueForValidCommentTag() + { + // Arrange + var sut = CreateTestParserForContent("-- Some comment content in here -->"); + + // Act & Assert + Assert.True(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsTrueForValidCommentTagWithExtraDashesAtClosingTag() + { + // Arrange + var sut = CreateTestParserForContent("-- Some comment content in here ----->"); + + // Act & Assert + Assert.True(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsFalseForContentWithBadEndingAndExtraDash() + { + // Arrange + var sut = CreateTestParserForContent("-- Some comment content in here "); + + // Act & Assert + Assert.False(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsTrueForValidCommentTagWithExtraInfoAfter() + { + // Arrange + var sut = CreateTestParserForContent("-- comment --> the first part is a valid comment without the Open angle and bang symbols"); + + // Act & Assert + Assert.True(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsFalseForNotClosedComment() + { + // Arrange + var sut = CreateTestParserForContent("-- not closed comment"); + + // Act & Assert + Assert.False(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsFalseForCommentWithoutLastClosingAngle() + { + // Arrange + var sut = CreateTestParserForContent("-- not closed comment--"); + + // Act & Assert + Assert.False(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsHtmlCommentAhead_ReturnsTrueForCommentWithCodeInside() + { + // Arrange + var sut = CreateTestParserForContent("-- not closed @DateTime.Now comment-->"); + + // Act & Assert + Assert.True(sut.IsHtmlCommentAhead()); + } + + [Fact] + public void IsCommentContentEndingInvalid_ReturnsFalseForAllowedContent() + { + // Arrange + var expectedSymbol1 = new HtmlSymbol("a", HtmlSymbolType.Text); + var sequence = Enumerable.Range((int)'a', 26).Select(item => new HtmlSymbol(((char)item).ToString(), HtmlSymbolType.Text)); + + // Act & Assert + Assert.False(HtmlMarkupParser.IsCommentContentEndingInvalid(sequence)); + } + + [Fact] + public void IsCommentContentEndingInvalid_ReturnsTrueForDisallowedContent() + { + // Arrange + var expectedSymbol1 = new HtmlSymbol("a", HtmlSymbolType.Text); + var sequence = new[] { new HtmlSymbol("<", HtmlSymbolType.OpenAngle), new HtmlSymbol("!", HtmlSymbolType.Bang), new HtmlSymbol("-", HtmlSymbolType.Text) }; + + // Act & Assert + Assert.True(HtmlMarkupParser.IsCommentContentEndingInvalid(sequence)); + } + + [Fact] + public void IsCommentContentEndingInvalid_ReturnsFalseForEmptyContent() + { + // Arrange + var expectedSymbol1 = new HtmlSymbol("a", HtmlSymbolType.Text); + var sequence = Array.Empty(); + + // Act & Assert + Assert.False(HtmlMarkupParser.IsCommentContentEndingInvalid(sequence)); + } + + private class TestHtmlMarkupParser : HtmlMarkupParser + { + public new HtmlSymbol PreviousSymbol + { + get => base.PreviousSymbol; + } + + public new bool IsHtmlCommentAhead() + { + return base.IsHtmlCommentAhead(); + } + + public TestHtmlMarkupParser(ParserContext context) : base(context) + { + this.EnsureCurrent(); + } + + public new HtmlSymbol AcceptAllButLastDoubleHyphens() + { + return base.AcceptAllButLastDoubleHyphens(); + } + + public override void BuildSpan(SpanBuilder span, SourceLocation start, string content) + { + base.BuildSpan(span, start, content); + } + } + + private static TestHtmlMarkupParser CreateTestParserForContent(string content) + { + var source = TestRazorSourceDocument.Create(content); + var options = RazorParserOptions.CreateDefault(); + var context = new ParserContext(source, options); + + return new TestHtmlMarkupParser(context); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlTagsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlTagsTest.cs index 0a86c7a06b..5f22a1bae7 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlTagsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlTagsTest.cs @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { ParseBlockTest(" Bar", new MarkupBlock( - Factory.Markup("").Accepts(AcceptedCharactersInternal.None), + BlockFactory.HtmlCommentBlock("Foo"), Factory.Markup(" ").Accepts(AcceptedCharactersInternal.None))); } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlToCodeSwitchTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlToCodeSwitchTest.cs index 29c1dfb81a..928762fbc6 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlToCodeSwitchTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/HtmlToCodeSwitchTest.cs @@ -112,13 +112,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( Factory.Markup("").Accepts(AcceptedCharactersInternal.None)), - Factory.Markup("").Accepts(AcceptedCharactersInternal.None), + BlockFactory.HtmlCommentBlock(Factory, f => new SyntaxTreeNode[] { + f.Markup(" ").Accepts(AcceptedCharactersInternal.WhiteSpace), + new ExpressionBlock( + f.CodeTransition(), + f.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + f.Markup(" ").Accepts(AcceptedCharactersInternal.WhiteSpace) }), new MarkupTagBlock( Factory.Markup("").Accepts(AcceptedCharactersInternal.None)))); } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs index 9b2adc4d07..88c774c93c 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperBlockRewriterTest.cs @@ -2950,7 +2950,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy intType), RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey( new SourceSpan(7, 0, 7, 11), - "int-prefix-", + "int-prefix-", "input"), } }, @@ -2973,7 +2973,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy stringType), RazorDiagnosticFactory.CreateParsing_TagHelperIndexerAttributeNameMustIncludeKey( new SourceSpan(7, 0, 7, 14), - "string-prefix-", + "string-prefix-", "input"), } }, @@ -3638,7 +3638,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy RazorDiagnosticFactory.CreateTagHelper_EmptyBoundAttribute( new SourceSpan(7, 0, 7, 21), "bound-required-string", - "input", + "input", stringType), } }, @@ -3962,7 +3962,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy .Build(), }; - var featureFlags = new TestRazorParserFeatureFlags(allowMinimizedBooleanTagHelperAttributes: false); + var featureFlags = new TestRazorParserFeatureFlags(allowMinimizedBooleanTagHelperAttributes: false, allowHtmlCommentsInTagHelper: false); var expectedOutput = new MarkupBlock( new MarkupTagHelperBlock( @@ -3994,12 +3994,15 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy private class TestRazorParserFeatureFlags : RazorParserFeatureFlags { - public TestRazorParserFeatureFlags(bool allowMinimizedBooleanTagHelperAttributes) + public TestRazorParserFeatureFlags(bool allowMinimizedBooleanTagHelperAttributes, bool allowHtmlCommentsInTagHelper) { AllowMinimizedBooleanTagHelperAttributes = allowMinimizedBooleanTagHelperAttributes; + AllowHtmlCommentsInTagHelpers = allowHtmlCommentsInTagHelper; } public override bool AllowMinimizedBooleanTagHelperAttributes { get; } + + public override bool AllowHtmlCommentsInTagHelpers { get; } } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs index 2feccbe1a4..3f37c82c4d 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TagHelperParseTreeRewriterTest.cs @@ -254,7 +254,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var descriptors = new TagHelperDescriptor[] { TagHelperDescriptorBuilder.Create("InputTagHelper", "SomeAssembly") - .TagMatchingRuleDescriptor(rule => + .TagMatchingRuleDescriptor(rule => rule .RequireTagName("input") .RequireTagStructure(TagStructure.WithoutEndTag)) @@ -371,7 +371,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy .TagMatchingRuleDescriptor(rule => rule.RequireTagName("strong")) .Build(), }; - + // Act & Assert EvaluateData( descriptors, @@ -793,7 +793,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var descriptors = new TagHelperDescriptor[] { TagHelperDescriptorBuilder.Create("StrongTagHelper", "SomeAssembly") - .TagMatchingRuleDescriptor(rule => + .TagMatchingRuleDescriptor(rule => rule .RequireTagName("strong") .RequireAttributeDescriptor(attribute => attribute.Name("required"))) @@ -830,7 +830,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy .TagMatchingRuleDescriptor(rule => rule.RequireTagName("strong")) .Build(), TagHelperDescriptorBuilder.Create("BRTagHelper", "SomeAssembly") - .TagMatchingRuleDescriptor(rule => + .TagMatchingRuleDescriptor(rule => rule .RequireTagName("br") .RequireTagStructure(TagStructure.WithoutEndTag)) @@ -1108,6 +1108,243 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy EvaluateData(descriptors, documentContent, (MarkupBlock)expectedOutput, (RazorDiagnostic[])expectedErrors); } + [Fact] + public void Rewrite_AllowsSimpleHtmlCommentsAsChildren() + { + // Arrange + IEnumerable allowedChildren = new List { "b" }; + string literal = "asdf"; + string commentOutput = "Hello World"; + string expectedOutput = $"

{literal}

"; + + var pTagHelperBuilder = TagHelperDescriptorBuilder + .Create("PTagHelper", "SomeAssembly") + .TagMatchingRuleDescriptor(rule => rule.RequireTagName("p")); + foreach (var childTag in allowedChildren) + { + pTagHelperBuilder.AllowChildTag(childTag); + } + + var descriptors = new TagHelperDescriptor[] + { + pTagHelperBuilder.Build() + }; + + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + var expectedMarkup = new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + factory.Markup(literal), + blockFactory.MarkupTagBlock(""), + blockFactory.HtmlCommentBlock(commentOutput))); + + // Act & Assert + EvaluateData( + descriptors, + expectedOutput, + expectedMarkup, + Array.Empty()); + } + + [Fact] + public void Rewrite_DoesntAllowSimpleHtmlCommentsAsChildrenWhenFeatureFlagIsOff() + { + // Arrange + Func nestedTagError = + (childName, parentName, allowed, location, length) => + RazorDiagnosticFactory.CreateTagHelper_InvalidNestedTag( + new SourceSpan(absoluteIndex: location, lineIndex: 0, characterIndex: location, length: length), childName, parentName, allowed); + Func nestedContentError = + (parentName, allowed, location, length) => + RazorDiagnosticFactory.CreateTagHelper_CannotHaveNonTagContent( + new SourceSpan(absoluteIndex: location, lineIndex: 0, characterIndex: location, length: length), parentName, allowed); + + IEnumerable allowedChildren = new List { "b" }; + string comment1 = "Hello"; + string expectedOutput = $"

"; + + var pTagHelperBuilder = TagHelperDescriptorBuilder + .Create("PTagHelper", "SomeAssembly") + .TagMatchingRuleDescriptor(rule => rule.RequireTagName("p")); + foreach (var childTag in allowedChildren) + { + pTagHelperBuilder.AllowChildTag(childTag); + } + + var descriptors = new TagHelperDescriptor[] + { + pTagHelperBuilder.Build() + }; + + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + var expectedMarkup = new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.HtmlCommentBlock(comment1))); + + // Act & Assert + EvaluateData( + descriptors, + expectedOutput, + expectedMarkup, + new[] + { + nestedContentError("p", "b", 3, 4), + nestedContentError("p", "b", 7, 5), + nestedContentError("p", "b", 12, 3), + }, + featureFlags: RazorParserFeatureFlags.Create(RazorLanguageVersion.Version_2_0)); + } + + [Fact] + public void Rewrite_FailsForContentWithCommentsAsChildren() + { + // Arrange + Func nestedTagError = + (childName, parentName, allowed, location, length) => + RazorDiagnosticFactory.CreateTagHelper_InvalidNestedTag( + new SourceSpan(absoluteIndex: location, lineIndex: 0, characterIndex: location, length: length), childName, parentName, allowed); + Func nestedContentError = + (parentName, allowed, location, length) => + RazorDiagnosticFactory.CreateTagHelper_CannotHaveNonTagContent( + new SourceSpan(absoluteIndex: location, lineIndex: 0, characterIndex: location, length: length), parentName, allowed); + + IEnumerable allowedChildren = new List { "b" }; + string comment1 = "Hello"; + string literal = "asdf"; + string comment2 = "World"; + string expectedOutput = $"

{literal}

"; + + var pTagHelperBuilder = TagHelperDescriptorBuilder + .Create("PTagHelper", "SomeAssembly") + .TagMatchingRuleDescriptor(rule => rule.RequireTagName("p")); + foreach (var childTag in allowedChildren) + { + pTagHelperBuilder.AllowChildTag(childTag); + } + + var descriptors = new TagHelperDescriptor[] + { + pTagHelperBuilder.Build() + }; + + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + var expectedMarkup = new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.HtmlCommentBlock(comment1), + factory.Markup(literal), + blockFactory.HtmlCommentBlock(comment2))); + + // Act & Assert + EvaluateData( + descriptors, + expectedOutput, + expectedMarkup, + new[] + { + nestedContentError("p", "b", 15, 4), + }); + } + + [Fact] + public void Rewrite_AllowsRazorCommentsAsChildren() + { + // Arrange + IEnumerable allowedChildren = new List { "b" }; + string literal = "asdf"; + string commentOutput = $"@*{literal}*@"; + string expectedOutput = $"

{literal}{commentOutput}

"; + + var pTagHelperBuilder = TagHelperDescriptorBuilder + .Create("PTagHelper", "SomeAssembly") + .TagMatchingRuleDescriptor(rule => rule.RequireTagName("p")); + foreach (var childTag in allowedChildren) + { + pTagHelperBuilder.AllowChildTag(childTag); + } + + var descriptors = new TagHelperDescriptor[] + { + pTagHelperBuilder.Build() + }; + + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + var expectedMarkup = new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + factory.Markup(literal), + blockFactory.MarkupTagBlock(""), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Comment, new HtmlSymbol(literal, HtmlSymbolType.RazorComment)).Accepts(AcceptedCharactersInternal.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None)))); + + // Act & Assert + EvaluateData( + descriptors, + expectedOutput, + expectedMarkup, + Array.Empty()); + } + + [Fact] + public void Rewrite_AllowsRazorMarkupInHtmlComment() + { + // Arrange + IEnumerable allowedChildren = new List { "b" }; + string literal = "asdf"; + string part1 = "Hello "; + string part2 = "World"; + string commentStart = ""; + string expectedOutput = $"

{literal}{commentStart}{part1}@{part2}{commentEnd}

"; + + var pTagHelperBuilder = TagHelperDescriptorBuilder + .Create("PTagHelper", "SomeAssembly") + .TagMatchingRuleDescriptor(rule => rule.RequireTagName("p")); + foreach (var childTag in allowedChildren) + { + pTagHelperBuilder.AllowChildTag(childTag); + } + + var descriptors = new TagHelperDescriptor[] + { + pTagHelperBuilder.Build() + }; + + var factory = new SpanFactory(); + var blockFactory = new BlockFactory(factory); + + var expectedMarkup = new MarkupBlock( + new MarkupTagHelperBlock("p", + blockFactory.MarkupTagBlock(""), + factory.Markup(literal), + blockFactory.MarkupTagBlock(""), + BlockFactory.HtmlCommentBlock(factory, f => new SyntaxTreeNode[] { + f.Markup(part1).Accepts(AcceptedCharactersInternal.WhiteSpace), + new ExpressionBlock( + f.CodeTransition(), + f.Code(part2) + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)) }))); + + // Act & Assert + EvaluateData( + descriptors, + expectedOutput, + expectedMarkup, + Array.Empty()); + } + [Fact] public void Rewrite_UnderstandsNullTagNameWithAllowedChildrenForCatchAll() { @@ -1173,7 +1410,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var descriptors = new TagHelperDescriptor[] { TagHelperDescriptorBuilder.Create("InputTagHelper", "SomeAssembly") - .TagMatchingRuleDescriptor(rule => + .TagMatchingRuleDescriptor(rule => rule .RequireTagName("input") .RequireTagStructure(TagStructure.WithoutEndTag)) @@ -1646,7 +1883,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var descriptors = new TagHelperDescriptor[] { TagHelperDescriptorBuilder.Create("pTagHelper", "SomeAssembly") - .TagMatchingRuleDescriptor(rule => + .TagMatchingRuleDescriptor(rule => rule .RequireTagName("p") .RequireAttributeDescriptor(attribute => attribute.Name("class"))) @@ -3901,14 +4138,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy get { var factory = new SpanFactory(); - + var blockFactory = new BlockFactory(factory); yield return new object[] { "", new MarkupBlock( new MarkupTagBlock( factory.Markup("")), - factory.Markup(""), + blockFactory.HtmlCommentBlock (" Hello World "), new MarkupTagBlock( factory.Markup(""))) }; @@ -3918,13 +4155,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new MarkupBlock( new MarkupTagBlock( factory.Markup("")), - factory.Markup(""), + BlockFactory.HtmlCommentBlock(factory, f=> new SyntaxTreeNode[]{ + f.Markup(" ").Accepts(AcceptedCharactersInternal.WhiteSpace), + new ExpressionBlock( + f.CodeTransition(), + f.Code("foo") + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + factory.Markup(" ").Accepts(AcceptedCharactersInternal.WhiteSpace) }), new MarkupTagBlock( factory.Markup(""))) }; @@ -4000,8 +4238,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy new ExpressionBlock( factory.CodeTransition(), factory.Code("foo") - .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) - .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), + .AsImplicitExpression(CSharpCodeParser.DefaultKeywords) + .Accepts(AcceptedCharactersInternal.NonWhiteSpace)), factory.Markup(" ]]>"), new MarkupTagBlock( factory.Markup("
"))) diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs index 6abd70ad3e..aa9ea560cc 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/TokenizerLookaheadTest.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Linq; using System.Text; using Xunit; @@ -56,6 +56,85 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy Assert.Equal("pre-existing values", tokenizer.Buffer.ToString(), StringComparer.Ordinal); } + [Fact] + public void LookaheadUntil_PassesThePreviousSymbolsInTheSameOrder() + { + // Arrange + var tokenizer = CreateContentTokenizer("asdf--fvd--<"); + + // Act + var i = 3; + IEnumerable previousSymbols = null; + var symbolFound = tokenizer.LookaheadUntil((s, p) => + { + previousSymbols = p; + return --i == 0; + }); + + // Assert + Assert.Equal(4, previousSymbols.Count()); + + // For the very first element, there will be no previous items, so null is expected + var orderIndex = 0; + Assert.Null(previousSymbols.ElementAt(orderIndex++)); + Assert.Equal(new HtmlSymbol("asdf", HtmlSymbolType.Text), previousSymbols.ElementAt(orderIndex++)); + Assert.Equal(new HtmlSymbol("--", HtmlSymbolType.DoubleHyphen), previousSymbols.ElementAt(orderIndex++)); + Assert.Equal(new HtmlSymbol("fvd", HtmlSymbolType.Text), previousSymbols.ElementAt(orderIndex++)); + } + + [Fact] + public void LookaheadUntil_ReturnsFalseAfterIteratingOverAllSymbolsIfConditionIsNotMet() + { + // Arrange + var tokenizer = CreateContentTokenizer("asdf--fvd"); + + // Act + var symbols = new Stack(); + var symbolFound = tokenizer.LookaheadUntil((s, p) => + { + symbols.Push(s); + return false; + }); + + // Assert + Assert.False(symbolFound); + Assert.Equal(3, symbols.Count); + Assert.Equal(new HtmlSymbol("fvd", HtmlSymbolType.Text), symbols.Pop()); + Assert.Equal(new HtmlSymbol("--", HtmlSymbolType.DoubleHyphen), symbols.Pop()); + Assert.Equal(new HtmlSymbol("asdf", HtmlSymbolType.Text), symbols.Pop()); + } + + [Fact] + public void LookaheadUntil_ReturnsTrueAndBreaksIteration() + { + // Arrange + var tokenizer = CreateContentTokenizer("asdf--fvd"); + + // Act + var symbols = new Stack(); + var symbolFound = tokenizer.LookaheadUntil((s, p) => + { + symbols.Push(s); + return s.Type == HtmlSymbolType.DoubleHyphen; + }); + + // Assert + Assert.True(symbolFound); + Assert.Equal(2, symbols.Count); + Assert.Equal(new HtmlSymbol("--", HtmlSymbolType.DoubleHyphen), symbols.Pop()); + Assert.Equal(new HtmlSymbol("asdf", HtmlSymbolType.Text), symbols.Pop()); + } + + private static TestTokenizerBackedParser CreateContentTokenizer(string content) + { + var source = TestRazorSourceDocument.Create(content); + var options = RazorParserOptions.CreateDefault(); + var context = new ParserContext(source, options); + + var tokenizer = new TestTokenizerBackedParser(HtmlLanguageCharacteristics.Instance, context); + return tokenizer; + } + private class ExposedTokenizer : Tokenizer { public ExposedTokenizer(string input) @@ -116,5 +195,27 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy throw new NotImplementedException(); } } + + private class TestTokenizerBackedParser : TokenizerBackedParser + { + internal TestTokenizerBackedParser(LanguageCharacteristics language, ParserContext context) : base(language, context) + { + } + + public override void ParseBlock() + { + throw new NotImplementedException(); + } + + protected override bool SymbolTypeEquals(HtmlSymbolType x, HtmlSymbolType y) + { + throw new NotImplementedException(); + } + + internal new bool LookaheadUntil(Func, bool> condition) + { + return base.LookaheadUntil(condition); + } + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs index db57a33fdf..cfdb9f023d 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/RazorParserFeatureFlagsTest.cs @@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Razor.Language // Assert Assert.True(context.AllowMinimizedBooleanTagHelperAttributes); + Assert.True(context.AllowHtmlCommentsInTagHelpers); } [Fact] @@ -26,6 +27,7 @@ namespace Microsoft.AspNetCore.Razor.Language // Assert Assert.False(context.AllowMinimizedBooleanTagHelperAttributes); + Assert.False(context.AllowHtmlCommentsInTagHelpers); } } } diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TagHelperParseTreeRewriterTests.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/TagHelperParseTreeRewriterTests.cs new file mode 100644 index 0000000000..a24a9c8d4f --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TagHelperParseTreeRewriterTests.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.AspNetCore.Razor.Language.Legacy; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.Test +{ + public class TagHelperParseTreeRewriterTests + { + public void IsComment_ReturnsTrueForSpanInHtmlCommentBlock() + { + // Arrange + SpanFactory spanFactory = new SpanFactory(); + + Span content = spanFactory.Markup(""); + Block commentBlock = new HtmlCommentBlock(content); + + // Act + bool actualResult = TagHelperParseTreeRewriter.IsComment(content); + + // Assert + Assert.True(actualResult); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_DesignTime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_DesignTime.ir.txt index aa6a00864d..4523eae7a3 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_DesignTime.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_DesignTime.ir.txt @@ -10,7 +10,10 @@ Document - IntermediateToken - - CSharp - #pragma warning restore 0414 MethodDeclaration - - public async - System.Threading.Tasks.Task - ExecuteAsync HtmlContent - (0:0,0 [45] HtmlCommentWithQuote_Double.cshtml) - IntermediateToken - (0:0,0 [12] HtmlCommentWithQuote_Double.cshtml) - Html - \n + IntermediateToken - (0:0,0 [4] HtmlCommentWithQuote_Double.cshtml) - Html - + IntermediateToken - (10:0,10 [2] HtmlCommentWithQuote_Double.cshtml) - Html - \n IntermediateToken - (12:1,0 [4] HtmlCommentWithQuote_Double.cshtml) - Html - diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_Runtime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_Runtime.ir.txt index 343985c189..046cad8407 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_Runtime.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Double_Runtime.ir.txt @@ -5,7 +5,10 @@ Document - ClassDeclaration - - public - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_HtmlCommentWithQuote_Double_Runtime - - MethodDeclaration - - public async - System.Threading.Tasks.Task - ExecuteAsync HtmlContent - (0:0,0 [45] HtmlCommentWithQuote_Double.cshtml) - IntermediateToken - (0:0,0 [12] HtmlCommentWithQuote_Double.cshtml) - Html - \n + IntermediateToken - (0:0,0 [4] HtmlCommentWithQuote_Double.cshtml) - Html - + IntermediateToken - (10:0,10 [2] HtmlCommentWithQuote_Double.cshtml) - Html - \n IntermediateToken - (12:1,0 [4] HtmlCommentWithQuote_Double.cshtml) - Html - diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_DesignTime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_DesignTime.ir.txt index 04795676d1..8b5f5b3ed5 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_DesignTime.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_DesignTime.ir.txt @@ -10,7 +10,10 @@ Document - IntermediateToken - - CSharp - #pragma warning restore 0414 MethodDeclaration - - public async - System.Threading.Tasks.Task - ExecuteAsync HtmlContent - (0:0,0 [45] HtmlCommentWithQuote_Single.cshtml) - IntermediateToken - (0:0,0 [12] HtmlCommentWithQuote_Single.cshtml) - Html - \n + IntermediateToken - (0:0,0 [4] HtmlCommentWithQuote_Single.cshtml) - Html - + IntermediateToken - (10:0,10 [2] HtmlCommentWithQuote_Single.cshtml) - Html - \n IntermediateToken - (12:1,0 [4] HtmlCommentWithQuote_Single.cshtml) - Html - diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_Runtime.ir.txt b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_Runtime.ir.txt index 65aafb6fe7..8596f51503 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_Runtime.ir.txt +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/TestFiles/IntegrationTests/CodeGenerationIntegrationTest/HtmlCommentWithQuote_Single_Runtime.ir.txt @@ -5,7 +5,10 @@ Document - ClassDeclaration - - public - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_HtmlCommentWithQuote_Single_Runtime - - MethodDeclaration - - public async - System.Threading.Tasks.Task - ExecuteAsync HtmlContent - (0:0,0 [45] HtmlCommentWithQuote_Single.cshtml) - IntermediateToken - (0:0,0 [12] HtmlCommentWithQuote_Single.cshtml) - Html - \n + IntermediateToken - (0:0,0 [4] HtmlCommentWithQuote_Single.cshtml) - Html - + IntermediateToken - (10:0,10 [2] HtmlCommentWithQuote_Single.cshtml) - Html - \n IntermediateToken - (12:1,0 [4] HtmlCommentWithQuote_Single.cshtml) - Html - diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs index 5c2ee95394..2551fee917 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Language.Legacy @@ -55,6 +56,24 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy ); } + public HtmlCommentBlock HtmlCommentBlock(string content) + { + return HtmlCommentBlock(_factory, f => new SyntaxTreeNode[] { f.Markup(content).Accepts(AcceptedCharactersInternal.WhiteSpace) }); + } + + public static HtmlCommentBlock HtmlCommentBlock(SpanFactory factory, Func> nodesBuilder = null) + { + var nodes = new List(); + nodes.Add(factory.Markup("").Accepts(AcceptedCharactersInternal.None)); + + return new HtmlCommentBlock(nodes.ToArray()); + } + public Block TagHelperBlock( string tagName, TagMode tagMode, diff --git a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs index c08e88d56f..56deb6b460 100644 --- a/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs +++ b/test/Microsoft.AspNetCore.Razor.Test.Common/Language/Legacy/BlockTypes.cs @@ -217,4 +217,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { } } + + internal class HtmlCommentBlock : Block + { + private const BlockKindInternal ThisBlockKind = BlockKindInternal.HtmlComment; + + public HtmlCommentBlock(params SyntaxTreeNode[] children) + : base(ThisBlockKind, children, ParentChunkGenerator.Null) + { + } + } }