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