Restore @model support in RazorHost

This commit is contained in:
Pranav K 2014-02-13 07:03:30 -08:00
parent 4a801b6c7a
commit 5a22d9b52c
12 changed files with 762 additions and 47 deletions

View File

@ -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

View File

@ -1,4 +1,5 @@
@using MvcSample.Models
@model User
@{
Layout = "/Views/Shared/_Layout.cshtml";
ViewBag.Title = "Home Page";
@ -9,11 +10,8 @@
<p class="lead">ASP.NET is a free web framework for building great Web sites and Web applications using HTML, CSS and JavaScript.</p>
<p><a href="http://asp.net" class="btn btn-primary btn-large">Learn more &raquo;</a></p>
</div>
@{
var user = new User { Name = "Test user" };
}
<div class="row">
<h3>Hello @user.Name!</h3>
<h3>Hello @Model.Name!</h3>
<div class="col-md-4">
<h2>Getting started</h2>
<p>

View File

@ -26,6 +26,9 @@
<div class="container body-content">
@RenderBody()
<hr />
<address>
@Model.Address
</address>
<footer>
<p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
</footer>

View File

@ -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);
}
}
}

View File

@ -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<dynamic>.
// 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 + "<dynamic>";
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);

View File

@ -0,0 +1,46 @@
// <auto-generated />
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);
/// <summary>
/// The 'inherits' keyword is not allowed when a '{0}' keyword is used.
/// </summary>
internal static string MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword"), p0);
}
/// <summary>
/// The '{0}' keyword must be followed by a type name on the same line.
/// </summary>
internal static string MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName"), p0);
}
/// <summary>
/// Only one '{0}' statement is allowed in a file.
/// </summary>
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;
}
}
}

View File

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="MvcRazorCodeParser_CannotHaveModelAndInheritsKeyword" xml:space="preserve">
<value>The 'inherits' keyword is not allowed when a '{0}' keyword is used.</value>
</data>
<data name="MvcRazorCodeParser_ModelKeywordMustBeFollowedByTypeName" xml:space="preserve">
<value>The '{0}' keyword must be followed by a type name on the same line.</value>
</data>
<data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
<value>Only one '{0}' statement is allowed in a file.</value>
</data>
</root>

View File

@ -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;
}
}
}

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Mvc.Startup
AddInstance<IControllerDescriptorProvider>(provider);
AddInstance<IFileSystem>(new PhysicalFileSystem(appRoot));
AddInstance<IMvcRazorHost>(new MvcRazorHost("Microsoft.AspNet.Mvc.Razor.RazorView<dynamic>"));
AddInstance<IMvcRazorHost>(new MvcRazorHost(typeof(RazorView).FullName));
#if NET45
Add<ICompilationService, CscBasedCompilationService>();

View File

@ -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 + "<Foo>"))
};
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 + "<Foo?>")),
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 + "<Foo[[]][]>")),
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<RazorError> errors = new List<RazorError>();
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<RazorError> errors = new List<RazorError>();
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 + "<Foo>")),
factory.CodeTransition(SyntaxConstants.TransitionString)
.Accepts(AcceptedCharacters.None),
factory.MetaCode("model ")
.Accepts(AcceptedCharacters.None),
factory.Code("Bar")
.As(new SetBaseTypeCodeGenerator(DefaultBaseType + "<Bar>"))
};
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<RazorError> errors = new List<RazorError>();
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 + "<Foo>")),
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<RazorError> errors = new List<RazorError>();
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 + "<Foo>"))
};
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<Span> ParseDocument(string documentContents, IList<RazorError> errors = null)
{
errors = errors ?? new List<RazorError>();
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);
}
}
}
}

View File

@ -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<ITextDocument, ITokenizer> MarkupTokenizerFactory { get; set; }
public Func<ITextDocument, ITokenizer> 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<ISymbol> Tokenize(IEnumerable<string> contentFragments, bool markup)
{
return contentFragments.SelectMany(fragment => Tokenize(fragment, markup));
}
private IEnumerable<ISymbol> 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<ISymbol> TestTokenizer(string str)
{
yield return new RawTextSymbol(SourceLocation.Zero, str);
}
public SpanConstructor(SpanKind kind, IEnumerable<ISymbol> 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<SpanEditHandler> 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;
}
}
}
}

View File

@ -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": { }
}
}