From 5a22d9b52cd9ff82c228a09b210356b11a59ec2d Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 13 Feb 2014 07:03:30 -0800 Subject: [PATCH] Restore @model support in RazorHost --- WebFx.sln | 7 + samples/MvcSample/Views/Home/MyView.cshtml | 6 +- samples/MvcSample/Views/Shared/_Layout.cshtml | 3 + .../MvcRazorCodeParser.cs | 24 +- .../MvcRazorHost.cs | 14 +- .../Properties/Resources.Designer.cs | 46 +++ .../Resources.resx | 129 ++++++++ .../SetModelTypeCodeGenerator.cs | 31 -- .../MvcServices.cs | 2 +- .../MvcRazorCodeParserTest.cs | 292 ++++++++++++++++++ .../SpanFactory.cs | 242 +++++++++++++++ .../project.json | 13 + 12 files changed, 762 insertions(+), 47 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx delete mode 100644 src/Microsoft.AspNet.Mvc.Razor.Host/SetModelTypeCodeGenerator.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorCodeParserTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json diff --git a/WebFx.sln b/WebFx.sln index 69f7014e52..9bda416a19 100644 --- a/WebFx.sln +++ b/WebFx.sln @@ -47,6 +47,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.Startu EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.Startup.k10", "src\Microsoft.AspNet.Mvc.Startup\Microsoft.AspNet.Mvc.Startup.k10.csproj", "{43ECCFDF-E646-4766-B339-F5CCD69DD6C3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Mvc.Razor.Host.Test.net45", "test\Microsoft.AspNet.Mvc.Razor.Host.Test\Microsoft.AspNet.Mvc.Razor.Host.Test.net45.csproj", "{537CC0EE-4B62-4789-9AE9-94BE28E0D25A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -113,6 +115,10 @@ Global {43ECCFDF-E646-4766-B339-F5CCD69DD6C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {43ECCFDF-E646-4766-B339-F5CCD69DD6C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {43ECCFDF-E646-4766-B339-F5CCD69DD6C3}.Release|Any CPU.Build.0 = Release|Any CPU + {537CC0EE-4B62-4789-9AE9-94BE28E0D25A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {537CC0EE-4B62-4789-9AE9-94BE28E0D25A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {537CC0EE-4B62-4789-9AE9-94BE28E0D25A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {537CC0EE-4B62-4789-9AE9-94BE28E0D25A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -137,5 +143,6 @@ Global {501817DD-8143-4A50-888D-99896A82CD12} = {222CA408-93EE-473A-9325-D04989EC9FEF} {A7D7CD66-A407-4144-8AB7-07F895F87137} = {CE037E26-9EB5-48E2-B73B-06C6FF6CC9F5} {42195A56-42C0-4CFF-A982-B6E24EFC6356} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {537CC0EE-4B62-4789-9AE9-94BE28E0D25A} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} EndGlobalSection EndGlobal diff --git a/samples/MvcSample/Views/Home/MyView.cshtml b/samples/MvcSample/Views/Home/MyView.cshtml index 8e873628c0..9eee55d9f8 100644 --- a/samples/MvcSample/Views/Home/MyView.cshtml +++ b/samples/MvcSample/Views/Home/MyView.cshtml @@ -1,4 +1,5 @@ @using MvcSample.Models +@model User @{ Layout = "/Views/Shared/_Layout.cshtml"; ViewBag.Title = "Home Page"; @@ -9,11 +10,8 @@

ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.

Learn more »

-@{ - var user = new User { Name = "Test user" }; -}
-

Hello @user.Name!

+

Hello @Model.Name!

Getting started

diff --git a/samples/MvcSample/Views/Shared/_Layout.cshtml b/samples/MvcSample/Views/Shared/_Layout.cshtml index 6b9dd0672a..1f7c228ce3 100644 --- a/samples/MvcSample/Views/Shared/_Layout.cshtml +++ b/samples/MvcSample/Views/Shared/_Layout.cshtml @@ -26,6 +26,9 @@

@RenderBody()
+
+ @Model.Address +

© @DateTime.Now.Year - My ASP.NET Application

diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs index 4c5eca4bb6..959c4e0d22 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorCodeParser.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Microsoft.AspNet.Mvc.Razor.Host; using Microsoft.AspNet.Razor.Generator; using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Text; @@ -8,13 +9,15 @@ namespace Microsoft.AspNet.Mvc.Razor { public class MvcRazorCodeParser : CSharpCodeParser { + private const string GenericTypeFormat = "{0}<{1}>"; private const string ModelKeyword = "model"; - private const string GenericTypeFormatString = "{0}<{1}>"; + private readonly string _baseType; private SourceLocation? _endInheritsLocation; private bool _modelStatementFound; - public MvcRazorCodeParser() + public MvcRazorCodeParser(string baseType) { + _baseType = baseType; MapDirectives(ModelDirective, ModelKeyword); } @@ -33,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor { if (_modelStatementFound && _endInheritsLocation.HasValue) { - Context.OnError(_endInheritsLocation.Value, String.Format(CultureInfo.CurrentCulture, "MvcResources.MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword", ModelKeyword)); + Context.OnError(_endInheritsLocation.Value, Resources.MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(ModelKeyword)); } } @@ -45,14 +48,12 @@ namespace Microsoft.AspNet.Mvc.Razor SourceLocation endModelLocation = CurrentLocation; - BaseTypeDirective( - String.Format(CultureInfo.CurrentCulture, - "MvcResources.MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName", ModelKeyword), - CreateModelCodeGenerator); + BaseTypeDirective(Resources.MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName(ModelKeyword), + CreateModelCodeGenerator); if (_modelStatementFound) { - Context.OnError(endModelLocation, String.Format(CultureInfo.CurrentCulture, "MvcResources.MvcRazorCodeParser_OnlyOneModelStatementIsAllowed", ModelKeyword)); + Context.OnError(endModelLocation, Resources.MvcRazorCodeParser_OnlyOneModelStatementIsAllowed(ModelKeyword)); } _modelStatementFound = true; @@ -62,7 +63,12 @@ namespace Microsoft.AspNet.Mvc.Razor private SpanCodeGenerator CreateModelCodeGenerator(string model) { - return new SetModelTypeCodeGenerator(model, GenericTypeFormatString); + // In the event we have an empty model, the name we generate does not matter since it's a parser error. + // We'll use the non-generic version of the base type. + string baseType = String.IsNullOrEmpty(model) ? + _baseType : + String.Format(CultureInfo.InvariantCulture, GenericTypeFormat, _baseType, model); + return new SetBaseTypeCodeGenerator(baseType); } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index e450cb64cf..f2dba8f026 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser; namespace Microsoft.AspNet.Mvc.Razor { @@ -17,16 +18,20 @@ namespace Microsoft.AspNet.Mvc.Razor "Microsoft.AspNet.Mvc.Razor" }; + // CodeGenerationContext.DefaultBaseClass is set to MyBaseType. + // This field holds the type name without the generic decoration (MyBaseType) + private readonly string _baseType; + public MvcRazorHost(Type baseType) : this(baseType.FullName) { - } public MvcRazorHost(string baseType) : base(new CSharpRazorCodeLanguage()) { - DefaultBaseClass = baseType; + _baseType = baseType; + DefaultBaseClass = baseType + ""; GeneratedClassContext = new GeneratedClassContext( executeMethodName: "Execute", writeMethodName: "Write", @@ -61,6 +66,11 @@ namespace Microsoft.AspNet.Mvc.Razor } } + public override ParserBase DecorateCodeParser(ParserBase incomingCodeParser) + { + return new MvcRazorCodeParser(_baseType); + } + private static string GenerateNamespace(string rootRelativePath) { var namespaceBuilder = new StringBuilder(rootRelativePath.Length); diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..0f6988b433 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Properties/Resources.Designer.cs @@ -0,0 +1,46 @@ + +// + +namespace Microsoft.AspNet.Mvc.Razor.Host +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Mvc.Razor.Host.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// The 'inherits' keyword is not allowed when a '{0}' keyword is used. + /// + internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"), p0); + } + + /// + /// The '{0}' keyword must be followed by a type name on the same line. + /// + internal static string MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName"), p0); + } + + /// + /// Only one '{0}' statement is allowed in a file. + /// + internal static string MvcRazorCodeParser_OnlyOneModelStatementIsAllowed(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0); + } + + private static string GetString(string name) + { + string value = _resourceManager.GetString(name); + System.Diagnostics.Debug.Assert(value != null); + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx new file mode 100644 index 0000000000..57f9ae7094 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The 'inherits' keyword is not allowed when a '{0}' keyword is used. + + + The '{0}' keyword must be followed by a type name on the same line. + + + Only one '{0}' statement is allowed in a file. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/SetModelTypeCodeGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/SetModelTypeCodeGenerator.cs deleted file mode 100644 index 243e4d4aba..0000000000 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/SetModelTypeCodeGenerator.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Globalization; -using Microsoft.AspNet.Razor.Generator; - -namespace Microsoft.AspNet.Mvc.Razor -{ - internal class SetModelTypeCodeGenerator : SetBaseTypeCodeGenerator - { - private string _genericTypeFormat; - - public SetModelTypeCodeGenerator(string modelType, string genericTypeFormat) - : base(modelType) - { - _genericTypeFormat = genericTypeFormat; - } - - protected override string ResolveType(CodeGeneratorContext context, string baseType) - { - return String.Format( - CultureInfo.InvariantCulture, - _genericTypeFormat, - context.Host.DefaultBaseClass, - baseType); - } - - public override string ToString() - { - return "Model:" + BaseType; - } - } -} diff --git a/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs b/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs index 2d8bce2b35..f1dde37713 100644 --- a/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc.Startup/MvcServices.cs @@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.Startup AddInstance(provider); AddInstance(new PhysicalFileSystem(appRoot)); - AddInstance(new MvcRazorHost("Microsoft.AspNet.Mvc.Razor.RazorView")); + AddInstance(new MvcRazorHost(typeof(RazorView).FullName)); #if NET45 Add(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorCodeParserTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorCodeParserTest.cs new file mode 100644 index 0000000000..dbd89f0877 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorCodeParserTest.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor.Host.Test +{ + public class MvcRazorCodeParserTest + { + private const string DefaultBaseType = "Microsoft.AspNet.ViewPage"; + + [Fact] + public void Constructor_AddsModelKeyword() + { + var parser = new TestMvcCSharpRazorCodeParser(); + + Assert.True(parser.HasDirective("model")); + } + + [Fact] + public void ParseModelKeyword_HandlesSingleInstance() + { + // Arrange + Act + var document = "@model Foo"; + var spans = ParseDocument(document); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code(" Foo") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")) + }; + Assert.Equal(expectedSpans, spans.ToArray()); + } + + [Fact] + public void ParseModelKeyword_HandlesNullableTypes() + { + // Arrange + Act + var document = "@model Foo?\r\nBar"; + var spans = ParseDocument(document); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo?\r\n") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")), + factory.Markup("Bar") + .With(new MarkupCodeGenerator()) + }; + Assert.Equal(expectedSpans, spans.ToArray()); + } + + [Fact] + public void ParseModelKeyword_HandlesArrays() + { + // Arrange + Act + var document = "@model Foo[[]][]\r\nBar"; + var spans = ParseDocument(document); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo[[]][]\r\n") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")), + factory.Markup("Bar") + .With(new MarkupCodeGenerator()) + }; + Assert.Equal(expectedSpans, spans.ToArray()); + } + + [Fact] + public void ParseModelKeyword_HandlesVSTemplateSyntax() + { + // Arrange + Act + var document = "@model $rootnamespace$.MyModel"; + var spans = ParseDocument(document); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("$rootnamespace$.MyModel") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "<$rootnamespace$.MyModel>")) + }; + Assert.Equal(expectedSpans, spans.ToArray()); + } + + [Fact] + public void ParseModelKeyword_ErrorOnMissingModelType() + { + // Arrange + Act + List errors = new List(); + var document = "@model "; + var spans = ParseDocument(document, errors); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code(" ") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType)), + }; + var expectedErrors = new[] + { + new RazorError("The 'model' keyword must be followed by a type name on the same line.", new SourceLocation(9, 0, 9), 1) + }; + Assert.Equal(expectedSpans, spans.ToArray()); + Assert.Equal(expectedErrors, errors.ToArray()); + } + + [Fact] + public void ParseModelKeyword_ErrorOnMultipleModelStatements() + { + // Arrange + Act + List errors = new List(); + var document = + "@model Foo" + Environment.NewLine + + "@model Bar"; + var spans = ParseDocument(document, errors); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo\r\n") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Bar") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")) + }; + + var expectedErrors = new[] + { + new RazorError("Only one 'model' statement is allowed in a file.", new SourceLocation(18, 1, 6), 1) + }; + expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span)); + Assert.Equal(expectedSpans, spans.ToArray()); + Assert.Equal(expectedErrors, errors.ToArray()); + } + + [Fact] + public void ParseModelKeyword_ErrorOnModelFollowedByInherits() + { + // Arrange + Act + List errors = new List(); + var document = + "@model Foo" + Environment.NewLine + + "@inherits Bar"; + var spans = ParseDocument(document, errors); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo\r\n") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inherits ") + .Accepts(AcceptedCharacters.None), + factory.Code("Bar") + .As(new SetBaseTypeCodeGenerator("Bar")) + }; + + var expectedErrors = new[] + { + new RazorError("The 'inherits' keyword is not allowed when a 'model' keyword is used.", new SourceLocation(21, 1, 9), 1) + }; + expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span)); + Assert.Equal(expectedSpans, spans.ToArray()); + Assert.Equal(expectedErrors, errors.ToArray()); + } + + [Fact] + public void ParseModelKeyword_ErrorOnInheritsFollowedByModel() + { + // Arrange + Act + List errors = new List(); + var document = + "@inherits Bar" + Environment.NewLine + + "@model Foo"; + var spans = ParseDocument(document, errors); + + // Assert + var factory = SpanFactory.CreateCsHtml(); + var expectedSpans = new Span[] + { + factory.EmptyHtml(), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("inherits ") + .Accepts(AcceptedCharacters.None), + factory.Code("Bar" + Environment.NewLine) + .As(new SetBaseTypeCodeGenerator("Bar")), + factory.CodeTransition(SyntaxConstants.TransitionString) + .Accepts(AcceptedCharacters.None), + factory.MetaCode("model ") + .Accepts(AcceptedCharacters.None), + factory.Code("Foo") + .As(new SetBaseTypeCodeGenerator(DefaultBaseType + "")) + }; + + var expectedErrors = new[] + { + new RazorError("The 'inherits' keyword is not allowed when a 'model' keyword is used.", new SourceLocation(9, 0, 9), 1) + }; + expectedSpans.Zip(spans, (exp, span) => new { expected = exp, span = span }).ToList().ForEach(i => Assert.Equal(i.expected, i.span)); + Assert.Equal(expectedSpans, spans.ToArray()); + Assert.Equal(expectedErrors, errors.ToArray()); + } + + private static List ParseDocument(string documentContents, IList errors = null) + { + errors = errors ?? new List(); + var markupParser = new HtmlMarkupParser(); + var codeParser = new TestMvcCSharpRazorCodeParser(); + var context = new ParserContext(new SeekableTextReader(documentContents), codeParser, markupParser, markupParser); + codeParser.Context = context; + markupParser.Context = context; + markupParser.ParseDocument(); + + ParserResults results = context.CompleteParse(); + foreach (RazorError error in results.ParserErrors) + { + errors.Add(error); + } + return results.Document.Flatten().ToList(); + } + + private sealed class TestMvcCSharpRazorCodeParser : MvcRazorCodeParser + { + public TestMvcCSharpRazorCodeParser(string baseType = DefaultBaseType) + : base(baseType) + { + + } + + public bool HasDirective(string directive) + { + Action handler; + return TryGetDirectiveHandler(directive, out handler); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory.cs new file mode 100644 index 0000000000..c4470de3fa --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/SpanFactory.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Mvc.Razor.Host.Test +{ + public static class SpanFactoryExtensions + { + public static SpanConstructor EmptyHtml(this SpanFactory self) + { + return self.Span(SpanKind.Markup, new HtmlSymbol(self.LocationTracker.CurrentLocation, String.Empty, HtmlSymbolType.Unknown)) + .With(new MarkupCodeGenerator()); + } + + public static UnclassifiedCodeSpanConstructor Code(this SpanFactory self, string content) + { + return new UnclassifiedCodeSpanConstructor( + self.Span(SpanKind.Code, content, markup: false)); + } + + public static SpanConstructor CodeTransition(this SpanFactory self, string content) + { + return self.Span(SpanKind.Transition, content, markup: false).Accepts(AcceptedCharacters.None); + } + + public static SpanConstructor MetaCode(this SpanFactory self, string content) + { + return self.Span(SpanKind.MetaCode, content, markup: false); + } + public static SpanConstructor Markup(this SpanFactory self, string content) + { + return self.Span(SpanKind.Markup, content, markup: true).With(new MarkupCodeGenerator()); + } + } + + public class SpanFactory + { + public Func MarkupTokenizerFactory { get; set; } + public Func CodeTokenizerFactory { get; set; } + public SourceLocationTracker LocationTracker { get; private set; } + + public static SpanFactory CreateCsHtml() + { + return new SpanFactory() + { + MarkupTokenizerFactory = doc => new HtmlTokenizer(doc), + CodeTokenizerFactory = doc => new CSharpTokenizer(doc) + }; + } + + public SpanFactory() + { + LocationTracker = new SourceLocationTracker(); + } + + + public SpanConstructor Span(SpanKind kind, string content, bool markup) + { + return new SpanConstructor(kind, Tokenize(new[] { content }, markup)); + } + + public SpanConstructor Span(SpanKind kind, params ISymbol[] symbols) + { + return new SpanConstructor(kind, symbols); + } + + private IEnumerable Tokenize(IEnumerable contentFragments, bool markup) + { + return contentFragments.SelectMany(fragment => Tokenize(fragment, markup)); + } + + private IEnumerable Tokenize(string content, bool markup) + { + ITokenizer tok = MakeTokenizer(markup, new SeekableTextReader(content)); + ISymbol sym; + ISymbol last = null; + while ((sym = tok.NextSymbol()) != null) + { + OffsetStart(sym, LocationTracker.CurrentLocation); + last = sym; + yield return sym; + } + LocationTracker.UpdateLocation(content); + } + + private ITokenizer MakeTokenizer(bool markup, SeekableTextReader seekableTextReader) + { + if (markup) + { + return MarkupTokenizerFactory(seekableTextReader); + } + else + { + return CodeTokenizerFactory(seekableTextReader); + } + } + + private void OffsetStart(ISymbol sym, SourceLocation sourceLocation) + { + sym.OffsetStart(sourceLocation); + } + } + + public static class SpanConstructorExtensions + { + public static SpanConstructor Accepts(this SpanConstructor self, AcceptedCharacters accepted) + { + return self.With(eh => eh.AcceptedCharacters = accepted); + } + } + + public class UnclassifiedCodeSpanConstructor + { + SpanConstructor _self; + + public UnclassifiedCodeSpanConstructor(SpanConstructor self) + { + _self = self; + } + + public SpanConstructor As(ISpanCodeGenerator codeGenerator) + { + return _self.With(codeGenerator); + } + } + + public class SpanConstructor + { + public SpanBuilder Builder { get; private set; } + + internal static IEnumerable TestTokenizer(string str) + { + yield return new RawTextSymbol(SourceLocation.Zero, str); + } + + public SpanConstructor(SpanKind kind, IEnumerable symbols) + { + Builder = new SpanBuilder(); + Builder.Kind = kind; + Builder.EditHandler = SpanEditHandler.CreateDefault(TestTokenizer); + foreach (ISymbol sym in symbols) + { + Builder.Accept(sym); + } + } + + private Span Build() + { + return Builder.Build(); + } + + public SpanConstructor With(ISpanCodeGenerator generator) + { + Builder.CodeGenerator = generator; + return this; + } + + public SpanConstructor With(SpanEditHandler handler) + { + Builder.EditHandler = handler; + return this; + } + + public SpanConstructor With(Action handlerConfigurer) + { + handlerConfigurer(Builder.EditHandler); + return this; + } + + public static implicit operator Span(SpanConstructor self) + { + return self.Build(); + } + } + + internal class RawTextSymbol : ISymbol + { + public SourceLocation Start { get; private set; } + public string Content { get; private set; } + + public RawTextSymbol(SourceLocation start, string content) + { + if (content == null) + { + throw new ArgumentNullException("content"); + } + + Start = start; + Content = content; + } + + public override bool Equals(object obj) + { + RawTextSymbol other = obj as RawTextSymbol; + return Equals(Start, other.Start) && Equals(Content, other.Content); + } + + internal bool EquivalentTo(ISymbol sym) + { + return Equals(Start, sym.Start) && Equals(Content, sym.Content); + } + + public override int GetHashCode() + { + return Start.GetHashCode(); + } + + public void OffsetStart(SourceLocation documentStart) + { + Start = documentStart + Start; + } + + public void ChangeStart(SourceLocation newStart) + { + Start = newStart; + } + + public override string ToString() + { + return String.Format(CultureInfo.InvariantCulture, "{0} RAW - [{1}]", Start, Content); + } + + internal void CalculateStart(Span prev) + { + if (prev == null) + { + Start = SourceLocation.Zero; + } + else + { + Start = new SourceLocationTracker(prev.Start).UpdateLocation(prev.Content).CurrentLocation; + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json new file mode 100644 index 0000000000..131195eca6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/project.json @@ -0,0 +1,13 @@ +{ + "version" : "0.1-alpha-*", + "dependencies": { + "Microsoft.AspNet.Razor" : "0.1-alpha-*", + "Microsoft.AspNet.Mvc.Razor.Host" : "", + "Moq": "4.0.10827", + "Xunit": "1.9.1", + "Xunit.extensions": "1.9.1" + }, + "configurations": { + "net45": { } + } +} \ No newline at end of file