From 24536898049364bc7df23abd4fc9e8d0bc0085e4 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Mon, 12 Jun 2017 17:36:41 -0700 Subject: [PATCH] Add file scoped extensible directives. - Added `DirectiveUsage` to enable extensible directive authors to indicate how their directives should be used. Currently support `Unrestricted` (how section directives have always worked) and a file scoped singly occurring directive. - Added directive parsing tests. - Removed no longer used `BlockKindInternal` items. #1376 --- .../DirectiveDescriptor.cs | 13 +- .../DirectiveUsage.cs | 11 + .../IDirectiveDescriptorBuilder.cs | 5 + .../Legacy/BlockKindInternal.cs | 3 - .../Legacy/CSharpCodeParser.cs | 63 +++- .../Legacy/SyntaxTreeBuilder.cs | 2 +- .../Properties/Resources.Designer.cs | 28 ++ .../Resources.resx | 6 + .../Legacy/BlockTest.cs | 4 +- .../Legacy/CSharpDirectivesTest.cs | 289 ++++++++++++++++++ 10 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Language/DirectiveUsage.cs diff --git a/src/Microsoft.AspNetCore.Razor.Language/DirectiveDescriptor.cs b/src/Microsoft.AspNetCore.Razor.Language/DirectiveDescriptor.cs index afb315948f..f2591b9f68 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/DirectiveDescriptor.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/DirectiveDescriptor.cs @@ -38,6 +38,11 @@ namespace Microsoft.AspNetCore.Razor.Language /// public abstract DirectiveKind Kind { get; } + /// + /// Gets the way a directive can be used. The usage determines how many, and where directives can exist per document. + /// + public abstract DirectiveUsage Usage { get; } + /// /// Gets the list of directive tokens that can follow the directive keyword. /// @@ -189,6 +194,8 @@ namespace Microsoft.AspNetCore.Razor.Language public DirectiveKind Kind { get; } + public DirectiveUsage Usage { get; set; } + public IList Tokens { get; } public DirectiveDescriptor Build() @@ -218,7 +225,7 @@ namespace Microsoft.AspNetCore.Razor.Language } } - return new DefaultDirectiveDescriptor(Directive, Kind, Tokens.ToArray(), DisplayName, Description); + return new DefaultDirectiveDescriptor(Directive, Kind, Usage, Tokens.ToArray(), DisplayName, Description); } } @@ -227,12 +234,14 @@ namespace Microsoft.AspNetCore.Razor.Language public DefaultDirectiveDescriptor( string directive, DirectiveKind kind, + DirectiveUsage usage, DirectiveTokenDescriptor[] tokens, string displayName, string description) { Directive = directive; Kind = kind; + Usage = usage; Tokens = tokens; DisplayName = displayName; Description = description; @@ -246,6 +255,8 @@ namespace Microsoft.AspNetCore.Razor.Language public override DirectiveKind Kind { get; } + public override DirectiveUsage Usage { get; } + public override IReadOnlyList Tokens { get; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Language/DirectiveUsage.cs b/src/Microsoft.AspNetCore.Razor.Language/DirectiveUsage.cs new file mode 100644 index 0000000000..5af9311748 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Language/DirectiveUsage.cs @@ -0,0 +1,11 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Language +{ + public enum DirectiveUsage + { + Unrestricted, + FileScopedSinglyOccurring, + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Language/IDirectiveDescriptorBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/IDirectiveDescriptorBuilder.cs index c8e79d8d05..465069dbcf 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/IDirectiveDescriptorBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/IDirectiveDescriptorBuilder.cs @@ -30,6 +30,11 @@ namespace Microsoft.AspNetCore.Razor.Language /// DirectiveKind Kind { get; } + /// + /// Gets or sets the directive usage. The usage determines how many, and where directives can exist per document. + /// + DirectiveUsage Usage { get; set; } + /// /// Gets a list of the directive tokens. /// diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs index 7533909396..8ad87468e0 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/BlockKindInternal.cs @@ -8,13 +8,10 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy // Code Statement, Directive, - Functions, Expression, - Helper, // Markup Markup, - Section, Template, // Special diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs index f85d6b2d46..960cc7cf04 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/CSharpCodeParser.cs @@ -54,6 +54,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy private Dictionary _directiveParsers = new Dictionary(StringComparer.Ordinal); private Dictionary> _keywordParsers = new Dictionary>(); + private HashSet _seenDirectives = new HashSet(StringComparer.Ordinal); public CSharpCodeParser(ParserContext context) : this(directives: Enumerable.Empty(), context: context) @@ -91,7 +92,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { foreach (var directive in directives) { - _directiveParsers.Add(directive, handler); + _directiveParsers.Add(directive, () => + { + handler(); + _seenDirectives.Add(directive); + }); + Keywords.Add(directive); // These C# keywords are reserved for use in directives. It's an error to use them outside of @@ -1589,10 +1595,13 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy AcceptAndMoveNext(); Output(SpanKindInternal.MetaCode, AcceptedCharactersInternal.None); + // Even if an error was logged do not bail out early. If a directive was used incorrectly it doesn't mean it can't be parsed. + ValidateDirectiveUsage(descriptor); + for (var i = 0; i < descriptor.Tokens.Count; i++) { if (!At(CSharpSymbolType.WhiteSpace) && - !At(CSharpSymbolType.NewLine) && + !At(CSharpSymbolType.NewLine) && !EndOfFile) { Context.ErrorSink.OnError( @@ -1767,6 +1776,56 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } + + private void ValidateDirectiveUsage(DirectiveDescriptor descriptor) + { + if (descriptor.Usage == DirectiveUsage.FileScopedSinglyOccurring) + { + if (_seenDirectives.Contains(descriptor.Directive)) + { + UsageError(Resources.FormatDuplicateDirective(descriptor.Directive)); + return; + } + + var root = Context.Builder.ActiveBlocks.Last(); + for (var i = 0; i < root.Children.Count; i++) + { + // Directives, comments and whitespace are valid prior to an unnested directive. + + var child = root.Children[i]; + if (child is Legacy.Block block) + { + if (block.Type == BlockKindInternal.Directive || block.Type == BlockKindInternal.Comment) + { + continue; + } + } + else if (child is Span span) + { + if (span.Length == 0 || + span.Kind == SpanKindInternal.Comment || + span.Symbols.All(symbol => string.IsNullOrWhiteSpace(symbol.Content))) + { + continue; + } + } + + UsageError(Resources.FormatDirectiveMustExistBeforeMarkupOrCode(descriptor.Directive)); + return; + } + } + + return; + + void UsageError(string message) + { + // There wil always be at least 1 child because of the `@` transition. + var directiveStart = Context.Builder.CurrentBlock.Children.First().Start; + var errorLength = /* @ */ 1 + descriptor.Directive.Length; + Context.ErrorSink.OnError(directiveStart, message, errorLength); + } + } + private void ParseDirectiveBlock(DirectiveDescriptor descriptor, Action parseChildren) { if (EndOfFile) diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeBuilder.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeBuilder.cs index 6dc4cac374..65db009fca 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/SyntaxTreeBuilder.cs @@ -17,7 +17,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy _endBlock = EndBlock; } - public IEnumerable ActiveBlocks => _blockStack; + public IReadOnlyCollection ActiveBlocks => _blockStack; public BlockBuilder CurrentBlock => _blockStack.Peek(); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs index 602d0627af..f03e6bd554 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Properties/Resources.Designer.cs @@ -514,6 +514,34 @@ namespace Microsoft.AspNetCore.Razor.Language internal static string FormatIntermediateNodeReference_CollectionIsReadOnly(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("IntermediateNodeReference_CollectionIsReadOnly"), p0); + /// + /// The '{0}' directive may only occur once per document. + /// + internal static string DuplicateDirective + { + get => GetString("DuplicateDirective"); + } + + /// + /// The '{0}' directive may only occur once per document. + /// + internal static string FormatDuplicateDirective(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DuplicateDirective"), p0); + + /// + /// The '{0}' directive must exist prior to markup or code. + /// + internal static string DirectiveMustExistBeforeMarkupOrCode + { + get => GetString("DirectiveMustExistBeforeMarkupOrCode"); + } + + /// + /// The '{0}' directive must exist prior to markup or code. + /// + internal static string FormatDirectiveMustExistBeforeMarkupOrCode(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("DirectiveMustExistBeforeMarkupOrCode"), p0); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx index f1ab0f0674..3298994894 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Resources.resx +++ b/src/Microsoft.AspNetCore.Razor.Language/Resources.resx @@ -225,4 +225,10 @@ The node '{0}' has a read-only child collection and cannot be modified. + + The '{0}' directive may only occur once per document. + + + The '{0}' directive must exist prior to markup or code. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs index f182c224a9..aa826bb323 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var expected = new ExpressionChunkGenerator(); var builder = new BlockBuilder() { - Type = BlockKindInternal.Helper, + Type = BlockKindInternal.Statement, ChunkGenerator = expected }; @@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy var expected = new SpanBuilder(SourceLocation.Undefined) { Kind = SpanKindInternal.Code }.Build(); var builder = new BlockBuilder() { - Type = BlockKindInternal.Functions + Type = BlockKindInternal.Statement }; builder.Children.Add(expected); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs index 4c48b7a8de..19e0b6aa5c 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/CSharpDirectivesTest.cs @@ -11,6 +11,295 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { public class CSharpDirectivesTest : CsHtmlCodeParserTestBase { + [Fact] + public void DirectiveDescriptor_SinglePreContent_ErrorsIfNestedInCode() + { + // Arrange + var descriptor = DirectiveDescriptor.CreateDirective( + "custom", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddTypeToken(); + }); + var chunkGenerator = new DirectiveChunkGenerator(descriptor); + chunkGenerator.Diagnostics.Add( + RazorDiagnostic.Create( + new RazorError( + Resources.FormatDirectiveMustExistBeforeMarkupOrCode("custom"), + 1 + Environment.NewLine.Length, 1, 0, 7))); + + // Act & Assert + ParseCodeBlockTest( +@"{ +@custom System.Text.Encoding.ASCIIEncoding +}", + new[] { descriptor }, + new StatementBlock( + Factory.MetaCode("{").Accepts(AcceptedCharactersInternal.None), + Factory.Code(Environment.NewLine) + .AsStatement() + .AutoCompleteWith(autoCompleteString: null, atEndOfSpan: false), + new DirectiveBlock(chunkGenerator, + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false).AsDirectiveToken(descriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + Factory.EmptyCSharp().AsStatement(), + Factory.MetaCode("}").Accepts(AcceptedCharactersInternal.None))); + } + + [Fact] + public void DirectiveDescriptor_SinglePreContent_ErrorsIfNestedInHtml() + { + // Arrange + var descriptor = DirectiveDescriptor.CreateDirective( + "custom", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddTypeToken(); + }); + var chunkGenerator = new DirectiveChunkGenerator(descriptor); + chunkGenerator.Diagnostics.Add( + RazorDiagnostic.Create( + new RazorError( + Resources.FormatDirectiveMustExistBeforeMarkupOrCode("custom"), + 3 + Environment.NewLine.Length, 1, 0, 7))); + + // Act & Assert + ParseDocumentTest( +@"

+@custom System.Text.Encoding.ASCIIEncoding +

", + new[] { descriptor }, + new MarkupBlock( + BlockFactory.MarkupTagBlock("

"), + Factory.Markup(Environment.NewLine), + new DirectiveBlock(chunkGenerator, + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false).AsDirectiveToken(descriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + BlockFactory.MarkupTagBlock("

"))); + } + + [Fact] + public void DirectiveDescriptor_SinglePreContent_ErrorsIfDuplicate() + { + // Arrange + var descriptor = DirectiveDescriptor.CreateDirective( + "custom", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddTypeToken(); + }); + var chunkGenerator = new DirectiveChunkGenerator(descriptor); + chunkGenerator.Diagnostics.Add( + RazorDiagnostic.Create( + new RazorError( + Resources.FormatDuplicateDirective("custom"), + 42 + Environment.NewLine.Length, 1, 0, 7))); + + // Act & Assert + ParseDocumentTest( +@"@custom System.Text.Encoding.ASCIIEncoding +@custom System.Text.Encoding.UTF8Encoding", + new[] { descriptor }, + new MarkupBlock( + Factory.EmptyHtml(), + new DirectiveBlock(new DirectiveChunkGenerator(descriptor), + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false).AsDirectiveToken(descriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + Factory.EmptyHtml(), + new DirectiveBlock(chunkGenerator, + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.UTF8Encoding", markup: false).AsDirectiveToken(descriptor.Tokens[0])), + Factory.EmptyHtml())); + } + + [Fact] + public void DirectiveDescriptor_SinglePreContent_CanBeBeneathOtherDirectives() + { + // Arrange + var customDescriptor = DirectiveDescriptor.CreateDirective( + "custom", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddTypeToken(); + }); + var somethingDescriptor = DirectiveDescriptor.CreateDirective( + "something", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddMemberToken(); + }); + + // Act & Assert + ParseDocumentTest( +@"@custom System.Text.Encoding.ASCIIEncoding +@something Else", + new[] { customDescriptor, somethingDescriptor }, + new MarkupBlock( + Factory.EmptyHtml(), + new DirectiveBlock(new DirectiveChunkGenerator(customDescriptor), + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false).AsDirectiveToken(customDescriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + Factory.EmptyHtml(), + new DirectiveBlock(new DirectiveChunkGenerator(somethingDescriptor), + Factory.CodeTransition(), + Factory.MetaCode("something").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "Else", markup: false).AsDirectiveToken(somethingDescriptor.Tokens[0])), + Factory.EmptyHtml())); + } + + [Fact] + public void DirectiveDescriptor_SinglePreContent_CanBeBeneathOtherWhiteSpaceCommentsAndDirectives() + { + // Arrange + var customDescriptor = DirectiveDescriptor.CreateDirective( + "custom", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddTypeToken(); + }); + var somethingDescriptor = DirectiveDescriptor.CreateDirective( + "something", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddMemberToken(); + }); + + // Act & Assert + ParseDocumentTest( +@"@* There are two directives beneath this *@ +@custom System.Text.Encoding.ASCIIEncoding + +@something Else + +

This is extra

", + new[] { customDescriptor, somethingDescriptor }, + new MarkupBlock( + Factory.EmptyHtml(), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Comment, new HtmlSymbol(" There are two directives beneath this ", HtmlSymbolType.RazorComment)).Accepts(AcceptedCharactersInternal.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None)), + Factory.Markup(Environment.NewLine), + new DirectiveBlock(new DirectiveChunkGenerator(customDescriptor), + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false).AsDirectiveToken(customDescriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + Factory.Markup(Environment.NewLine), + new DirectiveBlock(new DirectiveChunkGenerator(somethingDescriptor), + Factory.CodeTransition(), + Factory.MetaCode("something").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "Else", markup: false).AsDirectiveToken(somethingDescriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + Factory.Markup(Environment.NewLine), + BlockFactory.MarkupTagBlock("

"), + Factory.Markup("This is extra"), + BlockFactory.MarkupTagBlock("

"))); + } + + [Fact] + public void DirectiveDescriptor_SinglePreContent_MixedContentErrors() + { + // Arrange + var customDescriptor = DirectiveDescriptor.CreateDirective( + "custom", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddTypeToken(); + }); + var somethingDescriptor = DirectiveDescriptor.CreateDirective( + "something", + DirectiveKind.SingleLine, + builder => + { + builder.Usage = DirectiveUsage.SinglePreContent; + builder.AddMemberToken(); + }); + var chunkGenerator = new DirectiveChunkGenerator(somethingDescriptor); + chunkGenerator.Diagnostics.Add( + RazorDiagnostic.Create( + new RazorError( + Resources.FormatDirectiveMustExistBeforeMarkupOrCode("something"), + 151 + Environment.NewLine.Length * 4, 4, 0, 10))); + + // Act & Assert + ParseDocumentTest( +@"@custom System.Text.Encoding.ASCIIEncoding +@* There is invalid content beneath this *@ +

Should cause error

+@* There is invalid content above this *@ +@something Else", + new[] { customDescriptor, somethingDescriptor }, + new MarkupBlock( + Factory.EmptyHtml(), + new DirectiveBlock(new DirectiveChunkGenerator(customDescriptor), + Factory.CodeTransition(), + Factory.MetaCode("custom").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "System.Text.Encoding.ASCIIEncoding", markup: false).AsDirectiveToken(customDescriptor.Tokens[0]), + Factory.MetaCode(Environment.NewLine).Accepts(AcceptedCharactersInternal.WhiteSpace)), + Factory.EmptyHtml(), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Comment, new HtmlSymbol(" There is invalid content beneath this ", HtmlSymbolType.RazorComment)).Accepts(AcceptedCharactersInternal.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None)), + Factory.Markup(Environment.NewLine), + BlockFactory.MarkupTagBlock("

"), + Factory.Markup("Should cause error"), + BlockFactory.MarkupTagBlock("

"), + Factory.Markup(Environment.NewLine), + new CommentBlock( + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Comment, new HtmlSymbol(" There is invalid content above this ", HtmlSymbolType.RazorComment)).Accepts(AcceptedCharactersInternal.Any), + Factory.MetaMarkup("*", HtmlSymbolType.RazorCommentStar).Accepts(AcceptedCharactersInternal.None), + Factory.MarkupTransition(HtmlSymbolType.RazorCommentTransition).Accepts(AcceptedCharactersInternal.None)), + Factory.Markup(Environment.NewLine).With(SpanChunkGenerator.Null), + new DirectiveBlock(chunkGenerator, + Factory.CodeTransition(), + Factory.MetaCode("something").Accepts(AcceptedCharactersInternal.None), + Factory.Span(SpanKindInternal.Code, " ", markup: false).Accepts(AcceptedCharactersInternal.WhiteSpace), + Factory.Span(SpanKindInternal.Code, "Else", markup: false).AsDirectiveToken(somethingDescriptor.Tokens[0])), + Factory.EmptyHtml())); + } + [Fact] public void DirectiveDescriptor_TokensMustBeSeparatedBySpace() {