diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 6cbde058da..ceb3079a04 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -5,7 +5,9 @@ ], "packages": { "Microsoft.AspNetCore.Razor": { }, - "Microsoft.AspNetCore.Razor.Runtime": { } + "Microsoft.AspNet.Razor.VSRC1": { }, + "Microsoft.AspNetCore.Razor.Runtime": { }, + "Microsoft.AspNet.Razor.Runtime.VSRC1": { } } }, "adx-nonshipping": { // Packages written by the ADX team but that don't ship on NuGet.org (thus, no need to scan anything in them) diff --git a/Razor.sln b/Razor.sln index 9edc90c924..5084517467 100644 --- a/Razor.sln +++ b/Razor.sln @@ -1,7 +1,6 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3C0D6505-79B3-49D0-B4C3-176F0F1836ED}" EndProject @@ -17,6 +16,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Razor. EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Razor.Test.Sources", "src\Microsoft.AspNetCore.Razor.Test.Sources\Microsoft.AspNetCore.Razor.Test.Sources.xproj", "{E3A2A305-634D-4BA6-95DB-AA06D6C442B0}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Razor.Runtime.VSRC1", "src\Microsoft.AspNet.Razor.Runtime.VSRC1\Microsoft.AspNet.Razor.Runtime.VSRC1.xproj", "{5E8EC8BB-69B9-43D4-A095-7A06F65838E2}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Razor.VSRC1", "src\Microsoft.AspNet.Razor.VSRC1\Microsoft.AspNet.Razor.VSRC1.xproj", "{AB5ABC37-201B-41FF-9FAF-E948B0D33F5A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +46,14 @@ Global {E3A2A305-634D-4BA6-95DB-AA06D6C442B0}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3A2A305-634D-4BA6-95DB-AA06D6C442B0}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3A2A305-634D-4BA6-95DB-AA06D6C442B0}.Release|Any CPU.Build.0 = Release|Any CPU + {5E8EC8BB-69B9-43D4-A095-7A06F65838E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E8EC8BB-69B9-43D4-A095-7A06F65838E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E8EC8BB-69B9-43D4-A095-7A06F65838E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E8EC8BB-69B9-43D4-A095-7A06F65838E2}.Release|Any CPU.Build.0 = Release|Any CPU + {AB5ABC37-201B-41FF-9FAF-E948B0D33F5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB5ABC37-201B-41FF-9FAF-E948B0D33F5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB5ABC37-201B-41FF-9FAF-E948B0D33F5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB5ABC37-201B-41FF-9FAF-E948B0D33F5A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -53,5 +64,7 @@ Global {D0196096-1B01-4133-AACE-1A10A0F7247C} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} {0535998A-E32C-4D1A-80D1-0B15A513C471} = {92463391-81BE-462B-AC3C-78C6C760741F} {E3A2A305-634D-4BA6-95DB-AA06D6C442B0} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} + {5E8EC8BB-69B9-43D4-A095-7A06F65838E2} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} + {AB5ABC37-201B-41FF-9FAF-E948B0D33F5A} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED} EndGlobalSection EndGlobal diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/BufferedHtmlContent.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/BufferedHtmlContent.cs new file mode 100644 index 0000000000..9d098518b8 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/BufferedHtmlContent.cs @@ -0,0 +1,125 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using Microsoft.AspNet.Html.Abstractions; +using Microsoft.Extensions.WebEncoders; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Enumerable object collection which knows how to write itself. + /// + [DebuggerDisplay("{DebuggerToString()}")] + internal class BufferedHtmlContent : IHtmlContentBuilder + { + // This is not List because that would lead to wrapping all strings to IHtmlContent + // which is not space performant. + // internal for testing. + internal List Entries { get; } = new List(); + + /// + /// Appends the to the collection. + /// + /// The string to be appended. + /// A reference to this instance after the Append operation has completed. + public IHtmlContentBuilder Append(string unencoded) + { + Entries.Add(unencoded); + return this; + } + + /// + /// Appends a to the collection. + /// + /// The to be appended. + /// A reference to this instance after the Append operation has completed. + public IHtmlContentBuilder Append(IHtmlContent htmlContent) + { + Entries.Add(htmlContent); + return this; + } + + /// + /// Appends the HTML encoded to the collection. + /// + /// The HTML encoded string to be appended. + /// A reference to this instance after the Append operation has completed. + public IHtmlContentBuilder AppendHtml(string encoded) + { + Entries.Add(new HtmlEncodedString(encoded)); + return this; + } + /// + /// Removes all the entries from the collection. + /// + /// A reference to this instance after the Clear operation has completed. + public IHtmlContentBuilder Clear() + { + Entries.Clear(); + return this; + } + + /// + public void WriteTo(TextWriter writer, IHtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + foreach (var entry in Entries) + { + if (entry == null) + { + continue; + } + + var entryAsString = entry as string; + if (entryAsString != null) + { + encoder.HtmlEncode(entryAsString, writer); + } + else + { + // Only string, IHtmlContent values can be added to the buffer. + ((IHtmlContent)entry).WriteTo(writer, encoder); + } + } + } + + private string DebuggerToString() + { + using (var writer = new StringWriter()) + { + WriteTo(writer, HtmlEncoder.Default); + return writer.ToString(); + } + } + + private class HtmlEncodedString : IHtmlContent + { + public static readonly IHtmlContent NewLine = new HtmlEncodedString(Environment.NewLine); + + private readonly string _value; + + public HtmlEncodedString(string value) + { + _value = value; + } + + public void WriteTo(TextWriter writer, IHtmlEncoder encoder) + { + writer.Write(_value); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/ClosedGenericMatcher.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/ClosedGenericMatcher.cs new file mode 100644 index 0000000000..490196d915 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/ClosedGenericMatcher.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Reflection; + +namespace Microsoft.Extensions.Internal +{ + /// + /// Helper related to generic interface definitions and implementing classes. + /// + internal static class ClosedGenericMatcher + { + /// + /// Determine whether is or implements a closed generic + /// created from . + /// + /// The of interest. + /// The open generic to match. Usually an interface. + /// + /// The closed generic created from that + /// is or implements. null if the two s have no such + /// relationship. + /// + /// + /// This method will return if is + /// typeof(KeyValuePair{,}), and is + /// typeof(KeyValuePair{string, object}). + /// + public static Type ExtractGenericInterface(Type queryType, Type interfaceType) + { + if (queryType == null) + { + throw new ArgumentNullException(nameof(queryType)); + } + + if (interfaceType == null) + { + throw new ArgumentNullException(nameof(interfaceType)); + } + + Func matchesInterface = + type => type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == interfaceType; + if (matchesInterface(queryType)) + { + // Checked type matches (i.e. is a closed generic type created from) the open generic type. + return queryType; + } + + // Otherwise check all interfaces the type implements for a match. + return queryType.GetTypeInfo().ImplementedInterfaces.FirstOrDefault(matchesInterface); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/CopyOnWriteDictionary.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/CopyOnWriteDictionary.cs new file mode 100644 index 0000000000..1408059ad9 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/CopyOnWriteDictionary.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Internal +{ + internal class CopyOnWriteDictionary : IDictionary + { + private readonly IDictionary _sourceDictionary; + private readonly IEqualityComparer _comparer; + private IDictionary _innerDictionary; + + public CopyOnWriteDictionary( + IDictionary sourceDictionary, + IEqualityComparer comparer) + { + if (sourceDictionary == null) + { + throw new ArgumentNullException(nameof(sourceDictionary)); + } + + if (comparer == null) + { + throw new ArgumentNullException(nameof(comparer)); + } + + _sourceDictionary = sourceDictionary; + _comparer = comparer; + } + + private IDictionary ReadDictionary + { + get + { + return _innerDictionary ?? _sourceDictionary; + } + } + + private IDictionary WriteDictionary + { + get + { + if (_innerDictionary == null) + { + _innerDictionary = new Dictionary(_sourceDictionary, + _comparer); + } + + return _innerDictionary; + } + } + + public virtual ICollection Keys + { + get + { + return ReadDictionary.Keys; + } + } + + public virtual ICollection Values + { + get + { + return ReadDictionary.Values; + } + } + + public virtual int Count + { + get + { + return ReadDictionary.Count; + } + } + + public virtual bool IsReadOnly + { + get + { + return false; + } + } + + public virtual TValue this[TKey key] + { + get + { + return ReadDictionary[key]; + } + set + { + WriteDictionary[key] = value; + } + } + + public virtual bool ContainsKey(TKey key) + { + return ReadDictionary.ContainsKey(key); + } + + public virtual void Add(TKey key, TValue value) + { + WriteDictionary.Add(key, value); + } + + public virtual bool Remove(TKey key) + { + return WriteDictionary.Remove(key); + } + + public virtual bool TryGetValue(TKey key, out TValue value) + { + return ReadDictionary.TryGetValue(key, out value); + } + + public virtual void Add(KeyValuePair item) + { + WriteDictionary.Add(item); + } + + public virtual void Clear() + { + WriteDictionary.Clear(); + } + + public virtual bool Contains(KeyValuePair item) + { + return ReadDictionary.Contains(item); + } + + public virtual void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ReadDictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return WriteDictionary.Remove(item); + } + + public virtual IEnumerator> GetEnumerator() + { + return ReadDictionary.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/CopyOnWriteDictionaryHolder.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/CopyOnWriteDictionaryHolder.cs new file mode 100644 index 0000000000..7cd935e940 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/CopyOnWriteDictionaryHolder.cs @@ -0,0 +1,166 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Internal +{ + internal struct CopyOnWriteDictionaryHolder + { + private readonly Dictionary _source; + private Dictionary _copy; + + public CopyOnWriteDictionaryHolder(Dictionary source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + _source = source; + _copy = null; + } + + public CopyOnWriteDictionaryHolder(CopyOnWriteDictionaryHolder source) + { + _source = source._copy ?? source._source; + _copy = null; + } + + public bool HasBeenCopied => _copy != null; + + public Dictionary ReadDictionary + { + get + { + if (_copy != null) + { + return _copy; + } + else if (_source != null) + { + return _source; + } + else + { + // Default-Constructor case + _copy = new Dictionary(); + return _copy; + } + } + } + + public Dictionary WriteDictionary + { + get + { + if (_copy == null && _source == null) + { + // Default-Constructor case + _copy = new Dictionary(); + } + else if (_copy == null) + { + _copy = new Dictionary(_source, _source.Comparer); + } + + return _copy; + } + } + + public Dictionary.KeyCollection Keys + { + get + { + return ReadDictionary.Keys; + } + } + + public Dictionary.ValueCollection Values + { + get + { + return ReadDictionary.Values; + } + } + + public int Count + { + get + { + return ReadDictionary.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public TValue this[TKey key] + { + get + { + return ReadDictionary[key]; + } + set + { + WriteDictionary[key] = value; + } + } + + public bool ContainsKey(TKey key) + { + return ReadDictionary.ContainsKey(key); + } + + public void Add(TKey key, TValue value) + { + WriteDictionary.Add(key, value); + } + + public bool Remove(TKey key) + { + return WriteDictionary.Remove(key); + } + + public bool TryGetValue(TKey key, out TValue value) + { + return ReadDictionary.TryGetValue(key, out value); + } + + public void Add(KeyValuePair item) + { + ((ICollection>)WriteDictionary).Add(item); + } + + public void Clear() + { + WriteDictionary.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((ICollection>)ReadDictionary).Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)ReadDictionary).CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return ((ICollection>)WriteDictionary).Remove(item); + } + + public Dictionary.Enumerator GetEnumerator() + { + return ReadDictionary.GetEnumerator(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Microsoft.AspNet.Razor.Runtime.VSRC1.xproj b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Microsoft.AspNet.Razor.Runtime.VSRC1.xproj new file mode 100644 index 0000000000..d178f985f4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Microsoft.AspNet.Razor.Runtime.VSRC1.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 5e8ec8bb-69b9-43d4-a095-7a06f65838e2 + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1bbf723e7d --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..5dfc9ee0e4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Properties/Resources.Designer.cs @@ -0,0 +1,478 @@ +// +namespace Microsoft.AspNet.Razor.Runtime +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNet.Razor.Runtime.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Invalid tag helper directive look up text '{0}'. The correct look up text format is: "typeName, assemblyName". + /// + internal static string TagHelperDescriptorResolver_InvalidTagHelperLookupText + { + get { return GetString("TagHelperDescriptorResolver_InvalidTagHelperLookupText"); } + } + + /// + /// Invalid tag helper directive look up text '{0}'. The correct look up text format is: "typeName, assemblyName". + /// + internal static string FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperLookupText"), p0); + } + + /// + /// Cannot resolve TagHelper containing assembly '{0}'. Error: {1} + /// + internal static string TagHelperTypeResolver_CannotResolveTagHelperAssembly + { + get { return GetString("TagHelperTypeResolver_CannotResolveTagHelperAssembly"); } + } + + /// + /// Cannot resolve TagHelper containing assembly '{0}'. Error: {1} + /// + internal static string FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperTypeResolver_CannotResolveTagHelperAssembly"), p0, p1); + } + + /// + /// Tag helper directive assembly name cannot be null or empty. + /// + internal static string TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull + { + get { return GetString("TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull"); } + } + + /// + /// Tag helper directive assembly name cannot be null or empty. + /// + internal static string FormatTagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull() + { + return GetString("TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull"); + } + + /// + /// Must call '{2}.{1}' before calling '{2}.{0}'. + /// + internal static string ScopeManager_EndCannotBeCalledWithoutACallToBegin + { + get { return GetString("ScopeManager_EndCannotBeCalledWithoutACallToBegin"); } + } + + /// + /// Must call '{2}.{1}' before calling '{2}.{0}'. + /// + internal static string FormatScopeManager_EndCannotBeCalledWithoutACallToBegin(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ScopeManager_EndCannotBeCalledWithoutACallToBegin"), p0, p1, p2); + } + + /// + /// {0} name cannot be null or whitespace. + /// + internal static string HtmlTargetElementAttribute_NameCannotBeNullOrWhitespace + { + get { return GetString("HtmlTargetElementAttribute_NameCannotBeNullOrWhitespace"); } + } + + /// + /// {0} name cannot be null or whitespace. + /// + internal static string FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlTargetElementAttribute_NameCannotBeNullOrWhitespace"), p0); + } + + /// + /// The value cannot be null or empty. + /// + internal static string ArgumentCannotBeNullOrEmpty + { + get { return GetString("ArgumentCannotBeNullOrEmpty"); } + } + + /// + /// The value cannot be null or empty. + /// + internal static string FormatArgumentCannotBeNullOrEmpty() + { + return GetString("ArgumentCannotBeNullOrEmpty"); + } + + /// + /// Encountered an unexpected error when attempting to resolve tag helper directive '{0}' with value '{1}'. Error: {2} + /// + internal static string TagHelperDescriptorResolver_EncounteredUnexpectedError + { + get { return GetString("TagHelperDescriptorResolver_EncounteredUnexpectedError"); } + } + + /// + /// Encountered an unexpected error when attempting to resolve tag helper directive '{0}' with value '{1}'. Error: {2} + /// + internal static string FormatTagHelperDescriptorResolver_EncounteredUnexpectedError(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_EncounteredUnexpectedError"), p0, p1, p2); + } + + /// + /// Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character. + /// + internal static string HtmlTargetElementAttribute_InvalidName + { + get { return GetString("HtmlTargetElementAttribute_InvalidName"); } + } + + /// + /// Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character. + /// + internal static string FormatHtmlTargetElementAttribute_InvalidName(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("HtmlTargetElementAttribute_InvalidName"), p0, p1, p2); + } + + /// + /// Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page. + /// + internal static string TagHelperDescriptorResolver_InvalidTagHelperDirective + { + get { return GetString("TagHelperDescriptorResolver_InvalidTagHelperDirective"); } + } + + /// + /// Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page. + /// + internal static string FormatTagHelperDescriptorResolver_InvalidTagHelperDirective(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperDirective"), p0); + } + + /// + /// Invalid tag helper directive '{0}' value. '{1}' is not allowed in prefix '{2}'. + /// + internal static string TagHelperDescriptorResolver_InvalidTagHelperPrefixValue + { + get { return GetString("TagHelperDescriptorResolver_InvalidTagHelperPrefixValue"); } + } + + /// + /// Invalid tag helper directive '{0}' value. '{1}' is not allowed in prefix '{2}'. + /// + internal static string FormatTagHelperDescriptorResolver_InvalidTagHelperPrefixValue(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperPrefixValue"), p0, p1, p2); + } + + /// + /// Attribute + /// + internal static string TagHelperDescriptorFactory_Attribute + { + get { return GetString("TagHelperDescriptorFactory_Attribute"); } + } + + /// + /// Attribute + /// + internal static string FormatTagHelperDescriptorFactory_Attribute() + { + return GetString("TagHelperDescriptorFactory_Attribute"); + } + + /// + /// name + /// + internal static string TagHelperDescriptorFactory_Name + { + get { return GetString("TagHelperDescriptorFactory_Name"); } + } + + /// + /// name + /// + internal static string FormatTagHelperDescriptorFactory_Name() + { + return GetString("TagHelperDescriptorFactory_Name"); + } + + /// + /// prefix + /// + internal static string TagHelperDescriptorFactory_Prefix + { + get { return GetString("TagHelperDescriptorFactory_Prefix"); } + } + + /// + /// prefix + /// + internal static string FormatTagHelperDescriptorFactory_Prefix() + { + return GetString("TagHelperDescriptorFactory_Prefix"); + } + + /// + /// Tag + /// + internal static string TagHelperDescriptorFactory_Tag + { + get { return GetString("TagHelperDescriptorFactory_Tag"); } + } + + /// + /// Tag + /// + internal static string FormatTagHelperDescriptorFactory_Tag() + { + return GetString("TagHelperDescriptorFactory_Tag"); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameAttribute + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameAttribute"), p0, p1, p2, p3); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace"), p0, p1, p2); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty"), p0, p1); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty"), p0, p1, p2, p3); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNull + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNull"), p0, p1, p2, p3, p4); + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + internal static string TagHelperDescriptorFactory_InvalidAttributePrefixNotNull + { + get { return GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"); } + } + + /// + /// Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull(object p0, object p1, object p2, object p3, object p4) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidAttributePrefixNotNull"), p0, p1, p2, p3, p4); + } + + /// + /// Cannot add a '{0}' with a null '{1}'. + /// + internal static string TagHelperAttributeList_CannotAddWithNullName + { + get { return GetString("TagHelperAttributeList_CannotAddWithNullName"); } + } + + /// + /// Cannot add a '{0}' with a null '{1}'. + /// + internal static string FormatTagHelperAttributeList_CannotAddWithNullName(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperAttributeList_CannotAddWithNullName"), p0, p1); + } + + /// + /// Cannot add a {0} with inconsistent names. The {1} property '{2}' must match the location '{3}'. + /// + internal static string TagHelperAttributeList_CannotAddAttribute + { + get { return GetString("TagHelperAttributeList_CannotAddAttribute"); } + } + + /// + /// Cannot add a {0} with inconsistent names. The {1} property '{2}' must match the location '{3}'. + /// + internal static string FormatTagHelperAttributeList_CannotAddAttribute(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperAttributeList_CannotAddAttribute"), p0, p1, p2, p3); + } + + /// + /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + /// + internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName + { + get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"); } + } + + /// + /// Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName(object p0, object p1, object p2, object p3) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName"), p0, p1, p2, p3); + } + + /// + /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + /// + internal static string TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace + { + get { return GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"); } + } + + /// + /// Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + /// + internal static string FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace"), p0, p1); + } + + /// + /// Parent Tag + /// + internal static string TagHelperDescriptorFactory_ParentTag + { + get { return GetString("TagHelperDescriptorFactory_ParentTag"); } + } + + /// + /// Parent Tag + /// + internal static string FormatTagHelperDescriptorFactory_ParentTag() + { + return GetString("TagHelperDescriptorFactory_ParentTag"); + } + + /// + /// Argument must be an instance of '{0}'. + /// + internal static string ArgumentMustBeAnInstanceOf + { + get { return GetString("ArgumentMustBeAnInstanceOf"); } + } + + /// + /// Argument must be an instance of '{0}'. + /// + internal static string FormatArgumentMustBeAnInstanceOf(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ArgumentMustBeAnInstanceOf"), p0); + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Resources.resx new file mode 100644 index 0000000000..cfc6719220 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Resources.resx @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Invalid tag helper directive look up text '{0}'. The correct look up text format is: "typeName, assemblyName". + + + Cannot resolve TagHelper containing assembly '{0}'. Error: {1} + + + Tag helper directive assembly name cannot be null or empty. + + + Must call '{2}.{1}' before calling '{2}.{0}'. + + + {0} name cannot be null or whitespace. + + + The value cannot be null or empty. + + + Encountered an unexpected error when attempting to resolve tag helper directive '{0}' with value '{1}'. Error: {2} + + + Tag helpers cannot target {0} name '{1}' because it contains a '{2}' character. + + + Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page. + + + Invalid tag helper directive '{0}' value. '{1}' is not allowed in prefix '{2}'. + + + Attribute + + + name + + + prefix + + + Tag + + + Invalid tag helper bound property '{0}.{1}'. An '{2}' must not be associated with a property with no public setter unless its type implements '{3}'. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} contains a '{4}' character. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with {2} '{3}' because {2} starts with '{4}'. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a whitespace {2}. + + + Invalid tag helper bound property '{0}.{1}'. Tag helpers cannot bind to HTML attributes with a null or empty name. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null or empty if property has no public setter. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must not be null if property has no public setter and its type implements '{4}'. + + + Invalid tag helper bound property '{0}.{1}'. '{2}.{3}' must be null unless property type implements '{4}'. + + + Cannot add a '{0}' with a null '{1}'. + + + Cannot add a {0} with inconsistent names. The {1} property '{2}' must match the location '{3}'. + + + Invalid '{0}' tag name '{1}' for tag helper '{2}'. Tag helpers cannot restrict child elements that contain a '{3}' character. + + + Invalid '{0}' tag name for tag helper '{1}'. Name cannot be null or whitespace. + + + Parent Tag + + + Argument must be an instance of '{0}'. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/Constants.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/Constants.cs new file mode 100644 index 0000000000..34710fe909 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/Constants.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + internal static class Constants + { + public static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/IMemberInfo.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/IMemberInfo.cs new file mode 100644 index 0000000000..6d569b0df1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/IMemberInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Metadata common to types and properties. + /// + public interface IMemberInfo + { + /// + /// Gets the name. + /// + string Name { get; } + + /// + /// Retrieves a collection of custom s of type applied + /// to this instance of . + /// + /// The type of to search for. + /// A sequence of custom s of type + /// . + /// Result not include inherited s. + IEnumerable GetCustomAttributes() + where TAttribute : Attribute; + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/IPropertyInfo.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/IPropertyInfo.cs new file mode 100644 index 0000000000..beb0840b57 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/IPropertyInfo.cs @@ -0,0 +1,26 @@ +// 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.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Contains property metadata. + /// + public interface IPropertyInfo : IMemberInfo + { + /// + /// Gets a value indicating whether this property has a public getter. + /// + bool HasPublicGetter { get; } + + /// + /// Gets a value indicating whether this property has a public setter. + /// + bool HasPublicSetter { get; } + + /// + /// Gets the of the property. + /// + ITypeInfo PropertyType { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/ITypeInfo.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/ITypeInfo.cs new file mode 100644 index 0000000000..00b670925f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/ITypeInfo.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Contains type metadata. + /// + public interface ITypeInfo : IMemberInfo, IEquatable + { + /// + /// Fully qualified name of the type. + /// + /// + /// On CoreCLR, some BCL types get type forwarded to the full desktop framework implementations at + /// runtime. For e.g. we compile against System.String in System.Runtime which is type forwarded to + /// mscorlib at runtime. Consequently for generic types where the includes the assembly + /// qualified name of generic parameters, FullNames would not match. + /// Use to compare s instead. + /// + string FullName { get; } + + /// + /// Gets s for all properties of the current type excluding indexers. + /// + /// + /// Indexers in this context refer to the CLR notion of an indexer (this [string name] + /// and does not overlap with the semantics of + /// . + /// + IEnumerable Properties { get; } + + /// + /// Gets a value indicating whether the type is public. + /// + bool IsPublic { get; } + + /// + /// Gets a value indicating whether the type is abstract or an interface. + /// + bool IsAbstract { get; } + + /// + /// Gets a value indicating whether the type is generic. + /// + bool IsGenericType { get; } + + /// + /// Gets a value indicating whether the type implements the interface. + /// + bool ImplementsInterface(ITypeInfo interfaceTypeInfo); + + /// + /// Gets the for the TKey and TValue parameters of + /// . + /// + /// + /// The of TKey and TValue + /// parameters if the type implements , otherwise null. + /// + /// + /// For open generic types, for generic type parameters is null. + /// + ITypeInfo[] GetGenericDictionaryParameters(); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/RuntimePropertyInfo.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/RuntimePropertyInfo.cs new file mode 100644 index 0000000000..03c4d51751 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/RuntimePropertyInfo.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// adapter for instances. + /// + public class RuntimePropertyInfo : IPropertyInfo + { + /// + /// Initializes a new instance of . + /// + /// The instance to adapt. + public RuntimePropertyInfo(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + Property = propertyInfo; + } + + /// + /// The instance. + /// + public PropertyInfo Property { get; } + + /// + public bool HasPublicGetter => Property.GetMethod != null && Property.GetMethod.IsPublic; + + /// + public bool HasPublicSetter => Property.SetMethod != null && Property.SetMethod.IsPublic; + + /// + public string Name => Property.Name; + + /// + public ITypeInfo PropertyType => new RuntimeTypeInfo(Property.PropertyType.GetTypeInfo()); + + /// + public IEnumerable GetCustomAttributes() where TAttribute : Attribute + => Property.GetCustomAttributes(inherit: false); + + /// + public override string ToString() => + Property.ToString(); + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/RuntimeTypeInfo.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/RuntimeTypeInfo.cs new file mode 100644 index 0000000000..eed802bf01 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/RuntimeTypeInfo.cs @@ -0,0 +1,179 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// adapter for instances. + /// + public class RuntimeTypeInfo : ITypeInfo + { + private static readonly Regex _fullNameSanitizer = new Regex( + @", [A-Za-z\.]+, Version=\d+\.\d+\.\d+\.\d+, Culture=neutral, PublicKeyToken=\w+", + RegexOptions.ExplicitCapture, + Constants.RegexMatchTimeout); + + private static readonly TypeInfo TagHelperTypeInfo = typeof(ITagHelper).GetTypeInfo(); + private IEnumerable _properties; + private string _sanitizedFullName; + + /// + /// Initializes a new instance of + /// + /// The instance to adapt. + public RuntimeTypeInfo(TypeInfo typeInfo) + { + if (typeInfo == null) + { + throw new ArgumentNullException(nameof(typeInfo)); + } + + TypeInfo = typeInfo; + } + + /// + /// The instance. + /// + public TypeInfo TypeInfo { get; } + + /// + public string Name => TypeInfo.Name; + + /// + public string FullName => TypeInfo.FullName; + + /// + public bool IsAbstract => TypeInfo.IsAbstract; + + /// + public bool IsGenericType => TypeInfo.IsGenericType; + + /// + public bool IsPublic => TypeInfo.IsPublic; + + /// + public IEnumerable Properties + { + get + { + if (_properties == null) + { + _properties = TypeInfo + .AsType() + .GetRuntimeProperties() + .Where(property => property.GetIndexParameters().Length == 0) + .Select(property => new RuntimePropertyInfo(property)); + } + + return _properties; + } + } + + /// + public bool ImplementsInterface(ITypeInfo interfaceTypeInfo) + { + if (interfaceTypeInfo == null) + { + throw new ArgumentNullException(nameof(interfaceTypeInfo)); + } + + var runtimeTypeInfo = interfaceTypeInfo as RuntimeTypeInfo; + if (runtimeTypeInfo == null) + { + throw new ArgumentException( + Resources.FormatArgumentMustBeAnInstanceOf(typeof(RuntimeTypeInfo).FullName), + nameof(interfaceTypeInfo)); + } + + return runtimeTypeInfo.TypeInfo.IsInterface && runtimeTypeInfo.TypeInfo.IsAssignableFrom(TypeInfo); + } + + private string SanitizedFullName + { + get + { + if (_sanitizedFullName == null) + { + _sanitizedFullName = SanitizeFullName(FullName); + } + + return _sanitizedFullName; + } + } + + /// + public IEnumerable GetCustomAttributes() where TAttribute : Attribute => + TypeInfo.GetCustomAttributes(inherit: false); + + /// + public ITypeInfo[] GetGenericDictionaryParameters() + { + return ClosedGenericMatcher.ExtractGenericInterface( + TypeInfo.AsType(), + typeof(IDictionary<,>)) + ?.GenericTypeArguments + .Select(type => type.IsGenericParameter ? null : new RuntimeTypeInfo(type.GetTypeInfo())) + .ToArray(); + } + + /// + public override string ToString() => TypeInfo.ToString(); + + /// + public override bool Equals(object obj) + { + return Equals(obj as ITypeInfo); + } + + /// + public bool Equals(ITypeInfo other) + { + if (other == null) + { + return false; + } + + var otherRuntimeType = other as RuntimeTypeInfo; + if (otherRuntimeType != null) + { + return otherRuntimeType.TypeInfo == TypeInfo; + } + + return string.Equals( + SanitizedFullName, + SanitizeFullName(other.FullName), + StringComparison.Ordinal); + } + + /// + public override int GetHashCode() => SanitizedFullName.GetHashCode(); + + /// + /// Removes assembly qualification from generic type parameters for the specified . + /// + /// Full name. + /// Full name without fully qualified generic parameters. + /// + /// typeof().FullName is + /// List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089] + /// Sanitize(typeof(.FullName returns + /// List`1[[System.String] + /// + public static string SanitizeFullName(string fullName) + { + // In CoreCLR, some types (such as System.String) are type forwarded from System.Runtime + // to mscorlib at runtime. Type names of generic type parameters includes the assembly qualified name; + // consequently the type name generated at precompilation differs from the one at runtime. We'll + // avoid dealing with these inconsistencies by removing assembly information from TypeInfo.FullName. + return _fullNameSanitizer.Replace(fullName, string.Empty); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDescriptorFactory.cs new file mode 100644 index 0000000000..def04994f4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDescriptorFactory.cs @@ -0,0 +1,753 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Factory for s from s. + /// + public static class TagHelperDescriptorFactory + { + private const string DataDashPrefix = "data-"; + private const string TagHelperNameEnding = "TagHelper"; + private const string HtmlCaseRegexReplacement = "-$1$2"; + + // This matches the following AFTER the start of the input string (MATCH). + // Any letter/number followed by an uppercase letter then lowercase letter: 1(Aa), a(Aa), A(Aa) + // Any lowercase letter followed by an uppercase letter: a(A) + // Each match is then prefixed by a "-" via the ToHtmlCase method. + private static readonly Regex HtmlCaseRegex = + new Regex( + "(? InvalidNonWhitespaceNameCharacters { get; } = new HashSet( + new[] { '@', '!', '<', '/', '?', '[', '>', ']', '=', '"', '\'', '*' }); + + /// + /// Creates a from the given . + /// + /// The assembly name that contains . + /// The to create a from. + /// + /// Indicates if the returned s should include + /// design time specific information. + /// The used to collect s encountered + /// when creating s for the given . + /// + /// A collection of s that describe the given . + /// + public static IEnumerable CreateDescriptors( + string assemblyName, + ITypeInfo typeInfo, + bool designTime, + ErrorSink errorSink) + { + if (typeInfo == null) + { + throw new ArgumentNullException(nameof(typeInfo)); + } + + if (errorSink == null) + { + throw new ArgumentNullException(nameof(errorSink)); + } + + if (ShouldSkipDescriptorCreation(designTime, typeInfo)) + { + return Enumerable.Empty(); + } + + var attributeDescriptors = GetAttributeDescriptors(typeInfo, designTime, errorSink); + var targetElementAttributes = GetValidHtmlTargetElementAttributes(typeInfo, errorSink); + var allowedChildren = GetAllowedChildren(typeInfo, errorSink); + + var tagHelperDescriptors = + BuildTagHelperDescriptors( + typeInfo, + assemblyName, + attributeDescriptors, + targetElementAttributes, + allowedChildren, + designTime); + + return tagHelperDescriptors.Distinct(TagHelperDescriptorComparer.Default); + } + + private static IEnumerable GetValidHtmlTargetElementAttributes( + ITypeInfo typeInfo, + ErrorSink errorSink) + { + var targetElementAttributes = typeInfo.GetCustomAttributes(); + + return targetElementAttributes.Where( + attribute => ValidHtmlTargetElementAttributeNames(attribute, errorSink)); + } + + private static IEnumerable BuildTagHelperDescriptors( + ITypeInfo typeInfo, + string assemblyName, + IEnumerable attributeDescriptors, + IEnumerable targetElementAttributes, + IEnumerable allowedChildren, + bool designTime) + { + TagHelperDesignTimeDescriptor typeDesignTimeDescriptor = null; + +#if !DOTNET5_4 + if (designTime) + { + var runtimeTypeInfo = typeInfo as RuntimeTypeInfo; + if (runtimeTypeInfo != null) + { + typeDesignTimeDescriptor = + TagHelperDesignTimeDescriptorFactory.CreateDescriptor(runtimeTypeInfo.TypeInfo.AsType()); + } + } +#endif + + var typeName = typeInfo.FullName; + + // If there isn't an attribute specifying the tag name derive it from the name + if (!targetElementAttributes.Any()) + { + var name = typeInfo.Name; + + if (name.EndsWith(TagHelperNameEnding, StringComparison.OrdinalIgnoreCase)) + { + name = name.Substring(0, name.Length - TagHelperNameEnding.Length); + } + + return new[] + { + BuildTagHelperDescriptor( + ToHtmlCase(name), + typeName, + assemblyName, + attributeDescriptors, + requiredAttributes: Enumerable.Empty(), + allowedChildren: allowedChildren, + tagStructure: default(TagStructure), + parentTag: null, + designTimeDescriptor: typeDesignTimeDescriptor) + }; + } + + return targetElementAttributes.Select( + attribute => + BuildTagHelperDescriptor( + typeName, + assemblyName, + attributeDescriptors, + attribute, + allowedChildren, + typeDesignTimeDescriptor)); + } + + private static IEnumerable GetAllowedChildren(ITypeInfo typeInfo, ErrorSink errorSink) + { + var restrictChildrenAttribute = typeInfo + .GetCustomAttributes() + .FirstOrDefault(); + if (restrictChildrenAttribute == null) + { + return null; + } + + var allowedChildren = restrictChildrenAttribute.ChildTags; + var validAllowedChildren = GetValidAllowedChildren(allowedChildren, typeInfo.FullName, errorSink); + + if (validAllowedChildren.Any()) + { + return validAllowedChildren; + } + else + { + // All allowed children were invalid, return null to indicate that any child is acceptable. + return null; + } + } + + // Internal for unit testing + internal static IEnumerable GetValidAllowedChildren( + IEnumerable allowedChildren, + string tagHelperName, + ErrorSink errorSink) + { + var validAllowedChildren = new List(); + + foreach (var name in allowedChildren) + { + var valid = TryValidateName( + name, + whitespaceError: + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeNameNullWhitespace( + nameof(RestrictChildrenAttribute), + tagHelperName), + characterErrorBuilder: (invalidCharacter) => + Resources.FormatTagHelperDescriptorFactory_InvalidRestrictChildrenAttributeName( + nameof(RestrictChildrenAttribute), + name, + tagHelperName, + invalidCharacter), + errorSink: errorSink); + + if (valid) + { + validAllowedChildren.Add(name); + } + } + + return validAllowedChildren; + } + + private static TagHelperDescriptor BuildTagHelperDescriptor( + string typeName, + string assemblyName, + IEnumerable attributeDescriptors, + HtmlTargetElementAttribute targetElementAttribute, + IEnumerable allowedChildren, + TagHelperDesignTimeDescriptor designTimeDescriptor) + { + var requiredAttributes = GetCommaSeparatedValues(targetElementAttribute.Attributes); + + return BuildTagHelperDescriptor( + targetElementAttribute.Tag, + typeName, + assemblyName, + attributeDescriptors, + requiredAttributes, + allowedChildren, + targetElementAttribute.ParentTag, + targetElementAttribute.TagStructure, + designTimeDescriptor); + } + + private static TagHelperDescriptor BuildTagHelperDescriptor( + string tagName, + string typeName, + string assemblyName, + IEnumerable attributeDescriptors, + IEnumerable requiredAttributes, + IEnumerable allowedChildren, + string parentTag, + TagStructure tagStructure, + TagHelperDesignTimeDescriptor designTimeDescriptor) + { + return new TagHelperDescriptor + { + TagName = tagName, + TypeName = typeName, + AssemblyName = assemblyName, + Attributes = attributeDescriptors, + RequiredAttributes = requiredAttributes, + AllowedChildren = allowedChildren, + RequiredParent = parentTag, + TagStructure = tagStructure, + DesignTimeDescriptor = designTimeDescriptor + }; + } + + /// + /// Internal for testing. + /// + internal static IEnumerable GetCommaSeparatedValues(string text) + { + // We don't want to remove empty entries, need to notify users of invalid values. + return text?.Split(',').Select(tagName => tagName.Trim()) ?? Enumerable.Empty(); + } + + /// + /// Internal for testing. + /// + internal static bool ValidHtmlTargetElementAttributeNames( + HtmlTargetElementAttribute attribute, + ErrorSink errorSink) + { + var validTagName = ValidateName(attribute.Tag, targetingAttributes: false, errorSink: errorSink); + var validAttributeNames = true; + var attributeNames = GetCommaSeparatedValues(attribute.Attributes); + + foreach (var attributeName in attributeNames) + { + if (!ValidateName(attributeName, targetingAttributes: true, errorSink: errorSink)) + { + validAttributeNames = false; + } + } + + var validParentTagName = ValidateParentTagName(attribute.ParentTag, errorSink); + + return validTagName && validAttributeNames && validParentTagName; + } + + /// + /// Internal for unit testing. + /// + internal static bool ValidateParentTagName(string parentTag, ErrorSink errorSink) + { + return parentTag == null || + TryValidateName( + parentTag, + Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace( + Resources.TagHelperDescriptorFactory_ParentTag), + characterErrorBuilder: (invalidCharacter) => + Resources.FormatHtmlTargetElementAttribute_InvalidName( + Resources.TagHelperDescriptorFactory_ParentTag.ToLower(), + parentTag, + invalidCharacter), + errorSink: errorSink); + } + + private static bool ValidateName( + string name, + bool targetingAttributes, + ErrorSink errorSink) + { + if (!targetingAttributes && + string.Equals( + name, + TagHelperDescriptorProvider.ElementCatchAllTarget, + StringComparison.OrdinalIgnoreCase)) + { + // '*' as the entire name is OK in the HtmlTargetElement catch-all case. + return true; + } + else if (targetingAttributes && + name.EndsWith( + TagHelperDescriptorProvider.RequiredAttributeWildcardSuffix, + StringComparison.OrdinalIgnoreCase)) + { + // A single '*' at the end of a required attribute is valid; everywhere else is invalid. Strip it from + // the end so we can validate the rest of the name. + name = name.Substring(0, name.Length - 1); + } + + var targetName = targetingAttributes ? + Resources.TagHelperDescriptorFactory_Attribute : + Resources.TagHelperDescriptorFactory_Tag; + + var validName = TryValidateName( + name, + whitespaceError: Resources.FormatHtmlTargetElementAttribute_NameCannotBeNullOrWhitespace(targetName), + characterErrorBuilder: (invalidCharacter) => + Resources.FormatHtmlTargetElementAttribute_InvalidName( + targetName.ToLower(), + name, + invalidCharacter), + errorSink: errorSink); + + return validName; + } + + private static bool TryValidateName( + string name, + string whitespaceError, + Func characterErrorBuilder, + ErrorSink errorSink) + { + var validName = true; + + if (string.IsNullOrWhiteSpace(name)) + { + errorSink.OnError(SourceLocation.Zero, whitespaceError, length: 0); + + validName = false; + } + else + { + foreach (var character in name) + { + if (char.IsWhiteSpace(character) || + InvalidNonWhitespaceNameCharacters.Contains(character)) + { + var error = characterErrorBuilder(character); + errorSink.OnError(SourceLocation.Zero, error, length: 0); + + validName = false; + } + } + } + + return validName; + } + + private static IEnumerable GetAttributeDescriptors( + ITypeInfo type, + bool designTime, + ErrorSink errorSink) + { + var attributeDescriptors = new List(); + + // Keep indexer descriptors separate to avoid sorting the combined list later. + var indexerDescriptors = new List(); + + var accessibleProperties = type.Properties.Where(IsAccessibleProperty); + foreach (var property in accessibleProperties) + { + if (ShouldSkipDescriptorCreation(designTime, property)) + { + continue; + } + + var attributeNameAttribute = property + .GetCustomAttributes() + .FirstOrDefault(); + var hasExplicitName = + attributeNameAttribute != null && !string.IsNullOrEmpty(attributeNameAttribute.Name); + var attributeName = hasExplicitName ? attributeNameAttribute.Name : ToHtmlCase(property.Name); + + TagHelperAttributeDescriptor mainDescriptor = null; + if (property.HasPublicSetter) + { + mainDescriptor = ToAttributeDescriptor(property, attributeName, designTime); + if (!ValidateTagHelperAttributeDescriptor(mainDescriptor, type, errorSink)) + { + // HtmlAttributeNameAttribute.Name is invalid. Ignore this property completely. + continue; + } + } + else if (hasExplicitName) + { + // Specified HtmlAttributeNameAttribute.Name though property has no public setter. + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNotNullOrEmpty( + type.FullName, + property.Name, + typeof(HtmlAttributeNameAttribute).FullName, + nameof(HtmlAttributeNameAttribute.Name)), + length: 0); + continue; + } + + bool isInvalid; + var indexerDescriptor = ToIndexerAttributeDescriptor( + property, + attributeNameAttribute, + parentType: type, + errorSink: errorSink, + defaultPrefix: attributeName + "-", + designTime: designTime, + isInvalid: out isInvalid); + if (indexerDescriptor != null && + !ValidateTagHelperAttributeDescriptor(indexerDescriptor, type, errorSink)) + { + isInvalid = true; + } + + if (isInvalid) + { + // The property type or HtmlAttributeNameAttribute.DictionaryAttributePrefix (or perhaps the + // HTML-casing of the property name) is invalid. Ignore this property completely. + continue; + } + + if (mainDescriptor != null) + { + attributeDescriptors.Add(mainDescriptor); + } + + if (indexerDescriptor != null) + { + indexerDescriptors.Add(indexerDescriptor); + } + } + + attributeDescriptors.AddRange(indexerDescriptors); + + return attributeDescriptors; + } + + // Internal for testing. + internal static bool ValidateTagHelperAttributeDescriptor( + TagHelperAttributeDescriptor attributeDescriptor, + ITypeInfo parentType, + ErrorSink errorSink) + { + string nameOrPrefix; + if (attributeDescriptor.IsIndexer) + { + nameOrPrefix = Resources.TagHelperDescriptorFactory_Prefix; + } + else if (string.IsNullOrEmpty(attributeDescriptor.Name)) + { + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameNullOrEmpty( + parentType.FullName, + attributeDescriptor.PropertyName), + length: 0); + + return false; + } + else + { + nameOrPrefix = Resources.TagHelperDescriptorFactory_Name; + } + + return ValidateTagHelperAttributeNameOrPrefix( + attributeDescriptor.Name, + parentType, + attributeDescriptor.PropertyName, + errorSink, + nameOrPrefix); + } + + private static bool ShouldSkipDescriptorCreation(bool designTime, IMemberInfo memberInfo) + { + if (designTime) + { + var editorBrowsableAttribute = memberInfo + .GetCustomAttributes() + .FirstOrDefault(); + + return editorBrowsableAttribute != null && + editorBrowsableAttribute.State == EditorBrowsableState.Never; + } + + return false; + } + + private static bool ValidateTagHelperAttributeNameOrPrefix( + string attributeNameOrPrefix, + ITypeInfo parentType, + string propertyName, + ErrorSink errorSink, + string nameOrPrefix) + { + if (string.IsNullOrEmpty(attributeNameOrPrefix)) + { + // ValidateTagHelperAttributeDescriptor validates Name is non-null and non-empty. The empty string is + // valid for DictionaryAttributePrefix and null is impossible at this point because it means "don't + // create a descriptor". (Empty DictionaryAttributePrefix is a corner case which would bind every + // attribute of a target element. Likely not particularly useful but unclear what minimum length + // should be required and what scenarios a minimum length would break.) + return true; + } + + if (string.IsNullOrWhiteSpace(attributeNameOrPrefix)) + { + // Provide a single error if the entire name is whitespace, not an error per character. + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixWhitespace( + parentType.FullName, + propertyName, + nameOrPrefix), + length: 0); + + return false; + } + + // data-* attributes are explicitly not implemented by user agents and are not intended for use on + // the server; therefore it's invalid for TagHelpers to bind to them. + if (attributeNameOrPrefix.StartsWith(DataDashPrefix, StringComparison.OrdinalIgnoreCase)) + { + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixStart( + parentType.FullName, + propertyName, + nameOrPrefix, + attributeNameOrPrefix, + DataDashPrefix), + length: 0); + + return false; + } + + var isValid = true; + foreach (var character in attributeNameOrPrefix) + { + if (char.IsWhiteSpace(character) || InvalidNonWhitespaceNameCharacters.Contains(character)) + { + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameOrPrefixCharacter( + parentType.FullName, + propertyName, + nameOrPrefix, + attributeNameOrPrefix, + character), + length: 0); + + isValid = false; + } + } + + return isValid; + } + + private static TagHelperAttributeDescriptor ToAttributeDescriptor( + IPropertyInfo property, + string attributeName, + bool designTime) + { + return ToAttributeDescriptor( + property, + attributeName, + property.PropertyType.FullName, + isIndexer: false, + isStringProperty: StringTypeInfo.Equals(property.PropertyType), + designTime: designTime); + } + + private static TagHelperAttributeDescriptor ToIndexerAttributeDescriptor( + IPropertyInfo property, + HtmlAttributeNameAttribute attributeNameAttribute, + ITypeInfo parentType, + ErrorSink errorSink, + string defaultPrefix, + bool designTime, + out bool isInvalid) + { + isInvalid = false; + var hasPublicSetter = property.HasPublicSetter; + var dictionaryTypeArguments = property.PropertyType.GetGenericDictionaryParameters(); + if (!StringTypeInfo.Equals(dictionaryTypeArguments?[0])) + { + if (attributeNameAttribute?.DictionaryAttributePrefix != null) + { + // DictionaryAttributePrefix is not supported unless associated with an + // IDictionary property. + isInvalid = true; + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNotNull( + parentType.FullName, + property.Name, + nameof(HtmlAttributeNameAttribute), + nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), + "IDictionary"), + length: 0); + } + else if (attributeNameAttribute != null && !hasPublicSetter) + { + // Associated an HtmlAttributeNameAttribute with a non-dictionary property that lacks a public + // setter. + isInvalid = true; + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributeNameAttribute( + parentType.FullName, + property.Name, + nameof(HtmlAttributeNameAttribute), + "IDictionary"), + length: 0); + } + + return null; + } + else if (!hasPublicSetter && + attributeNameAttribute != null && + !attributeNameAttribute.DictionaryAttributePrefixSet) + { + // Must set DictionaryAttributePrefix when using HtmlAttributeNameAttribute with a dictionary property + // that lacks a public setter. + isInvalid = true; + errorSink.OnError( + SourceLocation.Zero, + Resources.FormatTagHelperDescriptorFactory_InvalidAttributePrefixNull( + parentType.FullName, + property.Name, + nameof(HtmlAttributeNameAttribute), + nameof(HtmlAttributeNameAttribute.DictionaryAttributePrefix), + "IDictionary"), + length: 0); + + return null; + } + + // Potential prefix case. Use default prefix (based on name)? + var useDefault = attributeNameAttribute == null || !attributeNameAttribute.DictionaryAttributePrefixSet; + + var prefix = useDefault ? defaultPrefix : attributeNameAttribute.DictionaryAttributePrefix; + if (prefix == null) + { + // DictionaryAttributePrefix explicitly set to null. Ignore. + return null; + } + + return ToAttributeDescriptor( + property, + attributeName: prefix, + typeName: dictionaryTypeArguments[1].FullName, + isIndexer: true, + isStringProperty: StringTypeInfo.Equals(dictionaryTypeArguments[1]), + designTime: designTime); + } + + private static TagHelperAttributeDescriptor ToAttributeDescriptor( + IPropertyInfo property, + string attributeName, + string typeName, + bool isIndexer, + bool isStringProperty, + bool designTime) + { + TagHelperAttributeDesignTimeDescriptor propertyDesignTimeDescriptor = null; + +#if !DOTNET5_4 + if (designTime) + { + var runtimeProperty = property as RuntimePropertyInfo; + if (runtimeProperty != null) + { + propertyDesignTimeDescriptor = + TagHelperDesignTimeDescriptorFactory.CreateAttributeDescriptor(runtimeProperty.Property); + } + } +#endif + + return new TagHelperAttributeDescriptor + { + Name = attributeName, + PropertyName = property.Name, + TypeName = typeName, + IsStringProperty = isStringProperty, + IsIndexer = isIndexer, + DesignTimeDescriptor = propertyDesignTimeDescriptor + }; + } + + private static bool IsAccessibleProperty(IPropertyInfo property) + { + // Accessible properties are those with public getters and without [HtmlAttributeNotBound]. + return property.HasPublicGetter && + property.GetCustomAttributes().FirstOrDefault() == null; + } + + /// + /// Converts from pascal/camel case to lower kebab-case. + /// + /// + /// SomeThing => some-thing + /// capsONInside => caps-on-inside + /// CAPSOnOUTSIDE => caps-on-outside + /// ALLCAPS => allcaps + /// One1Two2Three3 => one1-two2-three3 + /// ONE1TWO2THREE3 => one1two2three3 + /// First_Second_ThirdHi => first_second_third-hi + /// + private static string ToHtmlCase(string name) + { + return HtmlCaseRegex.Replace(name, HtmlCaseRegexReplacement).ToLowerInvariant(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDescriptorResolver.cs new file mode 100644 index 0000000000..d96346ea0a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -0,0 +1,307 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; +using Microsoft.AspNet.Razor.Parser; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Used to resolve s. + /// + public class TagHelperDescriptorResolver : ITagHelperDescriptorResolver + { + private static readonly IReadOnlyDictionary _directiveNames = + new Dictionary + { + { TagHelperDirectiveType.AddTagHelper, SyntaxConstants.CSharp.AddTagHelperKeyword }, + { TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword }, + { TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword }, + }; + + private readonly TagHelperTypeResolver _typeResolver; + private readonly bool _designTime; + + /// + /// Instantiates a new instance of the class. + /// + /// Indicates whether resolved s should include + /// design time specific information. + public TagHelperDescriptorResolver(bool designTime) + : this(new TagHelperTypeResolver(), designTime) + { + } + + /// + /// Instantiates a new instance of class with the + /// specified . + /// + /// The . + /// Indicates whether resolved s should include + /// design time specific information. + public TagHelperDescriptorResolver(TagHelperTypeResolver typeResolver, bool designTime) + { + _typeResolver = typeResolver; + _designTime = designTime; + } + + /// + public IEnumerable Resolve(TagHelperDescriptorResolutionContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var resolvedDescriptors = new HashSet(TagHelperDescriptorComparer.Default); + + // tagHelperPrefix directives do not affect which TagHelperDescriptors are added or removed from the final + // list, need to remove them. + var actionableDirectiveDescriptors = context.DirectiveDescriptors.Where( + directive => directive.DirectiveType != TagHelperDirectiveType.TagHelperPrefix); + + foreach (var directiveDescriptor in actionableDirectiveDescriptors) + { + try + { + var lookupInfo = GetLookupInfo(directiveDescriptor, context.ErrorSink); + + // Could not resolve the lookup info. + if (lookupInfo == null) + { + return Enumerable.Empty(); + } + + if (directiveDescriptor.DirectiveType == TagHelperDirectiveType.RemoveTagHelper) + { + resolvedDescriptors.RemoveWhere(descriptor => MatchesLookupInfo(descriptor, lookupInfo)); + } + else if (directiveDescriptor.DirectiveType == TagHelperDirectiveType.AddTagHelper) + { + var descriptors = ResolveDescriptorsInAssembly( + lookupInfo.AssemblyName, + lookupInfo.AssemblyNameLocation, + context.ErrorSink); + + // Only use descriptors that match our lookup info + descriptors = descriptors.Where(descriptor => MatchesLookupInfo(descriptor, lookupInfo)); + + resolvedDescriptors.UnionWith(descriptors); + } + } + catch (Exception ex) + { + string directiveName; + _directiveNames.TryGetValue(directiveDescriptor.DirectiveType, out directiveName); + Debug.Assert(!string.IsNullOrEmpty(directiveName)); + + context.ErrorSink.OnError( + directiveDescriptor.Location, + Resources.FormatTagHelperDescriptorResolver_EncounteredUnexpectedError( + "@" + directiveName, + directiveDescriptor.DirectiveText, + ex.Message), + GetErrorLength(directiveDescriptor.DirectiveText)); + } + } + + var prefixedDescriptors = PrefixDescriptors(context, resolvedDescriptors); + + return prefixedDescriptors; + } + + /// + /// Resolves all s for s from the + /// given . + /// + /// + /// The name of the assembly to resolve s from. + /// + /// The of the directive. + /// Used to record errors found when resolving s + /// within the given . + /// s for s from the given + /// . + // This is meant to be overridden by tooling to enable assembly level caching. + protected virtual IEnumerable ResolveDescriptorsInAssembly( + string assemblyName, + SourceLocation documentLocation, + ErrorSink errorSink) + { + // Resolve valid tag helper types from the assembly. + var tagHelperTypes = _typeResolver.Resolve(assemblyName, documentLocation, errorSink); + + // Convert types to TagHelperDescriptors + var descriptors = tagHelperTypes.SelectMany( + type => TagHelperDescriptorFactory.CreateDescriptors(assemblyName, type, _designTime, errorSink)); + + return descriptors; + } + + private static IEnumerable PrefixDescriptors( + TagHelperDescriptorResolutionContext context, + IEnumerable descriptors) + { + var tagHelperPrefix = ResolveTagHelperPrefix(context); + + if (!string.IsNullOrEmpty(tagHelperPrefix)) + { + return descriptors.Select(descriptor => + new TagHelperDescriptor + { + Prefix = tagHelperPrefix, + TagName = descriptor.TagName, + TypeName = descriptor.TypeName, + AssemblyName = descriptor.AssemblyName, + Attributes = descriptor.Attributes, + RequiredAttributes = descriptor.RequiredAttributes, + AllowedChildren = descriptor.AllowedChildren, + RequiredParent = descriptor.RequiredParent, + TagStructure = descriptor.TagStructure, + DesignTimeDescriptor = descriptor.DesignTimeDescriptor + }); + } + + return descriptors; + } + + private static string ResolveTagHelperPrefix(TagHelperDescriptorResolutionContext context) + { + var prefixDirectiveDescriptors = context.DirectiveDescriptors.Where( + descriptor => descriptor.DirectiveType == TagHelperDirectiveType.TagHelperPrefix); + + TagHelperDirectiveDescriptor prefixDirective = null; + + foreach (var directive in prefixDirectiveDescriptors) + { + if (prefixDirective == null) + { + prefixDirective = directive; + } + else + { + // For each invalid @tagHelperPrefix we need to create an error. + context.ErrorSink.OnError( + directive.Location, + Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperDirective( + SyntaxConstants.CSharp.TagHelperPrefixKeyword), + GetErrorLength(directive.DirectiveText)); + } + } + + var prefix = prefixDirective?.DirectiveText; + + if (prefix != null && !EnsureValidPrefix(prefix, prefixDirective.Location, context.ErrorSink)) + { + prefix = null; + } + + return prefix; + } + + private static bool EnsureValidPrefix( + string prefix, + SourceLocation directiveLocation, + ErrorSink errorSink) + { + foreach (var character in prefix) + { + // Prefixes are correlated with tag names, tag names cannot have whitespace. + if (char.IsWhiteSpace(character) || + TagHelperDescriptorFactory.InvalidNonWhitespaceNameCharacters.Contains(character)) + { + errorSink.OnError( + directiveLocation, + Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperPrefixValue( + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + character, + prefix), + prefix.Length); + + return false; + } + } + + return true; + } + + private static bool MatchesLookupInfo(TagHelperDescriptor descriptor, LookupInfo lookupInfo) + { + if (!string.Equals(descriptor.AssemblyName, lookupInfo.AssemblyName, StringComparison.Ordinal)) + { + return false; + } + + if (lookupInfo.TypePattern.EndsWith("*", StringComparison.Ordinal)) + { + if (lookupInfo.TypePattern.Length == 1) + { + // TypePattern is "*". + return true; + } + + var lookupTypeName = lookupInfo.TypePattern.Substring(0, lookupInfo.TypePattern.Length - 1); + + return descriptor.TypeName.StartsWith(lookupTypeName, StringComparison.Ordinal); + } + + return string.Equals(descriptor.TypeName, lookupInfo.TypePattern, StringComparison.Ordinal); + } + + private static LookupInfo GetLookupInfo( + TagHelperDirectiveDescriptor directiveDescriptor, + ErrorSink errorSink) + { + var lookupText = directiveDescriptor.DirectiveText; + var lookupStrings = lookupText?.Split(new[] { ',' }); + + // Ensure that we have valid lookupStrings to work with. The valid format is "typeName, assemblyName" + if (lookupStrings == null || + lookupStrings.Any(string.IsNullOrWhiteSpace) || + lookupStrings.Length != 2) + { + errorSink.OnError( + directiveDescriptor.Location, + Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperLookupText(lookupText), + GetErrorLength(lookupText)); + + return null; + } + + var trimmedAssemblyName = lookupStrings[1].Trim(); + + // + 1 is for the comma separator in the lookup text. + var assemblyNameIndex = lookupStrings[0].Length + 1 + lookupStrings[1].IndexOf(trimmedAssemblyName); + var assemblyNamePrefix = directiveDescriptor.DirectiveText.Substring(0, assemblyNameIndex); + var assemblyNameLocation = SourceLocation.Advance(directiveDescriptor.Location, assemblyNamePrefix); + + return new LookupInfo + { + TypePattern = lookupStrings[0].Trim(), + AssemblyName = trimmedAssemblyName, + AssemblyNameLocation = assemblyNameLocation, + }; + } + + private static int GetErrorLength(string directiveText) + { + var nonNullLength = directiveText == null ? 1 : directiveText.Length; + var normalizeEmptyStringLength = Math.Max(nonNullLength, 1); + + return normalizeEmptyStringLength; + } + + private class LookupInfo + { + public string AssemblyName { get; set; } + + public string TypePattern { get; set; } + + public SourceLocation AssemblyNameLocation { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDesignTimeDescriptorFactory.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDesignTimeDescriptorFactory.cs new file mode 100644 index 0000000000..22757e37a2 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperDesignTimeDescriptorFactory.cs @@ -0,0 +1,215 @@ +// 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. + +#if !DOTNET5_4 // Cannot accurately resolve the location of the documentation XML file in coreclr. +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Factory for providing s from s and + /// s from s. + /// + public static class TagHelperDesignTimeDescriptorFactory + { + /// + /// Creates a from the given . + /// + /// + /// The to create a from. + /// + /// A that describes design time specific information + /// for the given . + public static TagHelperDesignTimeDescriptor CreateDescriptor(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + var id = XmlDocumentationProvider.GetId(type); + var documentationDescriptor = CreateDocumentationDescriptor(type.Assembly, id); + + // Purposefully not using the TypeInfo.GetCustomAttributes method here to make it easier to mock the Type. + var outputElementHintAttribute = type + .GetCustomAttributes(inherit: false) + ?.OfType() + .FirstOrDefault(); + var outputElementHint = outputElementHintAttribute?.OutputElement; + + if (documentationDescriptor != null || outputElementHint != null) + { + return new TagHelperDesignTimeDescriptor + { + Summary = documentationDescriptor?.Summary, + Remarks = documentationDescriptor?.Remarks, + OutputElementHint = outputElementHint + }; + } + + return null; + } + + /// + /// Creates a from the given + /// . + /// + /// + /// The to create a from. + /// + /// A that describes design time specific + /// information for the given . + public static TagHelperAttributeDesignTimeDescriptor CreateAttributeDescriptor( + PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + var id = XmlDocumentationProvider.GetId(propertyInfo); + var declaringAssembly = propertyInfo.DeclaringType.Assembly; + var documentationDescriptor = CreateDocumentationDescriptor(declaringAssembly, id); + + if (documentationDescriptor != null) + { + return new TagHelperAttributeDesignTimeDescriptor + { + Summary = documentationDescriptor.Summary, + Remarks = documentationDescriptor.Remarks + }; + } + + return null; + } + + private static DocumentationDescriptor CreateDocumentationDescriptor(Assembly assembly, string id) + { + var assemblyLocation = assembly.Location; + + if (string.IsNullOrEmpty(assemblyLocation) && !string.IsNullOrEmpty(assembly.CodeBase)) + { + var uri = new UriBuilder(assembly.CodeBase); + + // Normalize the path to a UNC path. This will remove things like file:// from start of the uri.Path. + assemblyLocation = Uri.UnescapeDataString(uri.Path); + } + + // Couldn't resolve a valid assemblyLocation. + if (string.IsNullOrEmpty(assemblyLocation)) + { + return null; + } + + var xmlDocumentationFile = GetXmlDocumentationFile(assembly, assemblyLocation); + + // Only want to process the file if it exists. + if (xmlDocumentationFile != null) + { + var documentationProvider = new XmlDocumentationProvider(xmlDocumentationFile.FullName); + + var summary = documentationProvider.GetSummary(id); + var remarks = documentationProvider.GetRemarks(id); + + if (!string.IsNullOrEmpty(summary) || !string.IsNullOrEmpty(remarks)) + { + return new DocumentationDescriptor + { + Summary = summary, + Remarks = remarks + }; + } + } + + return null; + } + + private static FileInfo GetXmlDocumentationFile(Assembly assembly, string assemblyLocation) + { + try + { + var assemblyDirectory = Path.GetDirectoryName(assemblyLocation); + var assemblyName = Path.GetFileName(assemblyLocation); + var assemblyXmlDocumentationName = Path.ChangeExtension(assemblyName, ".xml"); + + // Check for a localized XML file for the current culture. + var xmlDocumentationFile = GetLocalizedXmlDocumentationFile( + CultureInfo.CurrentCulture, + assemblyDirectory, + assemblyXmlDocumentationName); + + if (xmlDocumentationFile == null) + { + // Check for a culture-neutral XML file next to the assembly + xmlDocumentationFile = new FileInfo( + Path.Combine(assemblyDirectory, assemblyXmlDocumentationName)); + + if (!xmlDocumentationFile.Exists) + { + xmlDocumentationFile = null; + } + } + + return xmlDocumentationFile; + } + catch (ArgumentException) + { + // Could not resolve XML file. + return null; + } + } + + private static IEnumerable ExpandPaths( + CultureInfo culture, + string assemblyDirectory, + string assemblyXmlDocumentationName) + { + // Following the fall-back process defined by: + // https://msdn.microsoft.com/en-us/library/sb6a8618.aspx#cpconpackagingdeployingresourcesanchor1 + do + { + var cultureName = culture.Name; + var cultureSpecificFileName = + Path.ChangeExtension(assemblyXmlDocumentationName, cultureName + ".xml"); + + // Look for a culture specific XML file next to the assembly. + yield return Path.Combine(assemblyDirectory, cultureSpecificFileName); + + // Look for an XML file with the same name as the assembly in a culture specific directory. + yield return Path.Combine(assemblyDirectory, cultureName, assemblyXmlDocumentationName); + + // Look for a culture specific XML file in a culture specific directory. + yield return Path.Combine(assemblyDirectory, cultureName, cultureSpecificFileName); + + culture = culture.Parent; + } while (culture != null && culture != CultureInfo.InvariantCulture); + } + + private static FileInfo GetLocalizedXmlDocumentationFile( + CultureInfo culture, + string assemblyDirectory, + string assemblyXmlDocumentationName) + { + var localizedXmlPaths = ExpandPaths(culture, assemblyDirectory, assemblyXmlDocumentationName); + var xmlDocumentationFile = localizedXmlPaths + .Select(path => new FileInfo(path)) + .FirstOrDefault(file => file.Exists); + + return xmlDocumentationFile; + } + + private class DocumentationDescriptor + { + public string Summary { get; set; } + public string Remarks { get; set; } + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperExecutionContext.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperExecutionContext.cs new file mode 100644 index 0000000000..1b9068611f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperExecutionContext.cs @@ -0,0 +1,255 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Class used to store information about a 's execution lifetime. + /// + public class TagHelperExecutionContext + { + private readonly List _tagHelpers; + private readonly Func _executeChildContentAsync; + private readonly Action _startTagHelperWritingScope; + private readonly Func _endTagHelperWritingScope; + private TagHelperContent _childContent; + + /// + /// Internal for testing purposes only. + /// + internal TagHelperExecutionContext(string tagName, TagMode tagMode) + : this(tagName, + tagMode, + items: new Dictionary(), + uniqueId: string.Empty, + executeChildContentAsync: async () => await Task.FromResult(result: true), + startTagHelperWritingScope: () => { }, + endTagHelperWritingScope: () => new DefaultTagHelperContent()) + { + } + + /// + /// Instantiates a new . + /// + /// The HTML tag name in the Razor source. + /// HTML syntax of the element in the Razor source. + /// The collection of items used to communicate with other + /// s + /// An identifier unique to the HTML element this context is for. + /// A delegate used to execute the child content asynchronously. + /// A delegate used to start a writing scope in a Razor page. + /// A delegate used to end a writing scope in a Razor page. + public TagHelperExecutionContext( + string tagName, + TagMode tagMode, + IDictionary items, + string uniqueId, + Func executeChildContentAsync, + Action startTagHelperWritingScope, + Func endTagHelperWritingScope) + { + if (tagName == null) + { + throw new ArgumentNullException(nameof(tagName)); + } + + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (uniqueId == null) + { + throw new ArgumentNullException(nameof(uniqueId)); + } + + if (executeChildContentAsync == null) + { + throw new ArgumentNullException(nameof(executeChildContentAsync)); + } + + if (startTagHelperWritingScope == null) + { + throw new ArgumentNullException(nameof(startTagHelperWritingScope)); + } + + if (endTagHelperWritingScope == null) + { + throw new ArgumentNullException(nameof(endTagHelperWritingScope)); + } + + _tagHelpers = new List(); + _executeChildContentAsync = executeChildContentAsync; + _startTagHelperWritingScope = startTagHelperWritingScope; + _endTagHelperWritingScope = endTagHelperWritingScope; + + TagMode = tagMode; + HTMLAttributes = new TagHelperAttributeList(); + AllAttributes = new TagHelperAttributeList(); + TagName = tagName; + Items = items; + UniqueId = uniqueId; + } + + /// + /// Gets the HTML syntax of the element in the Razor source. + /// + public TagMode TagMode { get; } + + /// + /// Indicates if has been called. + /// + public bool ChildContentRetrieved + { + get + { + return _childContent != null; + } + } + + /// + /// Gets the collection of items used to communicate with other s. + /// + public IDictionary Items { get; } + + /// + /// HTML attributes. + /// + public TagHelperAttributeList HTMLAttributes { get; } + + /// + /// bound attributes and HTML attributes. + /// + public TagHelperAttributeList AllAttributes { get; } + + /// + /// An identifier unique to the HTML element this context is for. + /// + public string UniqueId { get; } + + /// + /// s that should be run. + /// + public IEnumerable TagHelpers + { + get + { + return _tagHelpers; + } + } + + /// + /// The HTML tag name in the Razor source. + /// + public string TagName { get; } + + /// + /// The s' output. + /// + public TagHelperOutput Output { get; set; } + + /// + /// Tracks the given . + /// + /// The tag helper to track. + public void Add(ITagHelper tagHelper) + { + if (tagHelper == null) + { + throw new ArgumentNullException(nameof(tagHelper)); + } + + _tagHelpers.Add(tagHelper); + } + + /// + /// Tracks the minimized HTML attribute in and . + /// + /// The minimized HTML attribute name. + public void AddMinimizedHtmlAttribute(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + HTMLAttributes.Add( + new TagHelperAttribute + { + Name = name, + Minimized = true + }); + AllAttributes.Add( + new TagHelperAttribute + { + Name = name, + Minimized = true + }); + } + + /// + /// Tracks the HTML attribute in and . + /// + /// The HTML attribute name. + /// The HTML attribute value. + public void AddHtmlAttribute(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + HTMLAttributes.Add(name, value); + AllAttributes.Add(name, value); + } + + /// + /// Tracks the bound attribute in . + /// + /// The bound attribute name. + /// The attribute value. + public void AddTagHelperAttribute(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + AllAttributes.Add(name, value); + } + + /// + /// Executes the child content asynchronously. + /// + /// A which on completion executes all child content. + public Task ExecuteChildContentAsync() + { + return _executeChildContentAsync(); + } + + /// + /// Execute and retrieve the rendered child content asynchronously. + /// + /// A that on completion returns the rendered child content. + /// + /// Child content is only executed once. Successive calls to this method or successive executions of the + /// returned return a cached result. + /// + public async Task GetChildContentAsync(bool useCachedResult) + { + if (!useCachedResult || _childContent == null) + { + _startTagHelperWritingScope(); + await _executeChildContentAsync(); + _childContent = _endTagHelperWritingScope(); + } + + return new DefaultTagHelperContent().SetContent(_childContent); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperRunner.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperRunner.cs new file mode 100644 index 0000000000..c6ebeddef4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperRunner.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// A class used to run s. + /// + public class TagHelperRunner + { + /// + /// Calls the method on s. + /// + /// Contains information associated with running s. + /// + /// Resulting from processing all of the + /// 's s. + public async Task RunAsync(TagHelperExecutionContext executionContext) + { + if (executionContext == null) + { + throw new ArgumentNullException(nameof(executionContext)); + } + + var tagHelperContext = new TagHelperContext( + executionContext.AllAttributes, + executionContext.Items, + executionContext.UniqueId); + var orderedTagHelpers = executionContext.TagHelpers.OrderBy(tagHelper => tagHelper.Order); + + foreach (var tagHelper in orderedTagHelpers) + { + tagHelper.Init(tagHelperContext); + } + + var tagHelperOutput = new TagHelperOutput( + executionContext.TagName, + executionContext.HTMLAttributes, + executionContext.GetChildContentAsync) + { + TagMode = executionContext.TagMode, + }; + + foreach (var tagHelper in orderedTagHelpers) + { + await tagHelper.ProcessAsync(tagHelperContext, tagHelperOutput); + } + + return tagHelperOutput; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperScopeManager.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperScopeManager.cs new file mode 100644 index 0000000000..f2ae450871 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperScopeManager.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Class that manages scopes. + /// + public class TagHelperScopeManager + { + private readonly Stack _executionScopes; + + /// + /// Instantiates a new . + /// + public TagHelperScopeManager() + { + _executionScopes = new Stack(); + } + + /// + /// Starts a scope. + /// + /// The HTML tag name that the scope is associated with. + /// HTML syntax of the element in the Razor source. + /// An identifier unique to the HTML element this scope is for. + /// A delegate used to execute the child content asynchronously. + /// A delegate used to start a writing scope in a Razor page. + /// A delegate used to end a writing scope in a Razor page. + /// A to use. + public TagHelperExecutionContext Begin( + string tagName, + TagMode tagMode, + string uniqueId, + Func executeChildContentAsync, + Action startTagHelperWritingScope, + Func endTagHelperWritingScope) + { + if (tagName == null) + { + throw new ArgumentNullException(nameof(tagName)); + } + + if (uniqueId == null) + { + throw new ArgumentNullException(nameof(uniqueId)); + } + + if (executeChildContentAsync == null) + { + throw new ArgumentNullException(nameof(executeChildContentAsync)); + } + + if (startTagHelperWritingScope == null) + { + throw new ArgumentNullException(nameof(startTagHelperWritingScope)); + } + + if (endTagHelperWritingScope == null) + { + throw new ArgumentNullException(nameof(endTagHelperWritingScope)); + } + + IDictionary items; + + // If we're not wrapped by another TagHelper, then there will not be a parentExecutionContext. + if (_executionScopes.Count > 0) + { + items = new CopyOnWriteDictionary( + _executionScopes.Peek().Items, + comparer: EqualityComparer.Default); + } + else + { + items = new Dictionary(); + } + + var executionContext = new TagHelperExecutionContext( + tagName, + tagMode, + items, + uniqueId, + executeChildContentAsync, + startTagHelperWritingScope, + endTagHelperWritingScope); + + _executionScopes.Push(executionContext); + + return executionContext; + } + + /// + /// Ends a scope. + /// + /// If the current scope is nested, the parent . + /// null otherwise. + public TagHelperExecutionContext End() + { + if (_executionScopes.Count == 0) + { + throw new InvalidOperationException( + Resources.FormatScopeManager_EndCannotBeCalledWithoutACallToBegin( + nameof(End), + nameof(Begin), + nameof(TagHelperScopeManager))); + } + + _executionScopes.Pop(); + + if (_executionScopes.Count != 0) + { + return _executionScopes.Peek(); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperTypeResolver.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperTypeResolver.cs new file mode 100644 index 0000000000..e132ce345f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/TagHelperTypeResolver.cs @@ -0,0 +1,116 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Class that locates valid s within an assembly. + /// + public class TagHelperTypeResolver + { + private static readonly ITypeInfo ITagHelperTypeInfo = new RuntimeTypeInfo(typeof(ITagHelper).GetTypeInfo()); + + /// + /// Locates valid types from the named . + /// + /// The name of an to search. + /// The of the associated + /// responsible for the current call. + /// + /// The used to record errors found when resolving + /// types. + /// An of valid types. + public IEnumerable Resolve( + string name, + SourceLocation documentLocation, + ErrorSink errorSink) + { + if (errorSink == null) + { + throw new ArgumentNullException(nameof(errorSink)); + } + + if (string.IsNullOrEmpty(name)) + { + var errorLength = name == null ? 1 : Math.Max(name.Length, 1); + errorSink.OnError( + documentLocation, + Resources.TagHelperTypeResolver_TagHelperAssemblyNameCannotBeEmptyOrNull, + errorLength); + + return Enumerable.Empty(); + } + + var assemblyName = new AssemblyName(name); + + IEnumerable libraryTypes; + try + { + libraryTypes = GetTopLevelExportedTypes(assemblyName); + } + catch (Exception ex) + { + errorSink.OnError( + documentLocation, + Resources.FormatTagHelperTypeResolver_CannotResolveTagHelperAssembly( + assemblyName.Name, + ex.Message), + name.Length); + + return Enumerable.Empty(); + } + + return libraryTypes.Where(IsTagHelper); + } + + /// + /// Returns all non-nested exported types from the given + /// + /// The to get s from. + /// + /// An of types exported from the given . + /// + protected virtual IEnumerable GetTopLevelExportedTypes(AssemblyName assemblyName) + { + if (assemblyName == null) + { + throw new ArgumentNullException(nameof(assemblyName)); + } + + var exportedTypeInfos = GetExportedTypes(assemblyName); + + return exportedTypeInfos + .Where(typeInfo => !typeInfo.IsNested) + .Select(typeInfo => new RuntimeTypeInfo(typeInfo)); + } + + /// + /// Returns all exported types from the given + /// + /// The to get s from. + /// + /// An of types exported from the given . + /// + protected virtual IEnumerable GetExportedTypes(AssemblyName assemblyName) + { + var assembly = Assembly.Load(assemblyName); + + return assembly.ExportedTypes.Select(type => type.GetTypeInfo()); + } + + // Internal for testing. + internal virtual bool IsTagHelper(ITypeInfo typeInfo) + { + return typeInfo.IsPublic && + !typeInfo.IsAbstract && + !typeInfo.IsGenericType && + typeInfo.ImplementsInterface(ITagHelperTypeInfo); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/XmlDocumentationProvider.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/XmlDocumentationProvider.cs new file mode 100644 index 0000000000..e7c236af9e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/Runtime/TagHelpers/XmlDocumentationProvider.cs @@ -0,0 +1,129 @@ +// 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. + +#if !DOTNET5_4 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Xml.Linq; + +namespace Microsoft.AspNet.Razor.Runtime.TagHelpers +{ + /// + /// Extracts summary and remarks XML documentation from an XML documentation file. + /// + public class XmlDocumentationProvider + { + private readonly IEnumerable _members; + + /// + /// Instantiates a new instance of the . + /// + /// Path to the XML documentation file to read. + public XmlDocumentationProvider(string xmlFileLocation) + { + // XML file processing is defined by: https://msdn.microsoft.com/en-us/library/fsbx0t7x.aspx + var xmlDocumentation = XDocument.Load(xmlFileLocation); + var documentationRootMembers = xmlDocumentation.Root.Element("members"); + _members = documentationRootMembers.Elements("member"); + } + + /// + /// Retrieves the <summary> documentation for the given . + /// + /// The id to lookup. + /// <summary> documentation for the given . + public string GetSummary(string id) + { + var associatedMemeber = GetMember(id); + var summaryElement = associatedMemeber?.Element("summary"); + + if (summaryElement != null) + { + var summaryValue = GetElementValue(summaryElement); + + return summaryValue; + } + + return null; + } + + /// + /// Retrieves the <remarks> documentation for the given . + /// + /// The id to lookup. + /// <remarks> documentation for the given . + public string GetRemarks(string id) + { + var associatedMemeber = GetMember(id); + var remarksElement = associatedMemeber?.Element("remarks"); + + if (remarksElement != null) + { + var remarksValue = GetElementValue(remarksElement); + + return remarksValue; + } + + return null; + } + + /// + /// Generates the identifier for the given . + /// + /// The to get the identifier for. + /// The identifier for the given . + public static string GetId(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + return $"T:{type.FullName}"; + } + + /// + /// Generates the identifier for the given . + /// + /// The to get the identifier for. + /// The identifier for the given . + public static string GetId(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + var declaringTypeInfo = propertyInfo.DeclaringType; + return $"P:{declaringTypeInfo.FullName}.{propertyInfo.Name}"; + } + + private XElement GetMember(string id) + { + var associatedMemeber = _members + .FirstOrDefault(element => + string.Equals(element.Attribute("name").Value, id, StringComparison.Ordinal)); + + return associatedMemeber; + } + + private static string GetElementValue(XElement element) + { + var stringBuilder = new StringBuilder(); + var node = element.FirstNode; + + while (node != null) + { + stringBuilder.Append(node.ToString(SaveOptions.DisableFormatting)); + + node = node.NextNode; + } + + return stringBuilder.ToString().Trim(); + } + } +} +#endif \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/DefaultTagHelperContent.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/DefaultTagHelperContent.cs new file mode 100644 index 0000000000..38a28193ae --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/DefaultTagHelperContent.cs @@ -0,0 +1,231 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.AspNet.Html.Abstractions; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.WebEncoders; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Default concrete . + /// + [DebuggerDisplay("{DebuggerToString(),nq}")] + public class DefaultTagHelperContent : TagHelperContent + { + private BufferedHtmlContent _buffer; + + private BufferedHtmlContent Buffer + { + get + { + if (_buffer == null) + { + _buffer = new BufferedHtmlContent(); + } + + return _buffer; + } + } + + /// + public override bool IsModified => _buffer != null; + + /// + /// Returns true for a cleared . + public override bool IsWhiteSpace + { + get + { + if (_buffer == null) + { + return true; + } + + using (var writer = new EmptyOrWhiteSpaceWriter()) + { + foreach (var entry in _buffer.Entries) + { + if (entry == null) + { + continue; + } + + var stringValue = entry as string; + if (stringValue != null) + { + if (!string.IsNullOrWhiteSpace(stringValue)) + { + return false; + } + } + else + { + ((IHtmlContent)entry).WriteTo(writer, HtmlEncoder.Default); + if (!writer.IsWhiteSpace) + { + return false; + } + } + } + } + + return true; + } + } + + /// + public override bool IsEmpty + { + get + { + if (_buffer == null) + { + return true; + } + + using (var writer = new EmptyOrWhiteSpaceWriter()) + { + foreach (var entry in _buffer.Entries) + { + if (entry == null) + { + continue; + } + + var stringValue = entry as string; + if (stringValue != null) + { + if (!string.IsNullOrEmpty(stringValue)) + { + return false; + } + } + else + { + ((IHtmlContent)entry).WriteTo(writer, HtmlEncoder.Default); + if (!writer.IsEmpty) + { + return false; + } + } + } + } + + return true; + } + } + + /// + public override TagHelperContent Append(string unencoded) + { + Buffer.Append(unencoded); + return this; + } + + /// + public override TagHelperContent AppendHtml(string encoded) + { + Buffer.AppendHtml(encoded); + return this; + } + + /// + public override TagHelperContent Append(IHtmlContent htmlContent) + { + Buffer.Append(htmlContent); + return this; + } + + /// + public override TagHelperContent Clear() + { + Buffer.Clear(); + return this; + } + + /// + public override string GetContent() + { + return GetContent(HtmlEncoder.Default); + } + + /// + public override string GetContent(IHtmlEncoder encoder) + { + if (_buffer == null) + { + return string.Empty; + } + + using (var writer = new StringWriter()) + { + WriteTo(writer, encoder); + return writer.ToString(); + } + } + + /// + public override void WriteTo(TextWriter writer, IHtmlEncoder encoder) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (encoder == null) + { + throw new ArgumentNullException(nameof(encoder)); + } + + Buffer.WriteTo(writer, encoder); + } + + private string DebuggerToString() + { + return GetContent(); + } + + // Overrides Write(string) to find if the content written is empty/whitespace. + private class EmptyOrWhiteSpaceWriter : TextWriter + { + public override Encoding Encoding + { + get + { + throw new NotImplementedException(); + } + } + + public bool IsEmpty { get; private set; } = true; + + public bool IsWhiteSpace { get; private set; } = true; + +#if DOTNET5_4 + // This is an abstract method in DNXCore + public override void Write(char value) + { + throw new NotImplementedException(); + } +#endif + + public override void Write(string value) + { + if (IsEmpty && !string.IsNullOrEmpty(value)) + { + IsEmpty = false; + } + + if (IsWhiteSpace && !string.IsNullOrWhiteSpace(value)) + { + IsWhiteSpace = false; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlAttributeNameAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlAttributeNameAttribute.cs new file mode 100644 index 0000000000..597aa0a88e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlAttributeNameAttribute.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Used to override an property's HTML attribute name. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class HtmlAttributeNameAttribute : Attribute + { + private string _dictionaryAttributePrefix; + + /// + /// Instantiates a new instance of the class with + /// equal to null. + /// + /// + /// Associated property must not have a public setter and must be compatible with + /// where TKey is + /// . + /// + public HtmlAttributeNameAttribute() + { + } + + /// + /// Instantiates a new instance of the class. + /// + /// + /// HTML attribute name for the associated property. Must be null or empty if associated property does + /// not have a public setter and is compatible with + /// where TKey is + /// . Otherwise must not be null or empty. + /// + public HtmlAttributeNameAttribute(string name) + { + Name = name; + } + + /// + /// HTML attribute name of the associated property. + /// + /// + /// null or empty if and only if associated property does not have a public setter and is compatible + /// with where TKey is + /// . + /// + public string Name { get; } + + /// + /// Gets or sets the prefix used to match HTML attribute names. Matching attributes are added to the + /// associated property (an ). + /// + /// + /// If non-null associated property must be compatible with + /// where TKey is + /// . + /// + /// + /// + /// If associated property is compatible with + /// , default value is Name + "-". + /// must not be null or empty in this case. + /// + /// + /// Otherwise default value is null. + /// + /// + public string DictionaryAttributePrefix + { + get + { + return _dictionaryAttributePrefix; + } + set + { + _dictionaryAttributePrefix = value; + DictionaryAttributePrefixSet = true; + } + } + + /// + /// Gets an indication whether has been set. Used to distinguish an + /// uninitialized value from an explicit null setting. + /// + /// true if was set. false otherwise. + public bool DictionaryAttributePrefixSet { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlAttributeNotBoundAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlAttributeNotBoundAttribute.cs new file mode 100644 index 0000000000..15421d0736 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlAttributeNotBoundAttribute.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Indicates the associated property should not be bound to HTML attributes. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class HtmlAttributeNotBoundAttribute : Attribute + { + /// + /// Instantiates a new instance of the class. + /// + public HtmlAttributeNotBoundAttribute() + { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlTargetElementAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlTargetElementAttribute.cs new file mode 100644 index 0000000000..7a3f789e4f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/HtmlTargetElementAttribute.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Provides an 's target. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public sealed class HtmlTargetElementAttribute : Attribute + { + public const string ElementCatchAllTarget = TagHelperDescriptorProvider.ElementCatchAllTarget; + + /// + /// Instantiates a new instance of the class that targets all HTML + /// elements with the required . + /// + /// is set to *. + public HtmlTargetElementAttribute() + : this(ElementCatchAllTarget) + { + } + + /// + /// Instantiates a new instance of the class with the given + /// as its value. + /// + /// + /// The HTML tag the targets. + /// + /// A * value indicates this + /// targets all HTML elements with the required . + public HtmlTargetElementAttribute(string tag) + { + Tag = tag; + } + + /// + /// The HTML tag the targets. A * value indicates this + /// targets all HTML elements with the required . + /// + public string Tag { get; } + + /// + /// A comma-separated of attribute names the HTML element must contain for the + /// to run. * at the end of an attribute name acts as a prefix match. + /// + public string Attributes { get; set; } + + /// + /// The expected tag structure. Defaults to . + /// + /// + /// If and no other tag helpers applying to the same element specify + /// their the behavior is used: + /// + /// + /// <my-tag-helper></my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// Otherwise, if another tag helper applying to the same element does specify their behavior, that behavior + /// is used. + /// + /// + /// If HTML elements can be written in the following formats: + /// + /// <my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// + /// + public TagStructure TagStructure { get; set; } + + /// + /// The required HTML element name of the direct parent. A null value indicates any HTML element name is + /// allowed. + /// + public string ParentTag { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/IReadOnlyTagHelperAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/IReadOnlyTagHelperAttribute.cs new file mode 100644 index 0000000000..5b7148b80c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/IReadOnlyTagHelperAttribute.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// A read-only HTML tag helper attribute. + /// + public interface IReadOnlyTagHelperAttribute : IEquatable + { + /// + /// Gets the name of the attribute. + /// + string Name { get; } + + /// + /// Gets the value of the attribute. + /// + object Value { get; } + + /// + /// Gets an indication whether the attribute is minimized or not. + /// + /// If true, will be ignored. + bool Minimized { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/ITagHelper.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/ITagHelper.cs new file mode 100644 index 0000000000..5cb6151f22 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/ITagHelper.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Contract used to filter matching HTML elements. + /// + public interface ITagHelper + { + /// + /// When a set of s are executed, their's + /// are first invoked in the specified ; then their + /// 's are invoked in the specified + /// . Lower values are executed first. + /// + int Order { get; } + + /// + /// Initializes the with the given . Additions to + /// should be done within this method to ensure they're added prior to + /// executing the children. + /// + /// Contains information associated with the current HTML tag. + /// When more than one runs on the same element, + /// may be invoked prior to . + /// + void Init(TagHelperContext context); + + /// + /// Asynchronously executes the with the given and + /// . + /// + /// Contains information associated with the current HTML tag. + /// A stateful HTML element used to generate an HTML tag. + /// A that on completion updates the . + Task ProcessAsync(TagHelperContext context, TagHelperOutput output); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/OutputElementHintAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/OutputElementHintAttribute.cs new file mode 100644 index 0000000000..a7cae8fd69 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/OutputElementHintAttribute.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Provides a hint of the 's output element. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public sealed class OutputElementHintAttribute : Attribute + { + /// + /// Instantiates a new instance of the class. + /// + /// + /// The HTML element the may output. + /// + public OutputElementHintAttribute(string outputElement) + { + if (outputElement == null) + { + throw new ArgumentNullException(nameof(outputElement)); + } + + OutputElement = outputElement; + } + + /// + /// The HTML element the may output. + /// + public string OutputElement { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/ReadOnlyTagHelperAttributeList.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/ReadOnlyTagHelperAttributeList.cs new file mode 100644 index 0000000000..19694da295 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/ReadOnlyTagHelperAttributeList.cs @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// A read-only collection of s. + /// + /// + /// The type of s in the collection. + /// + public class ReadOnlyTagHelperAttributeList : IReadOnlyList + where TAttribute : IReadOnlyTagHelperAttribute + { + /// + /// Instantiates a new instance of with an empty + /// collection. + /// + protected ReadOnlyTagHelperAttributeList() + { + Attributes = new List(); + } + + /// + /// Instantiates a new instance of with the specified + /// . + /// + /// The collection to wrap. + public ReadOnlyTagHelperAttributeList(IEnumerable attributes) + { + if (attributes == null) + { + throw new ArgumentNullException(nameof(attributes)); + } + + Attributes = new List(attributes); + } + + /// + /// The underlying collection of s. + /// + /// Intended for use in a non-read-only subclass. Changes to this will + /// affect all getters that provides. + protected List Attributes { get; } + + /// + public TAttribute this[int index] + { + get + { + return Attributes[index]; + } + } + + /// + /// Gets the first with + /// matching . + /// + /// + /// The of the to get. + /// + /// The first with + /// matching . + /// + /// is compared case-insensitively. + public TAttribute this[string name] + { + get + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return Attributes.FirstOrDefault(attribute => NameEquals(name, attribute)); + } + } + + /// + public int Count + { + get + { + return Attributes.Count; + } + } + + /// + /// Determines whether a matching exists in the + /// collection. + /// + /// The to locate. + /// + /// true if an matching exists in the + /// collection; otherwise, false. + /// + /// + /// s is compared case-insensitively. + /// + public bool Contains(TAttribute item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + return Attributes.Contains(item); + } + + /// + /// Determines whether a with the same + /// exists in the collection. + /// + /// The of the + /// to get. + /// + /// true if a with the same + /// exists in the collection; otherwise, false. + /// + /// is compared case-insensitively. + public bool ContainsName(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return Attributes.Any(attribute => NameEquals(name, attribute)); + } + + /// + /// Searches for a matching in the collection and + /// returns the zero-based index of the first occurrence. + /// + /// The to locate. + /// The zero-based index of the first occurrence of a matching + /// in the collection, if found; otherwise, –1. + /// + /// s is compared case-insensitively. + /// + public int IndexOf(TAttribute item) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + return Attributes.IndexOf(item); + } + + /// + /// Retrieves the first with + /// matching . + /// + /// The of the + /// to get. + /// When this method returns, the first with + /// matching , if found; otherwise, + /// null. + /// true if a with the same + /// exists in the collection; otherwise, false. + /// is compared case-insensitively. + public bool TryGetAttribute(string name, out TAttribute attribute) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + attribute = Attributes.FirstOrDefault(attr => NameEquals(name, attr)); + + return attribute != null; + } + + /// + /// Retrieves s in the collection with + /// matching . + /// + /// The of the + /// s to get. + /// When this method returns, the s with + /// matching , if at least one is + /// found; otherwise, null. + /// true if at least one with the same + /// exists in the collection; otherwise, false. + /// is compared case-insensitively. + public bool TryGetAttributes(string name, out IEnumerable attributes) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + attributes = Attributes.Where(attribute => NameEquals(name, attribute)); + + return attributes.Any(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public IEnumerator GetEnumerator() + { + return Attributes.GetEnumerator(); + } + + /// + /// Determines if the specified has the same name as . + /// + /// The value to compare against s + /// . + /// The attribute to compare against. + /// true if case-insensitively matches s + /// . + protected static bool NameEquals(string name, TAttribute attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return string.Equals(name, attribute.Name, StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/RestrictChildrenAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/RestrictChildrenAttribute.cs new file mode 100644 index 0000000000..7f0626c134 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/RestrictChildrenAttribute.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Restricts children of the 's element. + /// + /// Combining this attribute with a that specifies its + /// as will result + /// in this attribute being ignored. + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public class RestrictChildrenAttribute : Attribute + { + /// + /// Instantiates a new instance of the class. + /// + /// + /// The tag name of an element allowed as a child. + /// + /// + /// Additional names of elements allowed as children. + /// + public RestrictChildrenAttribute(string childTag, params string[] childTags) + { + var concatenatedNames = new string[1 + childTags.Length]; + concatenatedNames[0] = childTag; + + childTags.CopyTo(concatenatedNames, 1); + + ChildTags = concatenatedNames; + } + + /// + /// Get the names of elements allowed as children. + /// + public IEnumerable ChildTags { get; } + } +} diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelper.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelper.cs new file mode 100644 index 0000000000..267a243deb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelper.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Class used to filter matching HTML elements. + /// + public abstract class TagHelper : ITagHelper + { + /// + /// Default order is 0. + public virtual int Order { get; } = 0; + + /// + public virtual void Init(TagHelperContext context) + { + } + + /// + /// Synchronously executes the with the given and + /// . + /// + /// Contains information associated with the current HTML tag. + /// A stateful HTML element used to generate an HTML tag. + public virtual void Process(TagHelperContext context, TagHelperOutput output) + { + } + + /// + /// Asynchronously executes the with the given and + /// . + /// + /// Contains information associated with the current HTML tag. + /// A stateful HTML element used to generate an HTML tag. + /// A that on completion updates the . + /// By default this calls into .. +#pragma warning disable 1998 + public virtual async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + Process(context, output); + } +#pragma warning restore 1998 + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperAttribute.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperAttribute.cs new file mode 100644 index 0000000000..9262bbe790 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperAttribute.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// An HTML tag helper attribute. + /// + public class TagHelperAttribute : IReadOnlyTagHelperAttribute + { + private static readonly int TypeHashCode = typeof(TagHelperAttribute).GetHashCode(); + + /// + /// Instantiates a new instance of . + /// + public TagHelperAttribute() + { + } + + /// + /// Instantiates a new instance of with values provided by the given + /// . + /// + /// A whose values should be copied. + public TagHelperAttribute(IReadOnlyTagHelperAttribute attribute) + : this(attribute?.Name, attribute?.Value) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + Minimized = attribute.Minimized; + } + + /// + /// Instantiates a new instance of with the specified + /// and . + /// + /// The of the attribute. + /// The of the attribute. + public TagHelperAttribute(string name, object value) + { + Name = name; + Value = value; + } + + /// + /// Gets or sets the name of the attribute. + /// + public string Name { get; set; } + + /// + /// Gets or sets the value of the attribute. + /// + public object Value { get; set; } + + /// + /// Gets or sets an indication whether the attribute is minimized or not. + /// + /// If true, will be ignored. + public bool Minimized { get; set; } + + /// + /// Converts the specified into a . + /// + /// The of the created . + /// Created s is set to null. + public static implicit operator TagHelperAttribute(string value) + { + return new TagHelperAttribute + { + Value = value + }; + } + + /// + /// is compared case-insensitively. + public bool Equals(IReadOnlyTagHelperAttribute other) + { + return + other != null && + string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) && + Minimized == other.Minimized && + (Minimized || Equals(Value, other.Value)); + } + + /// + public override bool Equals(object obj) + { + var other = obj as IReadOnlyTagHelperAttribute; + + return Equals(other); + } + + /// + public override int GetHashCode() + { + return TypeHashCode; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperAttributeList.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperAttributeList.cs new file mode 100644 index 0000000000..a98971701d --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperAttributeList.cs @@ -0,0 +1,292 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Runtime; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// A collection of s. + /// + public class TagHelperAttributeList : ReadOnlyTagHelperAttributeList, IList + { + /// + /// Instantiates a new instance of with an empty collection. + /// + public TagHelperAttributeList() + : base() + { + } + + /// + /// Instantiates a new instance of with the specified + /// . + /// + /// The collection to wrap. + public TagHelperAttributeList(IEnumerable attributes) + : base(attributes) + { + if (attributes == null) + { + throw new ArgumentNullException(nameof(attributes)); + } + } + + /// + /// + /// 's must not be null. + /// + public new TagHelperAttribute this[int index] + { + get + { + return base[index]; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (value.Name == null) + { + throw new ArgumentException( + Resources.FormatTagHelperAttributeList_CannotAddWithNullName( + typeof(TagHelperAttribute).FullName, + nameof(TagHelperAttribute.Name)), + nameof(value)); + } + + Attributes[index] = value; + } + } + + /// + /// Gets the first with matching + /// . When setting, replaces the first matching + /// with the specified and removes any additional + /// matching s. If a matching is not found, + /// adds the specified to the end of the collection. + /// + /// + /// The of the to get or set. + /// + /// The first with matching + /// . + /// + /// is compared case-insensitively. When setting, + /// s must be null or + /// case-insensitively match the specified . + /// + /// + /// var attributes = new TagHelperAttributeList(); + /// + /// // Will "value" be converted to a TagHelperAttribute with a null Name + /// attributes["name"] = "value"; + /// + /// // TagHelperAttribute.Name must match the specified name. + /// attributes["name"] = new TagHelperAttribute("name", "value"); + /// + /// + public new TagHelperAttribute this[string name] + { + get + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return base[name]; + } + set + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + // Name will be null if user attempts to set the attribute via an implicit conversion: + // output.Attributes["someName"] = "someValue" + if (value.Name == null) + { + value.Name = name; + } + else if (!NameEquals(name, value)) + { + throw new ArgumentException( + Resources.FormatTagHelperAttributeList_CannotAddAttribute( + nameof(TagHelperAttribute), + nameof(TagHelperAttribute.Name), + value.Name, + name), + nameof(name)); + } + + var attributeReplaced = false; + + for (var i = 0; i < Attributes.Count; i++) + { + if (NameEquals(name, Attributes[i])) + { + // We replace the first attribute with the provided value, remove all the rest. + if (!attributeReplaced) + { + // We replace the first attribute we find with the same name. + Attributes[i] = value; + attributeReplaced = true; + } + else + { + Attributes.RemoveAt(i--); + } + } + } + + // If we didn't replace an attribute value we should add value to the end of the collection. + if (!attributeReplaced) + { + Add(value); + } + } + } + + /// + bool ICollection.IsReadOnly + { + get + { + return false; + } + } + + /// + /// Adds a to the end of the collection with the specified + /// and . + /// + /// The of the attribute to add. + /// The of the attribute to add. + public void Add(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Attributes.Add(new TagHelperAttribute(name, value)); + } + + /// + /// + /// 's must not be null. + /// + public void Add(TagHelperAttribute attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + if (attribute.Name == null) + { + throw new ArgumentException( + Resources.FormatTagHelperAttributeList_CannotAddWithNullName( + typeof(TagHelperAttribute).FullName, + nameof(TagHelperAttribute.Name)), + nameof(attribute)); + } + + Attributes.Add(attribute); + } + + /// + /// + /// 's must not be null. + /// + public void Insert(int index, TagHelperAttribute attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + if (attribute.Name == null) + { + throw new ArgumentException( + Resources.FormatTagHelperAttributeList_CannotAddWithNullName( + typeof(TagHelperAttribute).FullName, + nameof(TagHelperAttribute.Name)), + nameof(attribute)); + } + + Attributes.Insert(index, attribute); + } + + /// + public void CopyTo(TagHelperAttribute[] array, int index) + { + if (array == null) + { + throw new ArgumentNullException(nameof(array)); + } + + Attributes.CopyTo(array, index); + } + + /// + /// + /// s is compared case-insensitively. + /// + public bool Remove(TagHelperAttribute attribute) + { + if (attribute == null) + { + throw new ArgumentNullException(nameof(attribute)); + } + + return Attributes.Remove(attribute); + } + + /// + public void RemoveAt(int index) + { + Attributes.RemoveAt(index); + } + + /// + /// Removes all s with matching + /// . + /// + /// + /// The of s to remove. + /// + /// + /// true if at least 1 was removed; otherwise, false. + /// + /// is compared case-insensitively. + public bool RemoveAll(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + return Attributes.RemoveAll(attribute => NameEquals(name, attribute)) > 0; + } + + /// + public void Clear() + { + Attributes.Clear(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperContent.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperContent.cs new file mode 100644 index 0000000000..8fe406274f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperContent.cs @@ -0,0 +1,171 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.AspNet.Html.Abstractions; +using Microsoft.Extensions.WebEncoders; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Abstract class used to buffer content returned by s. + /// + public abstract class TagHelperContent : IHtmlContentBuilder + { + /// + /// Gets a value indicating whether the content was modifed. + /// + public abstract bool IsModified { get; } + + /// + /// Gets a value indicating whether the content is empty. + /// + public abstract bool IsEmpty { get; } + + /// + /// Gets a value indicating whether the content is whitespace. + /// + public abstract bool IsWhiteSpace { get; } + + /// + /// Sets the content. + /// + /// The that replaces the content. + /// A reference to this instance after the set operation has completed. + public TagHelperContent SetContent(IHtmlContent htmlContent) + { + HtmlContentBuilderExtensions.SetContent(this, htmlContent); + return this; + } + + /// + /// Sets the content. + /// + /// + /// The that replaces the content. The value is assume to be unencoded + /// as-provided and will be HTML encoded before being written. + /// + /// A reference to this instance after the set operation has completed. + public TagHelperContent SetContent(string unencoded) + { + HtmlContentBuilderExtensions.SetContent(this, unencoded); + return this; + } + + /// + /// Sets the content. + /// + /// + /// The that replaces the content. The value is assume to be HTML encoded + /// as-provided and no further encoding will be performed. + /// + /// A reference to this instance after the set operation has completed. + public TagHelperContent SetHtmlContent(string encoded) + { + HtmlContentBuilderExtensions.SetHtmlContent(this, encoded); + return this; + } + + /// + /// Appends to the existing content. + /// + /// The to be appended. + /// A reference to this instance after the append operation has completed. + public abstract TagHelperContent Append(string unencoded); + + /// + /// Appends to the existing content. + /// + /// The to be appended. + /// A reference to this instance after the append operation has completed. + public abstract TagHelperContent Append(IHtmlContent htmlContent); + + /// + /// Appends to the existing content. is assumed + /// to be an HTML encoded and no further encoding will be performed. + /// + /// The to be appended. + /// A reference to this instance after the append operation has completed. + public abstract TagHelperContent AppendHtml(string encoded); + + /// + /// Appends the specified to the existing content after + /// replacing each format item with the HTML encoded representation of the + /// corresponding item in the array. + /// + /// + /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). + /// + /// The object array to format. + /// A reference to this instance after the append operation has completed. + public TagHelperContent AppendFormat(string format, params object[] args) + { + HtmlContentBuilderExtensions.AppendFormat(this, null, format, args); + return this; + } + + /// + /// Appends the specified to the existing content with information from the + /// after replacing each format item with the HTML encoded + /// representation of the corresponding item in the array. + /// + /// An object that supplies culture-specific formatting information. + /// + /// The composite format (see http://msdn.microsoft.com/en-us/library/txafckwd.aspx). + /// + /// The object array to format. + /// A reference to this instance after the append operation has completed. + public TagHelperContent AppendFormat(IFormatProvider provider, string format, params object[] args) + { + HtmlContentBuilderExtensions.AppendFormat(this, provider, format, args); + return this; + } + + /// + /// Clears the content. + /// + /// A reference to this instance after the clear operation has completed. + public abstract TagHelperContent Clear(); + + /// + /// Gets the content. + /// + /// A containing the content. + public abstract string GetContent(); + + /// + /// Gets the content. + /// + /// The . + /// A containing the content. + public abstract string GetContent(IHtmlEncoder encoder); + + /// + public abstract void WriteTo(TextWriter writer, IHtmlEncoder encoder); + + /// + IHtmlContentBuilder IHtmlContentBuilder.Append(IHtmlContent content) + { + return Append(content); + } + + /// + IHtmlContentBuilder IHtmlContentBuilder.Append(string unencoded) + { + return Append(unencoded); + } + + /// + IHtmlContentBuilder IHtmlContentBuilder.AppendHtml(string encoded) + { + return AppendHtml(encoded); + } + + /// + IHtmlContentBuilder IHtmlContentBuilder.Clear() + { + return Clear(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperContext.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperContext.cs new file mode 100644 index 0000000000..272ec5a48c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperContext.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Contains information related to the execution of s. + /// + public class TagHelperContext + { + /// + /// Instantiates a new . + /// + /// Every attribute associated with the current HTML element. + /// Collection of items used to communicate with other s. + /// The unique identifier for the source element this + /// applies to. + public TagHelperContext( + IEnumerable allAttributes, + IDictionary items, + string uniqueId) + { + if (allAttributes == null) + { + throw new ArgumentNullException(nameof(allAttributes)); + } + + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (uniqueId == null) + { + throw new ArgumentNullException(nameof(uniqueId)); + } + + AllAttributes = new ReadOnlyTagHelperAttributeList( + allAttributes.Select(attribute => new TagHelperAttribute(attribute.Name, attribute.Value))); + Items = items; + UniqueId = uniqueId; + } + + /// + /// Every attribute associated with the current HTML element. + /// + public ReadOnlyTagHelperAttributeList AllAttributes { get; } + + /// + /// Gets the collection of items used to communicate with other s. + /// + /// + /// This is copy-on-write in order to ensure items added to this + /// collection are visible only to other s targeting child elements. + /// + public IDictionary Items { get; } + + /// + /// An identifier unique to the HTML element this context is for. + /// + public string UniqueId { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperOutput.cs b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperOutput.cs new file mode 100644 index 0000000000..af8eca767c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/TagHelpers/TagHelperOutput.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Razor.TagHelpers +{ + /// + /// Class used to represent the output of an . + /// + public class TagHelperOutput + { + private readonly Func> _getChildContentAsync; + + // Internal for testing + internal TagHelperOutput(string tagName) + : this( + tagName, + new TagHelperAttributeList(), + (cachedResult) => Task.FromResult(new DefaultTagHelperContent())) + { + } + + /// + /// Instantiates a new instance of . + /// + /// The HTML element's tag name. + /// The HTML attributes. + /// A delegate used to execute and retrieve the rendered child content + /// asynchronously. + public TagHelperOutput( + string tagName, + TagHelperAttributeList attributes, + Func> getChildContentAsync) + { + if (attributes == null) + { + throw new ArgumentNullException(nameof(attributes)); + } + + if (getChildContentAsync == null) + { + throw new ArgumentNullException(nameof(getChildContentAsync)); + } + + TagName = tagName; + Attributes = new TagHelperAttributeList(attributes); + _getChildContentAsync = getChildContentAsync; + } + + /// + /// The HTML element's tag name. + /// + /// + /// A whitespace or null value results in no start or end tag being rendered. + /// + public string TagName { get; set; } + + /// + /// Content that precedes the HTML element. + /// + /// Value is rendered before the HTML element. + public TagHelperContent PreElement { get; } = new DefaultTagHelperContent(); + + /// + /// The HTML element's pre content. + /// + /// Value is prepended to the 's final output. + public TagHelperContent PreContent { get; } = new DefaultTagHelperContent(); + + /// + /// The HTML element's main content. + /// + /// Value occurs in the 's final output after and + /// before + public TagHelperContent Content { get; } = new DefaultTagHelperContent(); + + /// + /// The HTML element's post content. + /// + /// Value is appended to the 's final output. + public TagHelperContent PostContent { get; } = new DefaultTagHelperContent(); + + /// + /// Content that follows the HTML element. + /// + /// Value is rendered after the HTML element. + public TagHelperContent PostElement { get; } = new DefaultTagHelperContent(); + + /// + /// true if has been set, false otherwise. + /// + public bool IsContentModified + { + get + { + return Content.IsModified; + } + } + + /// + /// Syntax of the element in the generated HTML. + /// + public TagMode TagMode { get; set; } + + /// + /// The HTML element's attributes. + /// + /// + /// MVC will HTML encode values when generating the start tag. It will not HTML encode + /// a Microsoft.AspNet.Mvc.Rendering.HtmlString instance. MVC converts most other types to a + /// , then HTML encodes the result. + /// + public TagHelperAttributeList Attributes { get; } + + /// + /// Changes to generate nothing. + /// + /// + /// Sets to null, and clears , , + /// , , and to suppress output. + /// + public void SuppressOutput() + { + TagName = null; + PreElement.Clear(); + PreContent.Clear(); + Content.Clear(); + PostContent.Clear(); + PostElement.Clear(); + } + + /// + /// A delegate used to execute children asynchronously. + /// + /// A that on completion returns content rendered by children. + /// This method is memoized. + public Task GetChildContentAsync() + { + return GetChildContentAsync(useCachedResult: true); + } + + /// + /// A delegate used to execute children asynchronously. + /// + /// If true multiple calls to this method will not cause re-execution + /// of child content; cached content will be returned. + /// A that on completion returns content rendered by children. + public Task GetChildContentAsync(bool useCachedResult) + { + return _getChildContentAsync(useCachedResult); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime.VSRC1/project.json b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/project.json new file mode 100644 index 0000000000..5dd579771c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.Runtime.VSRC1/project.json @@ -0,0 +1,24 @@ +{ + "description": "Runtime components for rendering Razor pages.", + "version": "4.0.0-rc1-final", + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "repository": { + "type": "git", + "url": "git://github.com/aspnet/razor" + }, + "dependencies": { + "Microsoft.AspNet.Html.Abstractions": "1.0.0-rc1-final", + "Microsoft.AspNet.Razor.VSRC1": "4.0.0-rc1-final" + }, + "frameworks": { + "net451": { + "frameworkAssemblies": { + "System.Xml": "4.0.0.0", + "System.Xml.Linq": "4.0.0.0" + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CSharpRazorCodeLanguage.cs b/src/Microsoft.AspNet.Razor.VSRC1/CSharpRazorCodeLanguage.cs new file mode 100644 index 0000000000..7c48b45c85 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CSharpRazorCodeLanguage.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Parser; +#if NET451 + +#endif + +namespace Microsoft.AspNet.Razor +{ + /// + /// Defines the C# Code Language for Razor + /// + public class CSharpRazorCodeLanguage : RazorCodeLanguage + { + private const string CSharpLanguageName = "csharp"; + + /// + /// Returns the name of the language: "csharp" + /// + public override string LanguageName + { + get { return CSharpLanguageName; } + } + + /// + /// Constructs a new instance of the code parser for this language + /// + public override ParserBase CreateCodeParser() + { + return new CSharpCodeParser(); + } + + /// + /// Constructs a new instance of the chunk generator for this language with the specified settings + /// + public override RazorChunkGenerator CreateChunkGenerator( + string className, + string rootNamespaceName, + string sourceFileName, + RazorEngineHost host) + { + return new RazorChunkGenerator(className, rootNamespaceName, sourceFileName, host); + } + + public override CodeGenerator CreateCodeGenerator(CodeGeneratorContext context) + { + return new CSharpCodeGenerator(context); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/AddTagHelperChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/AddTagHelperChunk.cs new file mode 100644 index 0000000000..cd10865d85 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/AddTagHelperChunk.cs @@ -0,0 +1,16 @@ +// 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.AspNet.Razor.Chunks +{ + /// + /// A used to look up s. + /// + public class AddTagHelperChunk : Chunk + { + /// + /// Text used to look up s. + /// + public string LookupText { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Chunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Chunk.cs new file mode 100644 index 0000000000..b63733bc99 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Chunk.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class Chunk + { + public SourceLocation Start { get; set; } + public SyntaxTreeNode Association { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ChunkTree.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ChunkTree.cs new file mode 100644 index 0000000000..8d45935c3c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ChunkTree.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class ChunkTree + { + public ChunkTree() + { + Chunks = new List(); + } + + public IList Chunks { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ChunkTreeBuilder.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ChunkTreeBuilder.cs new file mode 100644 index 0000000000..fe828e4752 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ChunkTreeBuilder.cs @@ -0,0 +1,169 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class ChunkTreeBuilder + { + private readonly Stack _parentStack; + private Chunk _lastChunk; + + public ChunkTreeBuilder() + { + ChunkTree = new ChunkTree(); + _parentStack = new Stack(); + } + + public ChunkTree ChunkTree { get; private set; } + + public void AddChunk(Chunk chunk, SyntaxTreeNode association, bool topLevel = false) + { + _lastChunk = chunk; + + chunk.Start = association.Start; + chunk.Association = association; + + // If we're not in the middle of a parent chunk + if (_parentStack.Count == 0 || topLevel == true) + { + ChunkTree.Chunks.Add(chunk); + } + else + { + _parentStack.Peek().Children.Add(chunk); + } + } + + public void AddTagHelperPrefixDirectiveChunk(string prefix, SyntaxTreeNode association) + { + AddChunk( + new TagHelperPrefixDirectiveChunk + { + Prefix = prefix + }, + association, + topLevel: true); + } + + public void AddAddTagHelperChunk(string lookupText, SyntaxTreeNode association) + { + AddChunk(new AddTagHelperChunk + { + LookupText = lookupText + }, association, topLevel: true); + } + + public void AddRemoveTagHelperChunk(string lookupText, SyntaxTreeNode association) + { + AddChunk(new RemoveTagHelperChunk + { + LookupText = lookupText + }, association, topLevel: true); + } + + public void AddLiteralChunk(string literal, SyntaxTreeNode association) + { + // If the previous chunk was also a LiteralChunk, append the content of the current node to the previous one. + var literalChunk = _lastChunk as LiteralChunk; + if (literalChunk != null) + { + // Literal chunks are always associated with Spans + var lastSpan = (Span)literalChunk.Association; + var currentSpan = (Span)association; + + var builder = new SpanBuilder(lastSpan); + foreach (var symbol in currentSpan.Symbols) + { + builder.Accept(symbol); + } + + literalChunk.Association = builder.Build(); + literalChunk.Text += literal; + } + else + { + AddChunk(new LiteralChunk + { + Text = literal, + }, association); + } + } + + public void AddExpressionChunk(string expression, SyntaxTreeNode association) + { + AddChunk(new ExpressionChunk + { + Code = expression + }, association); + } + + public void AddStatementChunk(string code, SyntaxTreeNode association) + { + AddChunk(new StatementChunk + { + Code = code, + }, association); + } + + public void AddUsingChunk(string usingNamespace, SyntaxTreeNode association) + { + AddChunk(new UsingChunk + { + Namespace = usingNamespace, + }, association, topLevel: true); + } + + public void AddTypeMemberChunk(string code, SyntaxTreeNode association) + { + AddChunk(new TypeMemberChunk + { + Code = code, + }, association, topLevel: true); + } + + public void AddLiteralCodeAttributeChunk(string code, SyntaxTreeNode association) + { + AddChunk(new LiteralCodeAttributeChunk + { + Code = code, + }, association); + } + + public void AddSetBaseTypeChunk(string typeName, SyntaxTreeNode association) + { + AddChunk(new SetBaseTypeChunk + { + TypeName = typeName.Trim() + }, association, topLevel: true); + } + + public T StartParentChunk(SyntaxTreeNode association) where T : ParentChunk, new() + { + return StartParentChunk(association, topLevel: false); + } + + public T StartParentChunk(SyntaxTreeNode association, bool topLevel) where T : ParentChunk, new() + { + var parentChunk = new T(); + + return StartParentChunk(parentChunk, association, topLevel); + } + + public T StartParentChunk(T parentChunk, SyntaxTreeNode association, bool topLevel) where T : ParentChunk + { + AddChunk(parentChunk, association, topLevel); + + _parentStack.Push(parentChunk); + + return parentChunk; + } + + public void EndParentChunk() + { + _lastChunk = _parentStack.Pop(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/CodeAttributeChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/CodeAttributeChunk.cs new file mode 100644 index 0000000000..cecd46ce19 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/CodeAttributeChunk.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class CodeAttributeChunk : ParentChunk + { + public string Attribute { get; set; } + public LocationTagged Prefix { get; set; } + public LocationTagged Suffix { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/DynamicCodeAttributeChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/DynamicCodeAttributeChunk.cs new file mode 100644 index 0000000000..54c0092c02 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/DynamicCodeAttributeChunk.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class DynamicCodeAttributeChunk : ParentChunk + { + public LocationTagged Prefix { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ExpressionBlockChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ExpressionBlockChunk.cs new file mode 100644 index 0000000000..dc6dc0bc8a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ExpressionBlockChunk.cs @@ -0,0 +1,9 @@ +// 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.AspNet.Razor.Chunks +{ + public class ExpressionBlockChunk : ParentChunk + { + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ExpressionChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ExpressionChunk.cs new file mode 100644 index 0000000000..c783151248 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ExpressionChunk.cs @@ -0,0 +1,15 @@ +// 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.AspNet.Razor.Chunks +{ + public class ExpressionChunk : Chunk + { + public string Code { get; set; } + + public override string ToString() + { + return Start + " = " + Code; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AddImportChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AddImportChunkGenerator.cs new file mode 100644 index 0000000000..27d1ba712a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AddImportChunkGenerator.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class AddImportChunkGenerator : SpanChunkGenerator + { + public AddImportChunkGenerator(string ns) + { + Namespace = ns; + } + + public string Namespace { get; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + var ns = Namespace; + + if (!string.IsNullOrEmpty(ns) && char.IsWhiteSpace(ns[0])) + { + ns = ns.Substring(1); + } + + context.ChunkTreeBuilder.AddUsingChunk(ns, target); + } + + public override string ToString() + { + return "Import:" + Namespace + ";"; + } + + public override bool Equals(object obj) + { + var other = obj as AddImportChunkGenerator; + return other != null && + string.Equals(Namespace, other.Namespace, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + return Namespace == null ? 0 : StringComparer.Ordinal.GetHashCode(Namespace); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AddOrRemoveTagHelperChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AddOrRemoveTagHelperChunkGenerator.cs new file mode 100644 index 0000000000..bfc64a4ebf --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AddOrRemoveTagHelperChunkGenerator.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + /// + /// A responsible for generating s and + /// s. + /// + public class AddOrRemoveTagHelperChunkGenerator : SpanChunkGenerator + { + /// + /// Instantiates a new . + /// + /// + /// Text used to look up s that should be added or removed. + /// + public AddOrRemoveTagHelperChunkGenerator(bool removeTagHelperDescriptors, string lookupText) + { + RemoveTagHelperDescriptors = removeTagHelperDescriptors; + LookupText = lookupText; + } + + /// + /// Gets the text used to look up s that should be added to or + /// removed from the Razor page. + /// + public string LookupText { get; } + + /// + /// Whether we want to remove s from the Razor page. + /// + /// If true generates s, + /// s otherwise. + public bool RemoveTagHelperDescriptors { get; } + + /// + /// Generates s if is + /// true, otherwise s are generated. + /// + /// + /// The responsible for this . + /// + /// A instance that contains information about + /// the current chunk generation process. + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + if (RemoveTagHelperDescriptors) + { + context.ChunkTreeBuilder.AddRemoveTagHelperChunk(LookupText, target); + } + else + { + context.ChunkTreeBuilder.AddAddTagHelperChunk(LookupText, target); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AttributeBlockChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AttributeBlockChunkGenerator.cs new file mode 100644 index 0000000000..9469fb35b1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/AttributeBlockChunkGenerator.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class AttributeBlockChunkGenerator : ParentChunkGenerator + { + public AttributeBlockChunkGenerator(string name, LocationTagged prefix, LocationTagged suffix) + { + Name = name; + Prefix = prefix; + Suffix = suffix; + } + + public string Name { get; } + + public LocationTagged Prefix { get; } + + public LocationTagged Suffix { get; } + + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + var chunk = context.ChunkTreeBuilder.StartParentChunk(target); + + chunk.Attribute = Name; + chunk.Prefix = Prefix; + chunk.Suffix = Suffix; + } + + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "Attr:{0},{1:F},{2:F}", Name, Prefix, Suffix); + } + + public override bool Equals(object obj) + { + var other = obj as AttributeBlockChunkGenerator; + return other != null && + string.Equals(other.Name, Name, StringComparison.Ordinal) && + Equals(other.Prefix, Prefix) && + Equals(other.Suffix, Suffix); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Name, StringComparer.Ordinal); + hashCodeCombiner.Add(Prefix); + hashCodeCombiner.Add(Suffix); + + return hashCodeCombiner; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ChunkGeneratorContext.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ChunkGeneratorContext.cs new file mode 100644 index 0000000000..95ff4493f8 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ChunkGeneratorContext.cs @@ -0,0 +1,44 @@ +// 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.AspNet.Razor.Chunks.Generators +{ + public class ChunkGeneratorContext + { + protected ChunkGeneratorContext(ChunkGeneratorContext context) + : this( + context.Host, + context.ClassName, + context.RootNamespace, + context.SourceFile, + // True because we're pulling from the provided context's source file. + shouldGenerateLinePragmas: true) + { + ChunkTreeBuilder = context.ChunkTreeBuilder; + } + + public ChunkGeneratorContext( + RazorEngineHost host, + string className, + string rootNamespace, + string sourceFile, + bool shouldGenerateLinePragmas) + { + ChunkTreeBuilder = new ChunkTreeBuilder(); + Host = host; + SourceFile = shouldGenerateLinePragmas ? sourceFile : null; + RootNamespace = rootNamespace; + ClassName = className; + } + + public string SourceFile { get; internal set; } + + public string RootNamespace { get; } + + public string ClassName { get; } + + public RazorEngineHost Host { get; } + + public ChunkTreeBuilder ChunkTreeBuilder { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/DynamicAttributeBlockChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/DynamicAttributeBlockChunkGenerator.cs new file mode 100644 index 0000000000..05879b6da2 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/DynamicAttributeBlockChunkGenerator.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class DynamicAttributeBlockChunkGenerator : ParentChunkGenerator + { + public DynamicAttributeBlockChunkGenerator(LocationTagged prefix, int offset, int line, int col) + : this(prefix, new SourceLocation(offset, line, col)) + { + } + + public DynamicAttributeBlockChunkGenerator(LocationTagged prefix, SourceLocation valueStart) + { + Prefix = prefix; + ValueStart = valueStart; + } + + public LocationTagged Prefix { get; } + + public SourceLocation ValueStart { get; } + + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + var chunk = context.ChunkTreeBuilder.StartParentChunk(target); + chunk.Start = ValueStart; + chunk.Prefix = Prefix; + } + + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "DynAttr:{0:F}", Prefix); + } + + public override bool Equals(object obj) + { + var other = obj as DynamicAttributeBlockChunkGenerator; + return other != null && + Equals(other.Prefix, Prefix); + } + + public override int GetHashCode() + { + return Prefix == null ? 0 : Prefix.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ExpressionChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ExpressionChunkGenerator.cs new file mode 100644 index 0000000000..adfee65da3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ExpressionChunkGenerator.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class ExpressionChunkGenerator : ISpanChunkGenerator, IParentChunkGenerator + { + private static readonly int TypeHashCode = typeof(ExpressionChunkGenerator).GetHashCode(); + + public void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.StartParentChunk(target); + } + + public void GenerateChunk(Span target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.AddExpressionChunk(target.Content, target); + } + + public void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + return "Expr"; + } + + public override bool Equals(object obj) + { + return obj != null && + GetType() == obj.GetType(); + } + + public override int GetHashCode() + { + return TypeHashCode; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/IParentChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/IParentChunkGenerator.cs new file mode 100644 index 0000000000..d4a300ba82 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/IParentChunkGenerator.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public interface IParentChunkGenerator + { + void GenerateStartParentChunk(Block target, ChunkGeneratorContext context); + void GenerateEndParentChunk(Block target, ChunkGeneratorContext context); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ISpanChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ISpanChunkGenerator.cs new file mode 100644 index 0000000000..48fcf0faad --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ISpanChunkGenerator.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public interface ISpanChunkGenerator + { + void GenerateChunk(Span target, ChunkGeneratorContext context); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/LiteralAttributeChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/LiteralAttributeChunkGenerator.cs new file mode 100644 index 0000000000..71b1b93ef6 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/LiteralAttributeChunkGenerator.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class LiteralAttributeChunkGenerator : SpanChunkGenerator + { + public LiteralAttributeChunkGenerator( + LocationTagged prefix, + LocationTagged valueGenerator) + { + Prefix = prefix; + ValueGenerator = valueGenerator; + } + + public LiteralAttributeChunkGenerator(LocationTagged prefix, LocationTagged value) + { + Prefix = prefix; + Value = value; + } + + public LocationTagged Prefix { get; } + + public LocationTagged Value { get; } + + public LocationTagged ValueGenerator { get; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + var chunk = context.ChunkTreeBuilder.StartParentChunk(target); + chunk.Prefix = Prefix; + chunk.Value = Value; + + if (ValueGenerator != null) + { + chunk.ValueLocation = ValueGenerator.Location; + + ValueGenerator.Value.GenerateChunk(target, context); + + chunk.ValueLocation = ValueGenerator.Location; + } + + context.ChunkTreeBuilder.EndParentChunk(); + } + + public override string ToString() + { + if (ValueGenerator == null) + { + return string.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},{1:F}", Prefix, Value); + } + else + { + return string.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},", Prefix, ValueGenerator); + } + } + + public override bool Equals(object obj) + { + var other = obj as LiteralAttributeChunkGenerator; + return other != null && + Equals(other.Prefix, Prefix) && + Equals(other.Value, Value) && + Equals(other.ValueGenerator, ValueGenerator); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + + hashCodeCombiner.Add(Prefix); + hashCodeCombiner.Add(Value); + hashCodeCombiner.Add(ValueGenerator); + + return hashCodeCombiner; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/MarkupChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/MarkupChunkGenerator.cs new file mode 100644 index 0000000000..26267b29be --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/MarkupChunkGenerator.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class MarkupChunkGenerator : SpanChunkGenerator + { + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.AddLiteralChunk(target.Content, target); + } + + public override string ToString() + { + return "Markup"; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ParentChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ParentChunkGenerator.cs new file mode 100644 index 0000000000..54629c6260 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/ParentChunkGenerator.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public abstract class ParentChunkGenerator : IParentChunkGenerator + { + private static readonly int TypeHashCode = typeof(ParentChunkGenerator).GetHashCode(); + + [SuppressMessage( + "Microsoft.Security", + "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "This class has no instance state")] + public static readonly IParentChunkGenerator Null = new NullParentChunkGenerator(); + + public virtual void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + } + + public virtual void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + } + + public override bool Equals(object obj) + { + return obj != null && + GetType() == obj.GetType(); + } + + public override int GetHashCode() + { + return TypeHashCode; + } + + private class NullParentChunkGenerator : IParentChunkGenerator + { + public void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + } + + public void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + } + + public override string ToString() + { + return "None"; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/RazorChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/RazorChunkGenerator.cs new file mode 100644 index 0000000000..a6d057f3b1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/RazorChunkGenerator.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class RazorChunkGenerator : ParserVisitor + { + private ChunkGeneratorContext _context; + + public RazorChunkGenerator( + string className, + string rootNamespaceName, + string sourceFileName, + RazorEngineHost host) + { + if (rootNamespaceName == null) + { + throw new ArgumentNullException(nameof(rootNamespaceName)); + } + + if (host == null) + { + throw new ArgumentNullException(nameof(host)); + } + + if (string.IsNullOrEmpty(className)) + { + throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, nameof(className)); + } + + ClassName = className; + RootNamespaceName = rootNamespaceName; + SourceFileName = sourceFileName; + GenerateLinePragmas = string.IsNullOrEmpty(SourceFileName) ? false : true; + Host = host; + } + + // Data pulled from constructor + public string ClassName { get; private set; } + public string RootNamespaceName { get; private set; } + public string SourceFileName { get; private set; } + public RazorEngineHost Host { get; private set; } + + // Generation settings + public bool GenerateLinePragmas { get; set; } + public bool DesignTimeMode { get; set; } + + public ChunkGeneratorContext Context + { + get + { + EnsureContextInitialized(); + return _context; + } + } + + public override void VisitStartBlock(Block block) + { + block.ChunkGenerator.GenerateStartParentChunk(block, Context); + } + + public override void VisitEndBlock(Block block) + { + block.ChunkGenerator.GenerateEndParentChunk(block, Context); + } + + public override void VisitSpan(Span span) + { + span.ChunkGenerator.GenerateChunk(span, Context); + } + + private void EnsureContextInitialized() + { + if (_context == null) + { + _context = new ChunkGeneratorContext(Host, + ClassName, + RootNamespaceName, + SourceFileName, + GenerateLinePragmas); + Initialize(_context); + } + } + + protected virtual void Initialize(ChunkGeneratorContext context) + { + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/RazorCommentChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/RazorCommentChunkGenerator.cs new file mode 100644 index 0000000000..ba0d202987 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/RazorCommentChunkGenerator.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Razor.Chunks.Generators +{ + public class RazorCommentChunkGenerator : ParentChunkGenerator + { + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SectionChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SectionChunkGenerator.cs new file mode 100644 index 0000000000..4ed679c982 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SectionChunkGenerator.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class SectionChunkGenerator : ParentChunkGenerator + { + public SectionChunkGenerator(string sectionName) + { + SectionName = sectionName; + } + + public string SectionName { get; } + + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + var chunk = context.ChunkTreeBuilder.StartParentChunk(target); + + chunk.Name = SectionName; + } + + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.EndParentChunk(); + } + + public override bool Equals(object obj) + { + var other = obj as SectionChunkGenerator; + return base.Equals(other) && + string.Equals(SectionName, other.SectionName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return SectionName == null ? 0 : StringComparer.Ordinal.GetHashCode(SectionName); + } + + public override string ToString() + { + return "Section:" + SectionName; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SetBaseTypeChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SetBaseTypeChunkGenerator.cs new file mode 100644 index 0000000000..4b4384ea38 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SetBaseTypeChunkGenerator.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class SetBaseTypeChunkGenerator : SpanChunkGenerator + { + public SetBaseTypeChunkGenerator(string baseType) + { + BaseType = baseType; + } + + public string BaseType { get; } + + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.AddSetBaseTypeChunk(BaseType, target); + } + + public override string ToString() + { + return "Base:" + BaseType; + } + + public override bool Equals(object obj) + { + var other = obj as SetBaseTypeChunkGenerator; + return other != null && + string.Equals(BaseType, other.BaseType, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return BaseType.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SpanChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SpanChunkGenerator.cs new file mode 100644 index 0000000000..225d33c8c1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/SpanChunkGenerator.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public abstract class SpanChunkGenerator : ISpanChunkGenerator + { + private static readonly int TypeHashCode = typeof(SpanChunkGenerator).GetHashCode(); + + [SuppressMessage( + "Microsoft.Security", + "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", + Justification = "This class has no instance state")] + public static readonly ISpanChunkGenerator Null = new NullSpanChunkGenerator(); + + public virtual void GenerateChunk(Span target, ChunkGeneratorContext context) + { + } + + public override bool Equals(object obj) + { + return obj != null && + GetType() == obj.GetType(); + } + + public override int GetHashCode() + { + return TypeHashCode; + } + + private class NullSpanChunkGenerator : ISpanChunkGenerator + { + public void GenerateChunk(Span target, ChunkGeneratorContext context) + { + } + + public override string ToString() + { + return "None"; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/StatementChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/StatementChunkGenerator.cs new file mode 100644 index 0000000000..7504fefc7e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/StatementChunkGenerator.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class StatementChunkGenerator : SpanChunkGenerator + { + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.AddStatementChunk(target.Content, target); + } + + public override string ToString() + { + return "Stmt"; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TagHelperChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TagHelperChunkGenerator.cs new file mode 100644 index 0000000000..fbe2f28b71 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TagHelperChunkGenerator.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Parser.TagHelpers; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + /// + /// A that is responsible for generating valid s. + /// + public class TagHelperChunkGenerator : ParentChunkGenerator + { + private IEnumerable _tagHelperDescriptors; + + /// + /// Instantiates a new . + /// + /// + /// s associated with the current HTML tag. + /// + public TagHelperChunkGenerator(IEnumerable tagHelperDescriptors) + { + _tagHelperDescriptors = tagHelperDescriptors; + } + + /// + /// Starts the generation of a . + /// + /// + /// The responsible for this . + /// + /// A instance that contains information about + /// the current chunk generation process. + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + var tagHelperBlock = target as TagHelperBlock; + + Debug.Assert( + tagHelperBlock != null, + $"A {nameof(TagHelperChunkGenerator)} must only be used with {nameof(TagHelperBlock)}s."); + + var attributes = new List>(); + + // We need to create a chunk generator to create chunks for each of the attributes. + var chunkGenerator = context.Host.CreateChunkGenerator( + context.ClassName, + context.RootNamespace, + context.SourceFile); + + foreach (var attribute in tagHelperBlock.Attributes) + { + ParentChunk attributeChunkValue = null; + + if (attribute.Value != null) + { + // Populates the chunk tree with chunks associated with attributes + attribute.Value.Accept(chunkGenerator); + + var chunks = chunkGenerator.Context.ChunkTreeBuilder.ChunkTree.Chunks; + var first = chunks.FirstOrDefault(); + + attributeChunkValue = new ParentChunk + { + Association = first?.Association, + Children = chunks, + Start = first == null ? SourceLocation.Zero : first.Start + }; + } + + attributes.Add(new KeyValuePair(attribute.Key, attributeChunkValue)); + + // Reset the chunk tree builder so we can build a new one for the next attribute + chunkGenerator.Context.ChunkTreeBuilder = new ChunkTreeBuilder(); + } + + var unprefixedTagName = tagHelperBlock.TagName.Substring(_tagHelperDescriptors.First().Prefix.Length); + + context.ChunkTreeBuilder.StartParentChunk( + new TagHelperChunk( + unprefixedTagName, + tagHelperBlock.TagMode, + attributes, + _tagHelperDescriptors), + target, + topLevel: false); + } + + /// + /// Ends the generation of a capturing all previously visited children + /// since the method was called. + /// + /// + /// The responsible for this . + /// + /// A instance that contains information about + /// the current chunk generation process. + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.EndParentChunk(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TagHelperPrefixDirectiveChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TagHelperPrefixDirectiveChunkGenerator.cs new file mode 100644 index 0000000000..dd00b86540 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TagHelperPrefixDirectiveChunkGenerator.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + /// + /// A responsible for generating + /// s. + /// + public class TagHelperPrefixDirectiveChunkGenerator : SpanChunkGenerator + { + /// + /// Instantiates a new . + /// + /// + /// Text used as a required prefix when matching HTML. + /// + public TagHelperPrefixDirectiveChunkGenerator(string prefix) + { + Prefix = prefix; + } + + /// + /// Text used as a required prefix when matching HTML. + /// + public string Prefix { get; } + + /// + /// Generates s. + /// + /// + /// The responsible for this . + /// + /// A instance that contains information about + /// the current chunk generation process. + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.AddTagHelperPrefixDirectiveChunk(Prefix, target); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TemplateBlockChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TemplateBlockChunkGenerator.cs new file mode 100644 index 0000000000..6874c7b37d --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TemplateBlockChunkGenerator.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class TemplateBlockChunkGenerator : ParentChunkGenerator + { + public override void GenerateStartParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.StartParentChunk(target); + } + + public override void GenerateEndParentChunk(Block target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.EndParentChunk(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TypeMemberChunkGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TypeMemberChunkGenerator.cs new file mode 100644 index 0000000000..cfda1aa1ed --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/Generators/TypeMemberChunkGenerator.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Chunks.Generators +{ + public class TypeMemberChunkGenerator : SpanChunkGenerator + { + public override void GenerateChunk(Span target, ChunkGeneratorContext context) + { + context.ChunkTreeBuilder.AddTypeMemberChunk(target.Content, target); + } + + public override string ToString() + { + return "TypeMember"; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/LiteralChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/LiteralChunk.cs new file mode 100644 index 0000000000..756e131ee2 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/LiteralChunk.cs @@ -0,0 +1,15 @@ +// 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.AspNet.Razor.Chunks +{ + public class LiteralChunk : Chunk + { + public string Text { get; set; } + + public override string ToString() + { + return Start + " = " + Text; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/LiteralCodeAttributeChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/LiteralCodeAttributeChunk.cs new file mode 100644 index 0000000000..dce9c0d5c6 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/LiteralCodeAttributeChunk.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class LiteralCodeAttributeChunk : ParentChunk + { + public string Code { get; set; } + public LocationTagged Prefix { get; set; } + public LocationTagged Value { get; set; } + public SourceLocation ValueLocation { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ParentChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ParentChunk.cs new file mode 100644 index 0000000000..da88454827 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/ParentChunk.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Chunks +{ + public class ParentChunk : Chunk + { + public ParentChunk() + { + Children = new List(); + } + + public IList Children { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/RemoveTagHelperChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/RemoveTagHelperChunk.cs new file mode 100644 index 0000000000..6de311905b --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/RemoveTagHelperChunk.cs @@ -0,0 +1,18 @@ +// 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.AspNet.Razor.Chunks +{ + /// + /// A used to look up s that should be ignored + /// within the Razor page. + /// + public class RemoveTagHelperChunk : Chunk + { + /// + /// Text used to look up s that should be ignored within the Razor + /// page. + /// + public string LookupText { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/SectionChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/SectionChunk.cs new file mode 100644 index 0000000000..6262484a22 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/SectionChunk.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Razor.Chunks +{ + public class SectionChunk : ParentChunk + { + public string Name { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/SetBaseTypeChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/SetBaseTypeChunk.cs new file mode 100644 index 0000000000..06d9e270d7 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/SetBaseTypeChunk.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Razor.Chunks +{ + public class SetBaseTypeChunk : Chunk + { + public string TypeName { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/StatementChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/StatementChunk.cs new file mode 100644 index 0000000000..49e40dc6fa --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/StatementChunk.cs @@ -0,0 +1,15 @@ +// 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.AspNet.Razor.Chunks +{ + public class StatementChunk : Chunk + { + public string Code { get; set; } + + public override string ToString() + { + return Start + " = " + Code; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TagHelperChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TagHelperChunk.cs new file mode 100644 index 0000000000..3e0a700685 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TagHelperChunk.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Razor.Chunks +{ + /// + /// A that represents a special HTML tag. + /// + public class TagHelperChunk : ParentChunk + { + /// + /// Instantiates a new . + /// + /// The tag name associated with the tag helpers HTML element. + /// HTML syntax of the element in the Razor source. + /// The attributes associated with the tag helpers HTML element. + /// + /// The s associated with this tag helpers HTML element. + /// + public TagHelperChunk( + string tagName, + TagMode tagMode, + IList> attributes, + IEnumerable descriptors) + { + TagName = tagName; + TagMode = tagMode; + Attributes = attributes; + Descriptors = descriptors; + } + + /// + /// The HTML attributes. + /// + /// + /// These attributes are => so attribute values can consist + /// of all sorts of Razor specific pieces. + /// + public IList> Attributes { get; set; } + + /// + /// The s that are associated with the tag helpers HTML element. + /// + public IEnumerable Descriptors { get; set; } + + /// + /// The HTML tag name. + /// + public string TagName { get; set; } + + /// + /// Gets the HTML syntax of the element in the Razor source. + /// + public TagMode TagMode { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TagHelperPrefixDirectiveChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TagHelperPrefixDirectiveChunk.cs new file mode 100644 index 0000000000..a935f2b60a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TagHelperPrefixDirectiveChunk.cs @@ -0,0 +1,17 @@ +// 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.AspNet.Razor.Chunks +{ + /// + /// A for the @tagHelperPrefix directive. + /// + public class TagHelperPrefixDirectiveChunk : Chunk + { + /// + /// Text used as a required prefix when matching HTML start and end tags in the Razor source to available + /// tag helpers. + /// + public string Prefix { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TemplateChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TemplateChunk.cs new file mode 100644 index 0000000000..4089f96245 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TemplateChunk.cs @@ -0,0 +1,9 @@ +// 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.AspNet.Razor.Chunks +{ + public class TemplateChunk : ParentChunk + { + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TypeMemberChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TypeMemberChunk.cs new file mode 100644 index 0000000000..394ce2a399 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/TypeMemberChunk.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Razor.Chunks +{ + public class TypeMemberChunk : Chunk + { + public string Code { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Chunks/UsingChunk.cs b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/UsingChunk.cs new file mode 100644 index 0000000000..60902ac7c1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Chunks/UsingChunk.cs @@ -0,0 +1,10 @@ +// 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.AspNet.Razor.Chunks +{ + public class UsingChunk : Chunk + { + public string Namespace { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeGenerator.cs new file mode 100644 index 0000000000..5d46d4f23e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeGenerator.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.CodeGenerators.Visitors; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class CSharpCodeGenerator : CodeGenerator + { + // See http://msdn.microsoft.com/en-us/library/system.codedom.codechecksumpragma.checksumalgorithmid.aspx + private const string Sha1AlgorithmId = "{ff1816ec-aa5e-4d10-87f7-6f4963833460}"; + private const int DisableAsyncWarning = 1998; + + public CSharpCodeGenerator(CodeGeneratorContext context) + : base(context) + { + } + + private ChunkTree Tree { get { return Context.ChunkTreeBuilder.ChunkTree; } } + public RazorEngineHost Host { get { return Context.Host; } } + + /// + /// Protected for testing. + /// + /// A new instance of . + protected virtual CSharpCodeWriter CreateCodeWriter() + { + return new CSharpCodeWriter(); + } + + public override CodeGeneratorResult Generate() + { + var writer = CreateCodeWriter(); + + if (!Host.DesignTimeMode && !string.IsNullOrEmpty(Context.Checksum)) + { + writer.Write("#pragma checksum \"") + .Write(Context.SourceFile) + .Write("\" \"") + .Write(Sha1AlgorithmId) + .Write("\" \"") + .Write(Context.Checksum) + .WriteLine("\""); + } + + using (writer.BuildNamespace(Context.RootNamespace)) + { + // Write out using directives + AddImports(Tree, writer, Host.NamespaceImports); + // Separate the usings and the class + writer.WriteLine(); + + using (BuildClassDeclaration(writer)) + { + if (Host.DesignTimeMode) + { + writer.WriteLine("private static object @__o;"); + } + + var csharpCodeVisitor = CreateCSharpCodeVisitor(writer, Context); + + new CSharpTypeMemberVisitor(csharpCodeVisitor, writer, Context).Accept(Tree.Chunks); + CreateCSharpDesignTimeCodeVisitor(csharpCodeVisitor, writer, Context) + .AcceptTree(Tree); + new CSharpTagHelperFieldDeclarationVisitor(writer, Context).Accept(Tree.Chunks); + + BuildConstructor(writer); + + // Add space in-between constructor and method body + writer.WriteLine(); + + using (writer.BuildDisableWarningScope(DisableAsyncWarning)) + { + using (writer.BuildMethodDeclaration("public override async", "Task", Host.GeneratedClassContext.ExecuteMethodName)) + { + new CSharpTagHelperRunnerInitializationVisitor(writer, Context).Accept(Tree.Chunks); + csharpCodeVisitor.Accept(Tree.Chunks); + } + } + } + } + + return new CodeGeneratorResult(writer.GenerateCode(), writer.LineMappingManager.Mappings); + } + + protected virtual CSharpCodeVisitor CreateCSharpCodeVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new CSharpCodeVisitor(writer, context); + } + + protected virtual CSharpDesignTimeCodeVisitor CreateCSharpDesignTimeCodeVisitor( + CSharpCodeVisitor csharpCodeVisitor, + CSharpCodeWriter writer, + CodeGeneratorContext context) + { + if (csharpCodeVisitor == null) + { + throw new ArgumentNullException(nameof(csharpCodeVisitor)); + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new CSharpDesignTimeCodeVisitor(csharpCodeVisitor, writer, context); + } + + protected virtual CSharpCodeWritingScope BuildClassDeclaration(CSharpCodeWriter writer) + { + var baseTypeVisitor = new CSharpBaseTypeVisitor(writer, Context); + baseTypeVisitor.Accept(Tree.Chunks); + + var baseType = baseTypeVisitor.CurrentBaseType ?? Host.DefaultBaseClass; + + var baseTypes = string.IsNullOrEmpty(baseType) ? Enumerable.Empty() : new string[] { baseType }; + + return writer.BuildClassDeclaration("public", Context.ClassName, baseTypes); + } + + protected virtual void BuildConstructor(CSharpCodeWriter writer) + { + writer.WriteLineHiddenDirective(); + using (writer.BuildConstructor(Context.ClassName)) + { + // Any constructor based logic that we need to add? + }; + } + + private void AddImports(ChunkTree chunkTree, CSharpCodeWriter writer, IEnumerable defaultImports) + { + // Write out using directives + var usingVisitor = new CSharpUsingVisitor(writer, Context); + foreach (Chunk chunk in Tree.Chunks) + { + usingVisitor.Accept(chunk); + } + + defaultImports = defaultImports.Except(usingVisitor.ImportedUsings); + + foreach (string import in defaultImports) + { + writer.WriteUsing(import); + } + + var taskNamespace = typeof(Task).Namespace; + + // We need to add the task namespace but ONLY if it hasn't been added by the default imports or using imports yet. + if (!defaultImports.Contains(taskNamespace) && !usingVisitor.ImportedUsings.Contains(taskNamespace)) + { + writer.WriteUsing(taskNamespace); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeWriter.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeWriter.cs new file mode 100644 index 0000000000..78c35736a7 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeWriter.cs @@ -0,0 +1,513 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class CSharpCodeWriter : CodeWriter + { + private const string InstanceMethodFormat = "{0}.{1}"; + + public CSharpCodeWriter() + { + LineMappingManager = new LineMappingManager(); + } + + public LineMappingManager LineMappingManager { get; private set; } + + public new CSharpCodeWriter Write(string data) + { + return (CSharpCodeWriter)base.Write(data); + } + + public new CSharpCodeWriter Indent(int size) + { + return (CSharpCodeWriter)base.Indent(size); + } + + public new CSharpCodeWriter ResetIndent() + { + return (CSharpCodeWriter)base.ResetIndent(); + } + + public new CSharpCodeWriter SetIndent(int size) + { + return (CSharpCodeWriter)base.SetIndent(size); + } + + public new CSharpCodeWriter IncreaseIndent(int size) + { + return (CSharpCodeWriter)base.IncreaseIndent(size); + } + + public new CSharpCodeWriter DecreaseIndent(int size) + { + return (CSharpCodeWriter)base.DecreaseIndent(size); + } + + public new CSharpCodeWriter WriteLine(string data) + { + return (CSharpCodeWriter)base.WriteLine(data); + } + + public new CSharpCodeWriter WriteLine() + { + return (CSharpCodeWriter)base.WriteLine(); + } + + public CSharpCodeWriter WriteVariableDeclaration(string type, string name, string value) + { + Write(type).Write(" ").Write(name); + if (!string.IsNullOrEmpty(value)) + { + Write(" = ").Write(value); + } + else + { + Write(" = null"); + } + + WriteLine(";"); + + return this; + } + + public CSharpCodeWriter WriteComment(string comment) + { + return Write("// ").WriteLine(comment); + } + + public CSharpCodeWriter WriteBooleanLiteral(bool value) + { + return Write(value.ToString().ToLowerInvariant()); + } + + public CSharpCodeWriter WriteStartAssignment(string name) + { + return Write(name).Write(" = "); + } + + public CSharpCodeWriter WriteParameterSeparator() + { + return Write(", "); + } + + public CSharpCodeWriter WriteStartNewObject(string typeName) + { + return Write("new ").Write(typeName).Write("("); + } + + public CSharpCodeWriter WriteLocationTaggedString(LocationTagged value) + { + WriteStringLiteral(value.Value); + WriteParameterSeparator(); + Write(value.Location.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)); + + return this; + } + + public CSharpCodeWriter WriteStringLiteral(string literal) + { + if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1) + { + WriteVerbatimStringLiteral(literal); + } + else + { + WriteCStyleStringLiteral(literal); + } + + return this; + } + + public CSharpCodeWriter WriteLineHiddenDirective() + { + return WriteLine("#line hidden"); + } + + public CSharpCodeWriter WritePragma(string value) + { + return Write("#pragma ").WriteLine(value); + } + + public CSharpCodeWriter WriteUsing(string name) + { + return WriteUsing(name, endLine: true); + } + + public CSharpCodeWriter WriteUsing(string name, bool endLine) + { + Write(string.Format("using {0}", name)); + + if (endLine) + { + WriteLine(";"); + } + + return this; + } + + public CSharpCodeWriter WriteLineDefaultDirective() + { + return WriteLine("#line default"); + } + + public CSharpCodeWriter WriteStartReturn() + { + return Write("return "); + } + + public CSharpCodeWriter WriteReturn(string value) + { + return WriteReturn(value, endLine: true); + } + + public CSharpCodeWriter WriteReturn(string value, bool endLine) + { + Write("return ").Write(value); + + if (endLine) + { + Write(";"); + } + + return WriteLine(); + } + + /// + /// Writes a #line pragma directive for the line number at the specified . + /// + /// The location to generate the line pragma for. + /// The file to generate the line pragma for. + /// The current instance of . + public CSharpCodeWriter WriteLineNumberDirective(SourceLocation location, string file) + { + if (location.FilePath != null) + { + file = location.FilePath; + } + + if (!string.IsNullOrEmpty(LastWrite) && + !LastWrite.EndsWith(NewLine, StringComparison.Ordinal)) + { + WriteLine(); + } + + var lineNumberAsString = (location.LineIndex + 1).ToString(CultureInfo.InvariantCulture); + return Write("#line ").Write(lineNumberAsString).Write(" \"").Write(file).WriteLine("\""); + } + + public CSharpCodeWriter WriteStartMethodInvocation(string methodName) + { + return WriteStartMethodInvocation(methodName, new string[0]); + } + + public CSharpCodeWriter WriteStartMethodInvocation(string methodName, params string[] genericArguments) + { + Write(methodName); + + if (genericArguments.Length > 0) + { + Write("<").Write(string.Join(", ", genericArguments)).Write(">"); + } + + return Write("("); + } + + public CSharpCodeWriter WriteEndMethodInvocation() + { + return WriteEndMethodInvocation(endLine: true); + } + public CSharpCodeWriter WriteEndMethodInvocation(bool endLine) + { + Write(")"); + if (endLine) + { + WriteLine(";"); + } + + return this; + } + + // Writes a method invocation for the given instance name. + public CSharpCodeWriter WriteInstanceMethodInvocation(string instanceName, + string methodName, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteInstanceMethodInvocation(instanceName, methodName, endLine: true, parameters: parameters); + } + + // Writes a method invocation for the given instance name. + public CSharpCodeWriter WriteInstanceMethodInvocation(string instanceName, + string methodName, + bool endLine, + params string[] parameters) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteMethodInvocation( + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName), + endLine, + parameters); + } + + public CSharpCodeWriter WriteStartInstanceMethodInvocation(string instanceName, + string methodName) + { + if (instanceName == null) + { + throw new ArgumentNullException(nameof(instanceName)); + } + + if (methodName == null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + return WriteStartMethodInvocation( + string.Format(CultureInfo.InvariantCulture, InstanceMethodFormat, instanceName, methodName)); + } + + public CSharpCodeWriter WriteMethodInvocation(string methodName, params string[] parameters) + { + return WriteMethodInvocation(methodName, endLine: true, parameters: parameters); + } + + public CSharpCodeWriter WriteMethodInvocation(string methodName, bool endLine, params string[] parameters) + { + return WriteStartMethodInvocation(methodName).Write(string.Join(", ", parameters)).WriteEndMethodInvocation(endLine); + } + + public CSharpDisableWarningScope BuildDisableWarningScope(int warning) + { + return new CSharpDisableWarningScope(this, warning); + } + + public CSharpCodeWritingScope BuildScope() + { + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildLambda(bool endLine, params string[] parameterNames) + { + return BuildLambda(endLine, async: false, parameterNames: parameterNames); + } + + public CSharpCodeWritingScope BuildAsyncLambda(bool endLine, params string[] parameterNames) + { + return BuildLambda(endLine, async: true, parameterNames: parameterNames); + } + + private CSharpCodeWritingScope BuildLambda(bool endLine, bool async, string[] parameterNames) + { + if (async) + { + Write("async"); + } + + Write("(").Write(string.Join(", ", parameterNames)).Write(") => "); + + var scope = new CSharpCodeWritingScope(this); + + if (endLine) + { + // End the lambda with a semicolon + scope.OnClose += () => + { + WriteLine(";"); + }; + } + + return scope; + } + + public CSharpCodeWritingScope BuildNamespace(string name) + { + Write("namespace ").WriteLine(name); + + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildClassDeclaration(string accessibility, string name) + { + return BuildClassDeclaration(accessibility, name, Enumerable.Empty()); + } + + public CSharpCodeWritingScope BuildClassDeclaration(string accessibility, string name, string baseType) + { + return BuildClassDeclaration(accessibility, name, new string[] { baseType }); + } + + public CSharpCodeWritingScope BuildClassDeclaration(string accessibility, string name, IEnumerable baseTypes) + { + Write(accessibility).Write(" class ").Write(name); + + if (baseTypes.Count() > 0) + { + Write(" : "); + Write(string.Join(", ", baseTypes)); + } + + WriteLine(); + + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildConstructor(string name) + { + return BuildConstructor("public", name); + } + + public CSharpCodeWritingScope BuildConstructor(string accessibility, string name) + { + return BuildConstructor(accessibility, name, Enumerable.Empty>()); + } + + public CSharpCodeWritingScope BuildConstructor(string accessibility, string name, IEnumerable> parameters) + { + Write(accessibility).Write(" ").Write(name).Write("(").Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))).WriteLine(")"); + + return new CSharpCodeWritingScope(this); + } + + public CSharpCodeWritingScope BuildMethodDeclaration(string accessibility, string returnType, string name) + { + return BuildMethodDeclaration(accessibility, returnType, name, Enumerable.Empty>()); + } + + public CSharpCodeWritingScope BuildMethodDeclaration(string accessibility, string returnType, string name, IEnumerable> parameters) + { + Write(accessibility).Write(" ").Write(returnType).Write(" ").Write(name).Write("(").Write(string.Join(", ", parameters.Select(p => p.Key + " " + p.Value))).WriteLine(")"); + + return new CSharpCodeWritingScope(this); + } + + // TODO: Do I need to look at the document content to determine its mapping length? + public CSharpLineMappingWriter BuildLineMapping(SourceLocation documentLocation, int contentLength, string sourceFilename) + { + return new CSharpLineMappingWriter(this, documentLocation, contentLength, sourceFilename); + } + + private void WriteVerbatimStringLiteral(string literal) + { + Write("@\""); + + foreach (char c in literal) + { + if (c == '\"') + { + Write("\"\""); + } + else + { + Write(c.ToString()); + } + } + + Write("\""); + } + + private void WriteCStyleStringLiteral(string literal) + { + // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM + Write("\""); + for (int i = 0; i < literal.Length; i++) + { + switch (literal[i]) + { + case '\r': + Write("\\r"); + break; + case '\t': + Write("\\t"); + break; + case '\"': + Write("\\\""); + break; + case '\'': + Write("\\\'"); + break; + case '\\': + Write("\\\\"); + break; + case '\0': + Write("\\\0"); + break; + case '\n': + Write("\\n"); + break; + case '\u2028': + case '\u2029': + Write("\\u"); + Write(((int)literal[i]).ToString("X4", CultureInfo.InvariantCulture)); + break; + default: + Write(literal[i].ToString()); + break; + } + if (i > 0 && i % 80 == 0) + { + // If current character is a high surrogate and the following + // character is a low surrogate, don't break them. + // Otherwise when we write the string to a file, we might lose + // the characters. + if (Char.IsHighSurrogate(literal[i]) + && (i < literal.Length - 1) + && Char.IsLowSurrogate(literal[i + 1])) + { + Write(literal[++i].ToString()); + } + + Write("\" +"); + Write(NewLine); + Write("\""); + } + } + Write("\""); + } + + public CSharpCodeWriter WriteStartInstrumentationContext( + ChunkGeneratorContext context, + SyntaxTreeNode syntaxNode, + bool isLiteral) + { + WriteStartMethodInvocation(context.Host.GeneratedClassContext.BeginContextMethodName); + Write(syntaxNode.Start.AbsoluteIndex.ToString(CultureInfo.InvariantCulture)); + WriteParameterSeparator(); + Write(syntaxNode.Length.ToString(CultureInfo.InvariantCulture)); + WriteParameterSeparator(); + Write(isLiteral ? "true" : "false"); + return WriteEndMethodInvocation(); + } + + public CSharpCodeWriter WriteEndInstrumentationContext(ChunkGeneratorContext context) + { + return WriteMethodInvocation(context.Host.GeneratedClassContext.EndContextMethodName); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeWritingScope.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeWritingScope.cs new file mode 100644 index 0000000000..2616c149b0 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpCodeWritingScope.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public struct CSharpCodeWritingScope : IDisposable + { + private CodeWriter _writer; + private bool _autoSpace; + private int _tabSize; + private int _startIndent; + + public CSharpCodeWritingScope(CodeWriter writer) : this(writer, true) { } + public CSharpCodeWritingScope(CodeWriter writer, int tabSize) : this(writer, tabSize, true) { } + // TODO: Make indents (tabs) environment specific + public CSharpCodeWritingScope(CodeWriter writer, bool autoSpace) : this(writer, 4, autoSpace) { } + public CSharpCodeWritingScope(CodeWriter writer, int tabSize, bool autoSpace) + { + _writer = writer; + _autoSpace = true; + _tabSize = tabSize; + _startIndent = -1; // Set in WriteStartScope + + OnClose = () => { }; + + WriteStartScope(); + } + + public Action OnClose; + + public void Dispose() + { + WriteEndScope(); + OnClose(); + } + + private void WriteStartScope() + { + TryAutoSpace(" "); + + _writer.WriteLine("{").IncreaseIndent(_tabSize); + _startIndent = _writer.CurrentIndent; + } + + private void WriteEndScope() + { + TryAutoSpace(_writer.NewLine); + + // Ensure the scope hasn't been modified + if (_writer.CurrentIndent == _startIndent) + { + _writer.DecreaseIndent(_tabSize); + } + + _writer.WriteLine("}"); + } + + private void TryAutoSpace(string spaceCharacter) + { + if (_autoSpace && _writer.LastWrite.Length > 0 && !Char.IsWhiteSpace(_writer.LastWrite.Last())) + { + _writer.Write(spaceCharacter); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpDisableWarningScope.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpDisableWarningScope.cs new file mode 100644 index 0000000000..3c319c5f12 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpDisableWarningScope.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public struct CSharpDisableWarningScope : IDisposable + { + private CSharpCodeWriter _writer; + int _warningNumber; + + public CSharpDisableWarningScope(CSharpCodeWriter writer) : this(writer, 219) + { } + + public CSharpDisableWarningScope(CSharpCodeWriter writer, int warningNumber) + { + _writer = writer; + _warningNumber = warningNumber; + + _writer.WritePragma("warning disable " + _warningNumber); + } + + public void Dispose() + { + _writer.WritePragma("warning restore " + _warningNumber); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpLineMappingWriter.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpLineMappingWriter.cs new file mode 100644 index 0000000000..e4f003e024 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpLineMappingWriter.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class CSharpLineMappingWriter : IDisposable + { + private readonly CSharpCodeWriter _writer; + private readonly MappingLocation _documentMapping; + private readonly int _startIndent; + private readonly bool _writePragmas; + private readonly bool _addLineMapping; + + private SourceLocation _generatedLocation; + private int _generatedContentLength; + + private CSharpLineMappingWriter(CSharpCodeWriter writer, bool addLineMappings) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _writer = writer; + _addLineMapping = addLineMappings; + _startIndent = _writer.CurrentIndent; + _writer.ResetIndent(); + } + + public CSharpLineMappingWriter(CSharpCodeWriter writer, SourceLocation documentLocation, int contentLength) + : this(writer, addLineMappings: true) + { + _documentMapping = new MappingLocation(documentLocation, contentLength); + _generatedLocation = _writer.GetCurrentSourceLocation(); + } + + public CSharpLineMappingWriter( + CSharpCodeWriter writer, + SourceLocation documentLocation, + int contentLength, + string sourceFilename) + : this(writer, documentLocation, contentLength) + { + _writePragmas = true; + + _writer.WriteLineNumberDirective(documentLocation, sourceFilename); + _generatedLocation = _writer.GetCurrentSourceLocation(); + } + + /// + /// Initializes a new instance of used for generation of runtime + /// line mappings. The constructed instance of does not track + /// mappings between the Razor content and the generated content. + /// + /// The to write output to. + /// The of the Razor content being mapping. + /// The input file path. + public CSharpLineMappingWriter( + CSharpCodeWriter writer, + SourceLocation documentLocation, + string sourceFileName) + : this(writer, addLineMappings: false) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + _writePragmas = true; + _writer.WriteLineNumberDirective(documentLocation, sourceFileName); + } + + public void MarkLineMappingStart() + { + _generatedLocation = _writer.GetCurrentSourceLocation(); + } + + public void MarkLineMappingEnd() + { + _generatedContentLength = _writer.GenerateCode().Length - _generatedLocation.AbsoluteIndex; + } + + public void Dispose() + { + if (_addLineMapping) + { + // Verify that the generated length has not already been calculated + if (_generatedContentLength == 0) + { + _generatedContentLength = _writer.GenerateCode().Length - _generatedLocation.AbsoluteIndex; + } + + var generatedLocation = new MappingLocation(_generatedLocation, _generatedContentLength); + var documentMapping = _documentMapping; + if (documentMapping.ContentLength == -1) + { + documentMapping = new MappingLocation( + location: new SourceLocation( + _documentMapping.FilePath, + _documentMapping.AbsoluteIndex, + _documentMapping.LineIndex, + _documentMapping.CharacterIndex), + contentLength: _generatedContentLength); + } + + _writer.LineMappingManager.AddMapping( + documentLocation: documentMapping, + generatedLocation: generatedLocation); + } + + if (_writePragmas) + { + // Need to add an additional line at the end IF there wasn't one already written. + // This is needed to work with the C# editor's handling of #line ... + var endsWithNewline = _writer.GenerateCode().EndsWith("\n"); + + // Always write at least 1 empty line to potentially separate code from pragmas. + _writer.WriteLine(); + + // Check if the previous empty line wasn't enough to separate code from pragmas. + if (!endsWithNewline) + { + _writer.WriteLine(); + } + + _writer.WriteLineDefaultDirective() + .WriteLineHiddenDirective(); + } + + // Reset indent back to when it was started + _writer.SetIndent(_startIndent); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpPaddingBuilder.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpPaddingBuilder.cs new file mode 100644 index 0000000000..43fbcb2d48 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpPaddingBuilder.cs @@ -0,0 +1,180 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class CSharpPaddingBuilder + { + private static readonly char[] _newLineChars = { '\r', '\n' }; + + private readonly RazorEngineHost _host; + + public CSharpPaddingBuilder(RazorEngineHost host) + { + _host = host; + } + + // Special case for statement padding to account for brace positioning in the editor. + public string BuildStatementPadding(Span target) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + var padding = CalculatePadding(target, generatedStart: 0); + + // We treat statement padding specially so for brace positioning, so that in the following example: + // @if (foo > 0) + // { + // } + // + // the braces shows up under the @ rather than under the if. + if (_host.DesignTimeMode && + padding > 0 && + target.Previous.Kind == SpanKind.Transition && // target.Previous is guaranteed to not be null if you have padding. + string.Equals(target.Previous.Content, SyntaxConstants.TransitionString, StringComparison.Ordinal)) + { + padding--; + } + + var generatedCode = BuildPaddingInternal(padding); + + return generatedCode; + } + + public string BuildExpressionPadding(Span target) + { + return BuildExpressionPadding(target, generatedStart: 0); + } + + public string BuildExpressionPadding(Span target, int generatedStart) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + var padding = CalculatePadding(target, generatedStart); + + return BuildPaddingInternal(padding); + } + + internal int CalculatePadding(Span target, int generatedStart) + { + if (target == null) + { + throw new ArgumentNullException(nameof(target)); + } + + int padding; + + padding = CollectSpacesAndTabs(target, _host.TabSize) - generatedStart; + + // if we add generated text that is longer than the padding we wanted to insert we have no recourse and we have to skip padding + // example: + // Razor code at column zero: @somecode() + // Generated code will be: + // In design time: __o = somecode(); + // In Run time: Write(somecode()); + // + // In both cases the padding would have been 1 space to remote the space the @ symbol takes, which will be smaller than the 6 + // chars the hidden generated code takes. + if (padding < 0) + { + padding = 0; + } + + return padding; + } + + private string BuildPaddingInternal(int padding) + { + if (_host.DesignTimeMode && _host.IsIndentingWithTabs) + { + var spaces = padding % _host.TabSize; + var tabs = padding / _host.TabSize; + + return new string('\t', tabs) + new string(' ', spaces); + } + else + { + return new string(' ', padding); + } + } + + private static int CollectSpacesAndTabs(Span target, int tabSize) + { + var firstSpanInLine = target; + + string currentContent = null; + + while (firstSpanInLine.Previous != null) + { + // When scanning previous spans we need to be break down the spans with spaces. The parser combines + // whitespace into existing spans so you'll see tabs, newlines etc. within spans. We only care about + // the \t in existing spans. + var previousContent = firstSpanInLine.Previous.Content ?? string.Empty; + + var lastNewLineIndex = previousContent.LastIndexOfAny(_newLineChars); + + if (lastNewLineIndex < 0) + { + firstSpanInLine = firstSpanInLine.Previous; + } + else + { + if (lastNewLineIndex != previousContent.Length - 1) + { + firstSpanInLine = firstSpanInLine.Previous; + currentContent = previousContent.Substring(lastNewLineIndex + 1); + } + + break; + } + } + + // We need to walk from the beginning of the line, because space + tab(tabSize) = tabSize columns, but tab(tabSize) + space = tabSize+1 columns. + var currentSpanInLine = firstSpanInLine; + + if (currentContent == null) + { + currentContent = currentSpanInLine.Content; + } + + var padding = 0; + while (currentSpanInLine != target) + { + if (currentContent != null) + { + for (int i = 0; i < currentContent.Length; i++) + { + if (currentContent[i] == '\t') + { + // Example: + // : + // iter 1) 1 + // iter 2) 2 + // iter 3) 4 = 2 + (4 - 2) + // iter 4) 8 = 4 + (4 - 0) + padding = padding + (tabSize - (padding % tabSize)); + } + else + { + padding++; + } + } + } + + currentSpanInLine = currentSpanInLine.Next; + currentContent = currentSpanInLine.Content; + } + + return padding; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpTagHelperCodeRenderer.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpTagHelperCodeRenderer.cs new file mode 100644 index 0000000000..dbd26e3a2c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CSharpTagHelperCodeRenderer.cs @@ -0,0 +1,761 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.CodeGenerators.Visitors; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + /// + /// Renders tag helper rendering code. + /// + public class CSharpTagHelperCodeRenderer + { + internal static readonly string ExecutionContextVariableName = "__tagHelperExecutionContext"; + internal static readonly string StringValueBufferVariableName = "__tagHelperStringValueBuffer"; + internal static readonly string ScopeManagerVariableName = "__tagHelperScopeManager"; + internal static readonly string RunnerVariableName = "__tagHelperRunner"; + + private readonly CSharpCodeWriter _writer; + private readonly CodeGeneratorContext _context; + private readonly IChunkVisitor _bodyVisitor; + private readonly IChunkVisitor _literalBodyVisitor; + private readonly TagHelperAttributeCodeVisitor _attributeCodeVisitor; + private readonly GeneratedTagHelperContext _tagHelperContext; + private readonly bool _designTimeMode; + + /// + /// Instantiates a new . + /// + /// The used to render chunks found in the body. + /// The used to write code. + /// A instance that contains information about + /// the current code generation process. + public CSharpTagHelperCodeRenderer( + IChunkVisitor bodyVisitor, + CSharpCodeWriter writer, + CodeGeneratorContext context) + { + if (bodyVisitor == null) + { + throw new ArgumentNullException(nameof(bodyVisitor)); + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _bodyVisitor = bodyVisitor; + _writer = writer; + _context = context; + _tagHelperContext = context.Host.GeneratedClassContext.GeneratedTagHelperContext; + _designTimeMode = context.Host.DesignTimeMode; + + _literalBodyVisitor = new CSharpLiteralCodeVisitor(this, writer, context); + _attributeCodeVisitor = new TagHelperAttributeCodeVisitor(writer, context); + AttributeValueCodeRenderer = new TagHelperAttributeValueCodeRenderer(); + } + + public TagHelperAttributeValueCodeRenderer AttributeValueCodeRenderer { get; set; } + + /// + /// Renders the code for the given . + /// + /// A to render. + public void RenderTagHelper(TagHelperChunk chunk) + { + // Remove any duplicate TagHelperDescriptors that reference the same type name. Duplicates can occur when + // multiple HtmlTargetElement attributes are on a TagHelper type and matches overlap for an HTML element. + // Having more than one descriptor with the same TagHelper type results in generated code that runs + // the same TagHelper X many times (instead of once) over a single HTML element. + var tagHelperDescriptors = chunk.Descriptors.Distinct(TypeBasedTagHelperDescriptorComparer.Default); + + RenderBeginTagHelperScope(chunk.TagName, chunk.TagMode, chunk.Children); + + RenderTagHelpersCreation(chunk, tagHelperDescriptors); + + RenderAttributes(chunk.Attributes, tagHelperDescriptors); + + // No need to run anything in design time mode. + if (!_designTimeMode) + { + RenderRunTagHelpers(); + RenderWriteTagHelperMethodCall(chunk); + RenderEndTagHelpersScope(); + } + } + + internal static string GetVariableName(TagHelperDescriptor descriptor) + { + return "__" + descriptor.TypeName.Replace('.', '_'); + } + + private void RenderBeginTagHelperScope(string tagName, TagMode tagMode, IList children) + { + // Scopes/execution contexts are a runtime feature. + if (_designTimeMode) + { + // Render all of the tag helper children inline for IntelliSense. + _bodyVisitor.Accept(children); + return; + } + + // Call into the tag helper scope manager to start a new tag helper scope. + // Also capture the value as the current execution context. + _writer + .WriteStartAssignment(ExecutionContextVariableName) + .WriteStartInstanceMethodInvocation( + ScopeManagerVariableName, + _tagHelperContext.ScopeManagerBeginMethodName); + + // Assign a unique ID for this instance of the source HTML tag. This must be unique + // per call site, e.g. if the tag is on the view twice, there should be two IDs. + _writer.WriteStringLiteral(tagName) + .WriteParameterSeparator() + .Write(nameof(TagMode)) + .Write(".") + .Write(tagMode.ToString()) + .WriteParameterSeparator() + .WriteStringLiteral(GenerateUniqueId()) + .WriteParameterSeparator(); + + // We remove the target writer so TagHelper authors can retrieve content. + var oldWriter = _context.TargetWriterName; + _context.TargetWriterName = null; + + using (_writer.BuildAsyncLambda(endLine: false)) + { + // Render all of the tag helper children. + _bodyVisitor.Accept(children); + } + + _context.TargetWriterName = oldWriter; + + _writer.WriteParameterSeparator() + .Write(_tagHelperContext.StartTagHelperWritingScopeMethodName) + .WriteParameterSeparator() + .Write(_tagHelperContext.EndTagHelperWritingScopeMethodName) + .WriteEndMethodInvocation(); + } + + /// + /// Generates a unique ID for an HTML element. + /// + /// + /// A globally unique ID. + /// + protected virtual string GenerateUniqueId() + { + return Guid.NewGuid().ToString("N"); + } + + private void RenderTagHelpersCreation( + TagHelperChunk chunk, + IEnumerable tagHelperDescriptors) + { + // This is to maintain value accessors for attributes when creating the TagHelpers. + // Ultimately it enables us to do scenarios like this: + // myTagHelper1.Foo = DateTime.Now; + // myTagHelper2.Foo = myTagHelper1.Foo; + var htmlAttributeValues = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var tagHelperDescriptor in tagHelperDescriptors) + { + var tagHelperVariableName = GetVariableName(tagHelperDescriptor); + + // Create the tag helper + _writer.WriteStartAssignment(tagHelperVariableName) + .WriteStartMethodInvocation(_tagHelperContext.CreateTagHelperMethodName, + tagHelperDescriptor.TypeName) + .WriteEndMethodInvocation(); + + // Execution contexts and throwing errors for null dictionary properties are a runtime feature. + if (_designTimeMode) + { + continue; + } + + _writer.WriteInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddMethodName, + tagHelperVariableName); + + // Track dictionary properties we have confirmed are non-null. + var confirmedDictionaries = new HashSet(StringComparer.Ordinal); + + // Ensure that all created TagHelpers have initialized dictionary bound properties which are used + // via TagHelper indexers. + foreach (var chunkAttribute in chunk.Attributes) + { + var associatedAttributeDescriptor = tagHelperDescriptor.Attributes.FirstOrDefault( + attributeDescriptor => attributeDescriptor.IsNameMatch(chunkAttribute.Key)); + + if (associatedAttributeDescriptor != null && + associatedAttributeDescriptor.IsIndexer && + confirmedDictionaries.Add(associatedAttributeDescriptor.PropertyName)) + { + // Throw a reasonable Exception at runtime if the dictionary property is null. + _writer + .Write("if (") + .Write(tagHelperVariableName) + .Write(".") + .Write(associatedAttributeDescriptor.PropertyName) + .WriteLine(" == null)"); + using (_writer.BuildScope()) + { + // System is in Host.NamespaceImports for all MVC scenarios. No need to generate FullName + // of InvalidOperationException type. + _writer + .Write("throw ") + .WriteStartNewObject(nameof(InvalidOperationException)) + .WriteStartMethodInvocation(_tagHelperContext.FormatInvalidIndexerAssignmentMethodName) + .WriteStringLiteral(chunkAttribute.Key) + .WriteParameterSeparator() + .WriteStringLiteral(tagHelperDescriptor.TypeName) + .WriteParameterSeparator() + .WriteStringLiteral(associatedAttributeDescriptor.PropertyName) + .WriteEndMethodInvocation(endLine: false) // End of method call + .WriteEndMethodInvocation(endLine: true); // End of new expression / throw statement + } + } + } + } + } + + private void RenderAttributes( + IList> chunkAttributes, + IEnumerable tagHelperDescriptors) + { + var renderedBoundAttributeNames = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Go through the HTML attributes in source order, assigning to properties or indexers or adding to + // TagHelperExecutionContext.HTMLAttributes' as we go. + foreach (var attribute in chunkAttributes) + { + var attributeName = attribute.Key; + var attributeValueChunk = attribute.Value; + var associatedDescriptors = tagHelperDescriptors.Where(descriptor => + descriptor.Attributes.Any(attributeDescriptor => attributeDescriptor.IsNameMatch(attributeName))); + + // Bound attributes have associated descriptors. First attribute value wins if there are duplicates; + // later values of duplicate bound attributes are treated as if they were unbound. + if (associatedDescriptors.Any() && renderedBoundAttributeNames.Add(attributeName)) + { + if (attributeValueChunk == null) + { + // Minimized attributes are not valid for bound attributes. TagHelperBlockRewriter has already + // logged an error if it was a bound attribute; so we can skip. + continue; + } + + // We need to capture the tag helper's property value accessor so we can retrieve it later + // if there are more tag helpers that need the value. + string valueAccessor = null; + + foreach (var associatedDescriptor in associatedDescriptors) + { + var associatedAttributeDescriptor = associatedDescriptor.Attributes.First( + attributeDescriptor => attributeDescriptor.IsNameMatch(attributeName)); + var tagHelperVariableName = GetVariableName(associatedDescriptor); + + valueAccessor = RenderBoundAttribute( + attributeName, + attributeValueChunk, + tagHelperVariableName, + valueAccessor, + associatedAttributeDescriptor); + } + } + else + { + RenderUnboundAttribute(attributeName, attributeValueChunk); + } + } + } + + private string RenderBoundAttribute( + string attributeName, + Chunk attributeValueChunk, + string tagHelperVariableName, + string previousValueAccessor, + TagHelperAttributeDescriptor attributeDescriptor) + { + var currentValueAccessor = string.Format( + CultureInfo.InvariantCulture, + "{0}.{1}", + tagHelperVariableName, + attributeDescriptor.PropertyName); + + if (attributeDescriptor.IsIndexer) + { + var dictionaryKey = attributeName.Substring(attributeDescriptor.Name.Length); + currentValueAccessor += $"[\"{dictionaryKey}\"]"; + } + + // If this attribute value has not been seen before, need to record its value. + if (previousValueAccessor == null) + { + // Bufferable attributes are attributes that can have Razor code inside of them. Such + // attributes have string values and may be calculated using a temporary TextWriter or other + // buffer. + var bufferableAttribute = attributeDescriptor.IsStringProperty; + + RenderNewAttributeValueAssignment( + attributeDescriptor, + bufferableAttribute, + attributeValueChunk, + currentValueAccessor); + + if (_designTimeMode) + { + // Execution contexts are a runtime feature. + return currentValueAccessor; + } + + // We need to inform the context of the attribute value. + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddTagHelperAttributeMethodName) + .WriteStringLiteral(attributeName) + .WriteParameterSeparator() + .Write(currentValueAccessor) + .WriteEndMethodInvocation(); + + return currentValueAccessor; + } + else + { + // The attribute value has already been determined and accessor was passed to us as + // previousValueAccessor, we don't want to evaluate the value twice so lets just use the + // previousValueLocation. + _writer + .WriteStartAssignment(currentValueAccessor) + .Write(previousValueAccessor) + .WriteLine(";"); + + return previousValueAccessor; + } + } + + // Render assignment of attribute value to the value accessor. + private void RenderNewAttributeValueAssignment( + TagHelperAttributeDescriptor attributeDescriptor, + bool bufferableAttribute, + Chunk attributeValueChunk, + string valueAccessor) + { + // Plain text values are non Razor code (@DateTime.Now) values. If an attribute is bufferable it + // may be more than just a plain text value, it may also contain Razor code which is why we attempt + // to retrieve a plain text value here. + string textValue; + var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue); + + if (bufferableAttribute) + { + if (!isPlainTextValue) + { + // If we haven't recorded a value and we need to buffer an attribute value and the value is not + // plain text then we need to prepare the value prior to setting it below. + BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: false); + } + + _writer.WriteStartAssignment(valueAccessor); + + if (isPlainTextValue) + { + // If the attribute is bufferable but has a plain text value that means the value + // is a string which needs to be surrounded in quotes. + RenderQuotedAttributeValue(textValue, attributeDescriptor); + } + else + { + // The value contains more than plain text e.g. stringAttribute ="Time: @DateTime.Now". + RenderBufferedAttributeValue(attributeDescriptor); + } + + _writer.WriteLine(";"); + } + else + { + // Write out simple assignment for non-string property value. Try to keep the whole + // statement together and the #line pragma correct to make debugging possible. + using (var lineMapper = new CSharpLineMappingWriter( + _writer, + attributeValueChunk.Association.Start, + _context.SourceFile)) + { + // Place the assignment LHS to align RHS with original attribute value's indentation. + // Unfortunately originalIndent is incorrect if original line contains tabs. Unable to + // use a CSharpPaddingBuilder because the Association has no Previous node; lost the + // original Span sequence when the parse tree was rewritten. + var originalIndent = attributeValueChunk.Start.CharacterIndex; + var generatedLength = valueAccessor.Length + " = ".Length; + var newIndent = originalIndent - generatedLength; + if (newIndent > 0) + { + _writer.Indent(newIndent); + } + + _writer.WriteStartAssignment(valueAccessor); + lineMapper.MarkLineMappingStart(); + + // Write out bare expression for this attribute value. Property is not a string. + // So quoting or buffering are not helpful. + RenderRawAttributeValue(attributeValueChunk, attributeDescriptor, isPlainTextValue); + + // End the assignment to the attribute. + lineMapper.MarkLineMappingEnd(); + _writer.WriteLine(";"); + } + } + } + + private void RenderUnboundAttribute(string attributeName, Chunk attributeValueChunk) + { + // Render children to provide IntelliSense at design time. No need for the execution context logic, it's + // a runtime feature. + if (_designTimeMode) + { + if (attributeValueChunk != null) + { + _bodyVisitor.Accept(attributeValueChunk); + } + + return; + } + + // If we have a minimized attribute there is no value + if (attributeValueChunk == null) + { + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddMinimizedHtmlAttributeMethodName) + .WriteStringLiteral(attributeName) + .WriteEndMethodInvocation(); + } + else + { + string textValue = null; + var isPlainTextValue = TryGetPlainTextValue(attributeValueChunk, out textValue); + + if (isPlainTextValue) + { + // If it's a plain text value then we need to surround the value with quotes. + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName) + .WriteStringLiteral(attributeName) + .WriteParameterSeparator() + .WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName) + .WriteStringLiteral(textValue) + .WriteEndMethodInvocation(endLine: false) + .WriteEndMethodInvocation(); + } + else if (IsDynamicAttributeValue(attributeValueChunk)) + { + // Dynamic attribute value should be run through the conditional attribute removal system. It's + // unbound and contains C#. + + // TagHelper attribute rendering is buffered by default. We do not want to write to the current + // writer. + var currentTargetWriter = _context.TargetWriterName; + var currentWriteAttributeMethodName = _context.Host.GeneratedClassContext.WriteAttributeValueMethodName; + _context.TargetWriterName = null; + + Debug.Assert(attributeValueChunk is ParentChunk); + var children = ((ParentChunk)attributeValueChunk).Children; + var attributeCount = children.Count(c => c is DynamicCodeAttributeChunk || c is LiteralCodeAttributeChunk); + + _writer + .WriteStartMethodInvocation(_tagHelperContext.BeginAddHtmlAttributeValuesMethodName) + .Write(ExecutionContextVariableName) + .WriteParameterSeparator() + .WriteStringLiteral(attributeName) + .WriteParameterSeparator() + .Write(attributeCount.ToString(CultureInfo.InvariantCulture)) + .WriteEndMethodInvocation(); + + _attributeCodeVisitor.Accept(attributeValueChunk); + + _writer.WriteMethodInvocation( + _tagHelperContext.EndAddHtmlAttributeValuesMethodName, + ExecutionContextVariableName); + + _context.TargetWriterName = currentTargetWriter; + } + else + { + // HTML attributes are always strings. This attribute contains C# but is not dynamic. This occurs + // when the attribute is a data-* attribute. + + // Attribute value is not plain text, must be buffered to determine its final value. + BuildBufferedWritingScope(attributeValueChunk, htmlEncodeValues: true); + + _writer + .WriteStartInstanceMethodInvocation( + ExecutionContextVariableName, + _tagHelperContext.ExecutionContextAddHtmlAttributeMethodName) + .WriteStringLiteral(attributeName) + .WriteParameterSeparator() + .WriteStartMethodInvocation(_tagHelperContext.MarkAsHtmlEncodedMethodName); + + RenderBufferedAttributeValueAccessor(_writer); + + _writer + .WriteEndMethodInvocation(endLine: false) + .WriteEndMethodInvocation(); + } + } + } + + private void RenderEndTagHelpersScope() + { + _writer.WriteStartAssignment(ExecutionContextVariableName) + .WriteInstanceMethodInvocation(ScopeManagerVariableName, + _tagHelperContext.ScopeManagerEndMethodName); + } + + private void RenderWriteTagHelperMethodCall(TagHelperChunk chunk) + { + _writer + .WriteStartInstrumentationContext(_context, chunk.Association, isLiteral: false) + .Write("await "); + + if (!string.IsNullOrEmpty(_context.TargetWriterName)) + { + _writer + .WriteStartMethodInvocation(_tagHelperContext.WriteTagHelperToAsyncMethodName) + .Write(_context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + _writer.WriteStartMethodInvocation(_tagHelperContext.WriteTagHelperAsyncMethodName); + } + + _writer + .Write(ExecutionContextVariableName) + .WriteEndMethodInvocation() + .WriteEndInstrumentationContext(_context); + } + + private void RenderRunTagHelpers() + { + _writer.Write(ExecutionContextVariableName) + .Write(".") + .WriteStartAssignment(_tagHelperContext.ExecutionContextOutputPropertyName) + .Write("await ") + .WriteStartInstanceMethodInvocation(RunnerVariableName, + _tagHelperContext.RunnerRunAsyncMethodName); + + _writer.Write(ExecutionContextVariableName) + .WriteEndMethodInvocation(); + } + + private void RenderBufferedAttributeValue(TagHelperAttributeDescriptor attributeDescriptor) + { + // Pass complexValue: false because variable.ToString() replaces any original complexity in the expression. + RenderAttributeValue( + attributeDescriptor, + valueRenderer: (writer) => + { + RenderBufferedAttributeValueAccessor(writer); + }, + complexValue: false); + } + + private void RenderRawAttributeValue( + Chunk attributeValueChunk, + TagHelperAttributeDescriptor attributeDescriptor, + bool isPlainTextValue) + { + RenderAttributeValue( + attributeDescriptor, + valueRenderer: (writer) => + { + var visitor = + new CSharpTagHelperAttributeValueVisitor(writer, _context, attributeDescriptor.TypeName); + visitor.Accept(attributeValueChunk); + }, + complexValue: !isPlainTextValue); + } + + private void RenderQuotedAttributeValue(string value, TagHelperAttributeDescriptor attributeDescriptor) + { + RenderAttributeValue( + attributeDescriptor, + valueRenderer: (writer) => + { + writer.WriteStringLiteral(value); + }, + complexValue: false); + } + + // Render a buffered writing scope for the HTML attribute value. + private void BuildBufferedWritingScope(Chunk htmlAttributeChunk, bool htmlEncodeValues) + { + // We're building a writing scope around the provided chunks which captures everything written from the + // page. Therefore, we do not want to write to any other buffer since we're using the pages buffer to + // ensure we capture all content that's written, directly or indirectly. + var oldWriter = _context.TargetWriterName; + _context.TargetWriterName = null; + + // Need to disable instrumentation inside of writing scopes, the instrumentation will not detect + // content written inside writing scopes. + var oldInstrumentation = _context.Host.EnableInstrumentation; + + try + { + _context.Host.EnableInstrumentation = false; + + // Scopes are a runtime feature. + if (!_designTimeMode) + { + _writer.WriteMethodInvocation(_tagHelperContext.StartTagHelperWritingScopeMethodName); + } + + var visitor = htmlEncodeValues ? _bodyVisitor : _literalBodyVisitor; + visitor.Accept(htmlAttributeChunk); + + // Scopes are a runtime feature. + if (!_designTimeMode) + { + _writer.WriteStartAssignment(StringValueBufferVariableName) + .WriteMethodInvocation(_tagHelperContext.EndTagHelperWritingScopeMethodName); + } + } + finally + { + // Reset instrumentation back to what it was, leaving the writing scope. + _context.Host.EnableInstrumentation = oldInstrumentation; + + // Reset the writer/buffer back to what it was, leaving the writing scope. + _context.TargetWriterName = oldWriter; + } + } + + private void RenderAttributeValue(TagHelperAttributeDescriptor attributeDescriptor, + Action valueRenderer, + bool complexValue) + { + AttributeValueCodeRenderer.RenderAttributeValue( + attributeDescriptor, + _writer, + _context, + valueRenderer, + complexValue); + } + + private void RenderBufferedAttributeValueAccessor(CSharpCodeWriter writer) + { + if (_designTimeMode) + { + // There is no value buffer in design time mode but we still want to write out a value. We write a + // value to ensure the tag helper's property type is string. + writer.Write("string.Empty"); + } + else + { + writer.WriteInstanceMethodInvocation( + StringValueBufferVariableName, + _tagHelperContext.TagHelperContentGetContentMethodName, + endLine: false, + parameters: new string[] { _tagHelperContext.HtmlEncoderPropertyName }); + } + } + + private static bool IsDynamicAttributeValue(Chunk attributeValueChunk) + { + var parentChunk = attributeValueChunk as ParentChunk; + if (parentChunk != null) + { + return parentChunk.Children.Any(child => child is DynamicCodeAttributeChunk); + } + + return false; + } + + private static bool TryGetPlainTextValue(Chunk chunk, out string plainText) + { + var parentChunk = chunk as ParentChunk; + + plainText = null; + + if (parentChunk == null || parentChunk.Children.Count != 1) + { + return false; + } + + var literalChildChunk = parentChunk.Children[0] as LiteralChunk; + + if (literalChildChunk == null) + { + return false; + } + + plainText = literalChildChunk.Text; + + return true; + } + + // A CSharpCodeVisitor which does not HTML encode values. Used when rendering bound string attribute values. + private class CSharpLiteralCodeVisitor : CSharpCodeVisitor + { + public CSharpLiteralCodeVisitor( + CSharpTagHelperCodeRenderer tagHelperRenderer, + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + // Ensure that no matter how this class is used, we don't create numerous CSharpTagHelperCodeRenderer + // instances. + TagHelperRenderer = tagHelperRenderer; + } + + protected override string WriteMethodName + { + get + { + return Context.Host.GeneratedClassContext.WriteLiteralMethodName; + } + } + + protected override string WriteToMethodName + { + get + { + return Context.Host.GeneratedClassContext.WriteLiteralToMethodName; + } + } + } + + private class TagHelperAttributeCodeVisitor : CSharpCodeVisitor + { + public TagHelperAttributeCodeVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + } + + protected override string WriteAttributeValueMethodName => + Context.Host.GeneratedClassContext.GeneratedTagHelperContext.AddHtmlAttributeValueMethodName; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGenerator.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGenerator.cs new file mode 100644 index 0000000000..e4930f42cb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGenerator.cs @@ -0,0 +1,22 @@ +// 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.AspNet.Razor.CodeGenerators +{ + public abstract class CodeGenerator + { + private readonly CodeGeneratorContext _context; + + public CodeGenerator(CodeGeneratorContext context) + { + _context = context; + } + + protected CodeGeneratorContext Context + { + get { return _context; } + } + + public abstract CodeGeneratorResult Generate(); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGeneratorContext.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGeneratorContext.cs new file mode 100644 index 0000000000..b3dc00fcdc --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGeneratorContext.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Chunks.Generators; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + /// + /// Context object with information used to generate a Razor page. + /// + public class CodeGeneratorContext : ChunkGeneratorContext + { + /// + /// Instantiates a new instance of the object. + /// + /// A to copy information from. + /// + /// The used to collect s encountered + /// when parsing the current Razor document. + /// + public CodeGeneratorContext(ChunkGeneratorContext generatorContext, ErrorSink errorSink) + : base(generatorContext) + { + ErrorSink = errorSink; + ExpressionRenderingMode = ExpressionRenderingMode.WriteToOutput; + } + + // Internal for testing. + internal CodeGeneratorContext( + RazorEngineHost host, + string className, + string rootNamespace, + string sourceFile, + bool shouldGenerateLinePragmas, + ErrorSink errorSink) + : base(host, className, rootNamespace, sourceFile, shouldGenerateLinePragmas) + { + ErrorSink = errorSink; + ExpressionRenderingMode = ExpressionRenderingMode.WriteToOutput; + } + + /// + /// The current C# rendering mode. + /// + /// + /// forces C# generation to write + /// s to the output page, i.e. WriteLiteral("Hello World"). + /// writes values in their + /// rawest form, i.g. "Hello World". + /// + public ExpressionRenderingMode ExpressionRenderingMode { get; set; } + + /// + /// The C# writer to write information to. + /// + /// + /// If is null values will be written using a default write method + /// i.e. WriteLiteral("Hello World"). + /// If is not null values will be written to the given + /// , i.e. WriteLiteralTo(myWriter, "Hello World"). + /// + public string TargetWriterName { get; set; } + + /// + /// Gets or sets the SHA1 based checksum for the file whose location is defined by + /// . + /// + public string Checksum { get; set; } + + /// + /// Used to aggregate s. + /// + public ErrorSink ErrorSink { get; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGeneratorResult.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGeneratorResult.cs new file mode 100644 index 0000000000..2d03a5fd4f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeGeneratorResult.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class CodeGeneratorResult + { + public CodeGeneratorResult(string code, IList designTimeLineMappings) + { + Code = code; + DesignTimeLineMappings = designTimeLineMappings; + } + + public string Code { get; private set; } + public IList DesignTimeLineMappings { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeWriter.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeWriter.cs new file mode 100644 index 0000000000..8190243d03 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/CodeWriter.cs @@ -0,0 +1,216 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class CodeWriter : IDisposable + { + private static readonly char[] NewLineCharacters = new char[] { '\r', '\n' }; + private readonly StringWriter _writer = new StringWriter(); + private bool _newLine; + private string _cache = string.Empty; + private bool _dirty = false; + + private int _absoluteIndex; + private int _currentLineIndex; + private int _currentLineCharacterIndex; + + public string LastWrite { get; private set; } + + public int CurrentIndent { get; private set; } + + public string NewLine + { + get + { + return _writer.NewLine; + } + set + { + _writer.NewLine = value; + } + } + + public CodeWriter ResetIndent() + { + return SetIndent(0); + } + + public CodeWriter IncreaseIndent(int size) + { + CurrentIndent += size; + + return this; + } + + public CodeWriter DecreaseIndent(int size) + { + CurrentIndent -= size; + + return this; + } + + public CodeWriter SetIndent(int size) + { + CurrentIndent = size; + + return this; + } + + public CodeWriter Indent(int size) + { + if (_newLine) + { + _writer.Write(new string(' ', size)); + Flush(); + + _currentLineCharacterIndex += size; + _absoluteIndex += size; + + _dirty = true; + _newLine = false; + } + + return this; + } + + public CodeWriter Write(string data) + { + Indent(CurrentIndent); + + _writer.Write(data); + Flush(); + + LastWrite = data; + _dirty = true; + _newLine = false; + + if (data == null || data.Length == 0) + { + return this; + } + + _absoluteIndex += data.Length; + + // The data string might contain a partial newline where the previously + // written string has part of the newline. + var i = 0; + int? trailingPartStart = null; + var builder = _writer.GetStringBuilder(); + + if ( + // Check the last character of the previous write operation. + builder.Length - data.Length - 1 >= 0 && + builder[builder.Length - data.Length - 1] == '\r' && + + // Check the first character of the current write operation. + builder[builder.Length - data.Length] == '\n') + { + // This is newline that's spread across two writes. Skip the first character of the + // current write operation. + // + // We don't need to increment our newline counter because we already did that when we + // saw the \r. + i += 1; + trailingPartStart = 1; + } + + // Iterate the string, stopping at each occurrence of a newline character. This lets us count the + // newline occurrences and keep the index of the last one. + while ((i = data.IndexOfAny(NewLineCharacters, i)) >= 0) + { + // Newline found. + _currentLineIndex++; + _currentLineCharacterIndex = 0; + + i++; + + // We might have stopped at a \r, so check if it's followed by \n and then advance the index to + // start the next search after it. + if (data.Length > i && + data[i - 1] == '\r' && + data[i] == '\n') + { + i++; + } + + // The 'suffix' of the current line starts after this newline token. + trailingPartStart = i; + } + + if (trailingPartStart == null) + { + // No newlines, just add the length of the data buffer + _currentLineCharacterIndex += data.Length; + } + else + { + // Newlines found, add the trailing part of 'data' + _currentLineCharacterIndex += (data.Length - trailingPartStart.Value); + } + + return this; + } + + public CodeWriter WriteLine() + { + LastWrite = _writer.NewLine; + + _writer.WriteLine(); + Flush(); + + _currentLineIndex++; + _currentLineCharacterIndex = 0; + _absoluteIndex += _writer.NewLine.Length; + + _dirty = true; + _newLine = true; + + return this; + } + + public CodeWriter WriteLine(string data) + { + return Write(data).WriteLine(); + } + + public CodeWriter Flush() + { + _writer.Flush(); + + return this; + } + + public string GenerateCode() + { + if (_dirty) + { + _cache = _writer.ToString(); + _dirty = false; + } + + return _cache; + } + + public SourceLocation GetCurrentSourceLocation() + { + return new SourceLocation(_absoluteIndex, _currentLineIndex, _currentLineCharacterIndex); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _writer.Dispose(); + } + } + + public void Dispose() + { + Dispose(disposing: true); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/ExpressionRenderingMode.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/ExpressionRenderingMode.cs new file mode 100644 index 0000000000..d580d375f1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/ExpressionRenderingMode.cs @@ -0,0 +1,29 @@ +// 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.AspNet.Razor.CodeGenerators +{ + public enum ExpressionRenderingMode + { + /// + /// Indicates that expressions should be written to the output stream + /// + /// + /// If @foo is rendered with WriteToOutput, the code generator would output the following code: + /// + /// Write(foo); + /// + WriteToOutput, + + /// + /// Indicates that expressions should simply be placed as-is in the code, and the context in which + /// the code exists will be used to render it + /// + /// + /// If @foo is rendered with InjectCode, the code generator would output the following code: + /// + /// foo + /// + InjectCode + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratedClassContext.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratedClassContext.cs new file mode 100644 index 0000000000..71eba9c828 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratedClassContext.cs @@ -0,0 +1,219 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public struct GeneratedClassContext + { + public static readonly string DefaultWriteMethodName = "Write"; + public static readonly string DefaultWriteLiteralMethodName = "WriteLiteral"; + public static readonly string DefaultExecuteMethodName = "ExecuteAsync"; + public static readonly string DefaultBeginWriteAttributeMethodName = "BeginWriteAttribute"; + public static readonly string DefaultBeginWriteAttributeToMethodName = "BeginWriteAttributeTo"; + public static readonly string DefaultEndWriteAttributeMethodName = "EndWriteAttribute"; + public static readonly string DefaultEndWriteAttributeToMethodName = "EndWriteAttributeTo"; + public static readonly string DefaultWriteAttributeValueMethodName = "WriteAttributeValue"; + public static readonly string DefaultWriteAttributeValueToMethodName = "WriteAttributeValueTo"; + + public static readonly GeneratedClassContext Default = + new GeneratedClassContext( + DefaultExecuteMethodName, + DefaultWriteMethodName, + DefaultWriteLiteralMethodName, + new GeneratedTagHelperContext()); + + public GeneratedClassContext( + string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + GeneratedTagHelperContext generatedTagHelperContext) + : this() + { + if (generatedTagHelperContext == null) + { + throw new ArgumentNullException(nameof(generatedTagHelperContext)); + } + + if (string.IsNullOrEmpty(executeMethodName)) + { + throw new ArgumentException( + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + nameof(executeMethodName)); + } + if (string.IsNullOrEmpty(writeMethodName)) + { + throw new ArgumentException( + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + nameof(writeMethodName)); + } + if (string.IsNullOrEmpty(writeLiteralMethodName)) + { + throw new ArgumentException( + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + nameof(writeLiteralMethodName)); + } + + GeneratedTagHelperContext = generatedTagHelperContext; + + WriteMethodName = writeMethodName; + WriteLiteralMethodName = writeLiteralMethodName; + ExecuteMethodName = executeMethodName; + + WriteToMethodName = null; + WriteLiteralToMethodName = null; + TemplateTypeName = null; + DefineSectionMethodName = null; + + BeginWriteAttributeMethodName = DefaultBeginWriteAttributeMethodName; + BeginWriteAttributeToMethodName = DefaultBeginWriteAttributeToMethodName; + EndWriteAttributeMethodName = DefaultEndWriteAttributeMethodName; + EndWriteAttributeToMethodName = DefaultEndWriteAttributeToMethodName; + WriteAttributeValueMethodName = DefaultWriteAttributeValueMethodName; + WriteAttributeValueToMethodName = DefaultWriteAttributeValueToMethodName; + } + + public GeneratedClassContext( + string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + string writeToMethodName, + string writeLiteralToMethodName, + string templateTypeName, + GeneratedTagHelperContext generatedTagHelperContext) + : this(executeMethodName, + writeMethodName, + writeLiteralMethodName, + generatedTagHelperContext) + { + WriteToMethodName = writeToMethodName; + WriteLiteralToMethodName = writeLiteralToMethodName; + TemplateTypeName = templateTypeName; + } + + public GeneratedClassContext( + string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + string writeToMethodName, + string writeLiteralToMethodName, + string templateTypeName, + string defineSectionMethodName, + GeneratedTagHelperContext generatedTagHelperContext) + : this(executeMethodName, + writeMethodName, + writeLiteralMethodName, + writeToMethodName, + writeLiteralToMethodName, + templateTypeName, + generatedTagHelperContext) + { + DefineSectionMethodName = defineSectionMethodName; + } + + public GeneratedClassContext( + string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + string writeToMethodName, + string writeLiteralToMethodName, + string templateTypeName, + string defineSectionMethodName, + string beginContextMethodName, + string endContextMethodName, + GeneratedTagHelperContext generatedTagHelperContext) + : this(executeMethodName, + writeMethodName, + writeLiteralMethodName, + writeToMethodName, + writeLiteralToMethodName, + templateTypeName, + defineSectionMethodName, + generatedTagHelperContext) + { + BeginContextMethodName = beginContextMethodName; + EndContextMethodName = endContextMethodName; + } + + // Required Items + public string WriteMethodName { get; } + public string WriteLiteralMethodName { get; } + public string WriteToMethodName { get; } + public string WriteLiteralToMethodName { get; } + public string ExecuteMethodName { get; } + public GeneratedTagHelperContext GeneratedTagHelperContext { get; } + + // Optional Items + public string BeginContextMethodName { get; set; } + public string EndContextMethodName { get; set; } + public string DefineSectionMethodName { get; set; } + public string TemplateTypeName { get; set; } + + public string BeginWriteAttributeMethodName { get; set; } + public string BeginWriteAttributeToMethodName { get; set; } + public string EndWriteAttributeMethodName { get; set; } + public string EndWriteAttributeToMethodName { get; set; } + public string WriteAttributeValueMethodName { get; set; } + public string WriteAttributeValueToMethodName { get; set; } + + public bool AllowSections + { + get { return !string.IsNullOrEmpty(DefineSectionMethodName); } + } + + public bool AllowTemplates + { + get { return !string.IsNullOrEmpty(TemplateTypeName); } + } + + public bool SupportsInstrumentation + { + get { return !string.IsNullOrEmpty(BeginContextMethodName) && !string.IsNullOrEmpty(EndContextMethodName); } + } + + public override bool Equals(object obj) + { + if (!(obj is GeneratedClassContext)) + { + return false; + } + + var other = (GeneratedClassContext)obj; + return string.Equals(DefineSectionMethodName, other.DefineSectionMethodName, StringComparison.Ordinal) && + string.Equals(WriteMethodName, other.WriteMethodName, StringComparison.Ordinal) && + string.Equals(WriteLiteralMethodName, other.WriteLiteralMethodName, StringComparison.Ordinal) && + string.Equals(WriteToMethodName, other.WriteToMethodName, StringComparison.Ordinal) && + string.Equals(WriteLiteralToMethodName, other.WriteLiteralToMethodName, StringComparison.Ordinal) && + string.Equals(ExecuteMethodName, other.ExecuteMethodName, StringComparison.Ordinal) && + string.Equals(TemplateTypeName, other.TemplateTypeName, StringComparison.Ordinal) && + string.Equals(BeginContextMethodName, other.BeginContextMethodName, StringComparison.Ordinal) && + string.Equals(EndContextMethodName, other.EndContextMethodName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + var hashCodeCombiner = HashCodeCombiner.Start(); + + hashCodeCombiner.Add(WriteMethodName, StringComparer.Ordinal); + hashCodeCombiner.Add(WriteLiteralMethodName, StringComparer.Ordinal); + hashCodeCombiner.Add(WriteToMethodName, StringComparer.Ordinal); + hashCodeCombiner.Add(WriteLiteralToMethodName, StringComparer.Ordinal); + hashCodeCombiner.Add(ExecuteMethodName, StringComparer.Ordinal); + + return hashCodeCombiner; + } + + public static bool operator ==(GeneratedClassContext left, GeneratedClassContext right) + { + return left.Equals(right); + } + + public static bool operator !=(GeneratedClassContext left, GeneratedClassContext right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratedTagHelperContext.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratedTagHelperContext.cs new file mode 100644 index 0000000000..138f9a74f1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratedTagHelperContext.cs @@ -0,0 +1,209 @@ +// 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.AspNet.Razor.CodeGenerators +{ + /// + /// Contains necessary information for the tag helper code generation process. + /// + public class GeneratedTagHelperContext + { + /// + /// Instantiates a new instance of the with default values. + /// + public GeneratedTagHelperContext() + { + BeginAddHtmlAttributeValuesMethodName = "BeginAddHtmlAttributeValues"; + EndAddHtmlAttributeValuesMethodName = "EndAddHtmlAttributeValues"; + AddHtmlAttributeValueMethodName = "AddHtmlAttributeValue"; + CreateTagHelperMethodName = "CreateTagHelper"; + RunnerRunAsyncMethodName = "RunAsync"; + ScopeManagerBeginMethodName = "Begin"; + ScopeManagerEndMethodName = "End"; + ExecutionContextAddMethodName = "Add"; + ExecutionContextAddTagHelperAttributeMethodName = "AddTagHelperAttribute"; + ExecutionContextAddMinimizedHtmlAttributeMethodName = "AddMinimizedHtmlAttribute"; + ExecutionContextAddHtmlAttributeMethodName = "AddHtmlAttribute"; + ExecutionContextOutputPropertyName = "Output"; + FormatInvalidIndexerAssignmentMethodName = "FormatInvalidIndexerAssignment"; + MarkAsHtmlEncodedMethodName = "Html.Raw"; + StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope"; + EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope"; + RunnerTypeName = "TagHelperRunner"; + ScopeManagerTypeName = "TagHelperScopeManager"; + ExecutionContextTypeName = "TagHelperExecutionContext"; + TagHelperContentTypeName = "TagHelperContent"; + WriteTagHelperAsyncMethodName = "WriteTagHelperAsync"; + WriteTagHelperToAsyncMethodName = "WriteTagHelperToAsync"; + TagHelperContentGetContentMethodName = "GetContent"; + HtmlEncoderPropertyName = "HtmlEncoder"; + } + + /// + /// The name of the method used to begin the addition of unbound, complex tag helper attributes to + /// TagHelperExecutionContexts. + /// + /// + /// Method signature should be + /// + /// public void BeginAddHtmlAttributeValues( + /// TagHelperExecutionContext executionContext, + /// string attributeName) + /// + /// + public string BeginAddHtmlAttributeValuesMethodName { get; set; } + + /// + /// Method name used to end addition of unbound, complex tag helper attributes to TagHelperExecutionContexts. + /// + /// + /// Method signature should be + /// + /// public void EndAddHtmlAttributeValues( + /// TagHelperExecutionContext executionContext) + /// + /// + public string EndAddHtmlAttributeValuesMethodName { get; set; } + + /// + /// Method name used to add individual components of an unbound, complex tag helper attribute to + /// TagHelperExecutionContexts. + /// + /// + /// Method signature: + /// + /// public void AddHtmlAttributeValues( + /// string prefix, + /// int prefixOffset, + /// string value, + /// int valueOffset, + /// int valueLength, + /// bool isLiteral) + /// + /// + public string AddHtmlAttributeValueMethodName { get; set; } + + /// + /// The name of the method used to create a tag helper. + /// + public string CreateTagHelperMethodName { get; set; } + + /// + /// The name of the method used to run tag helpers. + /// + public string RunnerRunAsyncMethodName { get; set; } + + /// + /// The name of the method used to start a scope. + /// + public string ScopeManagerBeginMethodName { get; set; } + + /// + /// The name of the method used to end a scope. + /// + public string ScopeManagerEndMethodName { get; set; } + + /// + /// The name of the method used to add tag helper attributes. + /// + public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } + + /// + /// The name of the method used to add minimized HTML attributes. + /// + public string ExecutionContextAddMinimizedHtmlAttributeMethodName { get; set; } + + /// + /// The name of the method used to add HTML attributes. + /// + public string ExecutionContextAddHtmlAttributeMethodName { get; set; } + + /// + /// The name of the method used to add tag helpers. + /// + public string ExecutionContextAddMethodName { get; set; } + + /// + /// The property accessor for the tag helper's output. + /// + public string ExecutionContextOutputPropertyName { get; set; } + + /// + /// The name of the method used to format an error message about using an indexer when the tag helper property + /// is null. + /// + /// + /// Method signature should be + /// + /// public string FormatInvalidIndexerAssignment( + /// string attributeName, // Name of the HTML attribute associated with the indexer. + /// string tagHelperTypeName, // Full name of the tag helper type. + /// string propertyName) // Dictionary property in the tag helper. + /// + /// + public string FormatInvalidIndexerAssignmentMethodName { get; set; } + + /// + /// The name of the method used to wrap a value and mark it as HTML-encoded. + /// + /// Used together with . + public string MarkAsHtmlEncodedMethodName { get; set; } + + /// + /// The name of the method used to start a new writing scope. + /// + public string StartTagHelperWritingScopeMethodName { get; set; } + + /// + /// The name of the method used to end a writing scope. + /// + public string EndTagHelperWritingScopeMethodName { get; set; } + + /// + /// The name of the type used to run tag helpers. + /// + public string RunnerTypeName { get; set; } + + /// + /// The name of the type used to create scoped instances. + /// + public string ScopeManagerTypeName { get; set; } + + /// + /// The name of the type describing a specific tag helper scope. + /// + /// + /// Contains information about in-scope tag helpers, HTML attributes, and the tag helpers' output. + /// + public string ExecutionContextTypeName { get; set; } + + /// + /// The name of the type containing tag helper content. + /// + /// + /// Contains the data returned by EndTagHelperWriteScope(). + /// + public string TagHelperContentTypeName { get; set; } + + /// + /// The name of the method used to write . + /// + public string WriteTagHelperAsyncMethodName { get; set; } + + /// + /// The name of the method used to write to a specified + /// . + /// + public string WriteTagHelperToAsyncMethodName { get; set; } + + /// + /// The name of the property containing the IHtmlEncoder. + /// + public string HtmlEncoderPropertyName { get; set; } + + /// + /// The name of the method used to convert a TagHelperContent into a . + /// + public string TagHelperContentGetContentMethodName { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratorResults.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratorResults.cs new file mode 100644 index 0000000000..c9094c1948 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/GeneratorResults.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + /// + /// The results of parsing and generating code for a Razor document. + /// + public class GeneratorResults : ParserResults + { + /// + /// Instantiates a new instance. + /// + /// The results of parsing a document. + /// The results of generating code for the document. + /// A for the document. + public GeneratorResults(ParserResults parserResults, + CodeGeneratorResult codeGeneratorResult, + ChunkTree chunkTree) + : this(parserResults.Document, + parserResults.TagHelperDescriptors, + parserResults.ErrorSink, + codeGeneratorResult, + chunkTree) + { + if (parserResults == null) + { + throw new ArgumentNullException(nameof(parserResults)); + } + if (codeGeneratorResult == null) + { + throw new ArgumentNullException(nameof(codeGeneratorResult)); + } + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + } + + /// + /// Instantiates a new instance. + /// + /// The for the syntax tree. + /// + /// The s that apply to the current Razor document. + /// + /// + /// The used to collect s encountered when parsing the + /// current Razor document. + /// + /// The results of generating code for the document. + /// A for the document. + public GeneratorResults(Block document, + IEnumerable tagHelperDescriptors, + ErrorSink errorSink, + CodeGeneratorResult codeGeneratorResult, + ChunkTree chunkTree) + : base(document, tagHelperDescriptors, errorSink) + { + if (document == null) + { + throw new ArgumentNullException(nameof(document)); + } + + if (tagHelperDescriptors == null) + { + throw new ArgumentNullException(nameof(tagHelperDescriptors)); + } + + if (errorSink == null) + { + throw new ArgumentNullException(nameof(errorSink)); + } + + if (codeGeneratorResult == null) + { + throw new ArgumentNullException(nameof(codeGeneratorResult)); + } + + if (chunkTree == null) + { + throw new ArgumentNullException(nameof(chunkTree)); + } + + GeneratedCode = codeGeneratorResult.Code; + DesignTimeLineMappings = codeGeneratorResult.DesignTimeLineMappings; + ChunkTree = chunkTree; + } + + /// + /// The generated code for the document. + /// + public string GeneratedCode { get; } + + /// + /// s used to project code from a file during design time. + /// + public IList DesignTimeLineMappings { get; } + + /// + /// A for the document. + /// + public ChunkTree ChunkTree { get; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/LineMapping.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/LineMapping.cs new file mode 100644 index 0000000000..f8dfc4d1cb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/LineMapping.cs @@ -0,0 +1,79 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class LineMapping + { + public LineMapping(MappingLocation documentLocation, MappingLocation generatedLocation) + { + DocumentLocation = documentLocation; + GeneratedLocation = generatedLocation; + } + + public MappingLocation DocumentLocation { get; } + + public MappingLocation GeneratedLocation { get; } + + public override bool Equals(object obj) + { + var other = obj as LineMapping; + if (ReferenceEquals(other, null)) + { + return false; + } + + return DocumentLocation.Equals(other.DocumentLocation) && + GeneratedLocation.Equals(other.GeneratedLocation); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(DocumentLocation); + hashCodeCombiner.Add(GeneratedLocation); + + return hashCodeCombiner; + } + + public static bool operator ==(LineMapping left, LineMapping right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return true; + } + + if (ReferenceEquals(left, null)) + { + return false; + } + + return left.Equals(right); + } + + public static bool operator !=(LineMapping left, LineMapping right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return false; + } + + if (ReferenceEquals(left, null)) + { + return true; + } + + return !left.Equals(right); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentUICulture, "{0} -> {1}", DocumentLocation, GeneratedLocation); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/LineMappingManager.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/LineMappingManager.cs new file mode 100644 index 0000000000..584a83111a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/LineMappingManager.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class LineMappingManager + { + public LineMappingManager() + { + Mappings = new List(); + } + + public List Mappings { get; } + + public void AddMapping(MappingLocation documentLocation, MappingLocation generatedLocation) + { + Mappings.Add(new LineMapping(documentLocation, generatedLocation)); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/MappingLocation.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/MappingLocation.cs new file mode 100644 index 0000000000..235974058a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/MappingLocation.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + public class MappingLocation + { + public MappingLocation() + { + } + + public MappingLocation(SourceLocation location, int contentLength) + { + ContentLength = contentLength; + AbsoluteIndex = location.AbsoluteIndex; + LineIndex = location.LineIndex; + CharacterIndex = location.CharacterIndex; + FilePath = location.FilePath; + } + + public int ContentLength { get; } + + public int AbsoluteIndex { get; } + + public int LineIndex { get; } + + public int CharacterIndex { get; } + + public string FilePath { get; } + + public override bool Equals(object obj) + { + var other = obj as MappingLocation; + if (ReferenceEquals(other, null)) + { + return false; + } + + return string.Equals(FilePath, other.FilePath, StringComparison.Ordinal) && + AbsoluteIndex == other.AbsoluteIndex && + ContentLength == other.ContentLength && + LineIndex == other.LineIndex && + CharacterIndex == other.CharacterIndex; + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(FilePath, StringComparer.Ordinal); + hashCodeCombiner.Add(AbsoluteIndex); + hashCodeCombiner.Add(ContentLength); + hashCodeCombiner.Add(LineIndex); + hashCodeCombiner.Add(CharacterIndex); + + return hashCodeCombiner; + } + + public override string ToString() + { + return string.Format( + CultureInfo.CurrentCulture, "({0}:{1},{2} [{3}] {4})", + AbsoluteIndex, + LineIndex, + CharacterIndex, + ContentLength, + FilePath); + } + + public static bool operator ==(MappingLocation left, MappingLocation right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return true; + } + + if (ReferenceEquals(left, null)) + { + return false; + } + + return left.Equals(right); + } + + public static bool operator !=(MappingLocation left, MappingLocation right) + { + if (ReferenceEquals(left, right)) + { + // Exact equality e.g. both objects are null. + return false; + } + + if (ReferenceEquals(left, null)) + { + return true; + } + + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/TagHelperAttributeValueCodeRenderer.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/TagHelperAttributeValueCodeRenderer.cs new file mode 100644 index 0000000000..7af38de6b3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/TagHelperAttributeValueCodeRenderer.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Compilation.TagHelpers; + +namespace Microsoft.AspNet.Razor.CodeGenerators +{ + /// + /// Renders code for tag helper property initialization. + /// + public class TagHelperAttributeValueCodeRenderer + { + /// + /// Called during Razor's code generation process to generate code that instantiates the value of the tag + /// helper's property. Last value written should not be or end with a semicolon. + /// + /// + /// The to generate code for. + /// + /// The that's used to write code. + /// A instance that contains + /// information about the current code generation process. + /// + /// that renders the raw value of the HTML attribute. + /// + /// + /// Indicates whether or not the source attribute value contains more than simple text. false for plain + /// C# expressions e.g. "PropertyName". true if the attribute value contain at least one in-line + /// Razor construct e.g. "@(@readonly)". + /// + public virtual void RenderAttributeValue( + TagHelperAttributeDescriptor attributeDescriptor, + CSharpCodeWriter writer, + CodeGeneratorContext context, + Action renderAttributeValue, + bool complexValue) + { + if (attributeDescriptor == null) + { + throw new ArgumentNullException(nameof(attributeDescriptor)); + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (renderAttributeValue == null) + { + throw new ArgumentNullException(nameof(renderAttributeValue)); + } + + renderAttributeValue(writer); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpBaseTypeVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpBaseTypeVisitor.cs new file mode 100644 index 0000000000..9bd5178000 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpBaseTypeVisitor.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CSharpBaseTypeVisitor : CodeVisitor + { + public CSharpBaseTypeVisitor(CSharpCodeWriter writer, CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + } + + public string CurrentBaseType { get; set; } + + protected override void Visit(SetBaseTypeChunk chunk) + { + CurrentBaseType = chunk.TypeName; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpCodeVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpCodeVisitor.cs new file mode 100644 index 0000000000..a6d6b27943 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpCodeVisitor.cs @@ -0,0 +1,563 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CSharpCodeVisitor : CodeVisitor + { + private const string ItemParameterName = "item"; + private const string ValueWriterName = "__razor_attribute_value_writer"; + private const string TemplateWriterName = "__razor_template_writer"; + + private CSharpPaddingBuilder _paddingBuilder; + private CSharpTagHelperCodeRenderer _tagHelperCodeRenderer; + + public CSharpCodeVisitor(CSharpCodeWriter writer, CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _paddingBuilder = new CSharpPaddingBuilder(context.Host); + } + + public CSharpTagHelperCodeRenderer TagHelperRenderer + { + get + { + if (_tagHelperCodeRenderer == null) + { + _tagHelperCodeRenderer = new CSharpTagHelperCodeRenderer(this, Writer, Context); + } + + return _tagHelperCodeRenderer; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _tagHelperCodeRenderer = value; + } + } + + /// + /// Method used to write an to the current output. + /// + /// Default is to HTML encode all but a few types. + protected virtual string WriteMethodName + { + get + { + return Context.Host.GeneratedClassContext.WriteMethodName; + } + } + + /// + /// Method used to write an to a specified . + /// + /// Default is to HTML encode all but a few types. + protected virtual string WriteToMethodName + { + get + { + return Context.Host.GeneratedClassContext.WriteToMethodName; + } + } + + /// + /// Gets the method name used to generate WriteAttribute invocations in the rendered page. + /// + /// Defaults to + protected virtual string WriteAttributeValueMethodName => + Context.Host.GeneratedClassContext.WriteAttributeValueMethodName; + + protected override void Visit(TagHelperChunk chunk) + { + TagHelperRenderer.RenderTagHelper(chunk); + } + + protected override void Visit(ParentChunk chunk) + { + Accept(chunk.Children); + } + + protected override void Visit(TemplateChunk chunk) + { + Writer.Write(ItemParameterName).Write(" => ") + .WriteStartNewObject(Context.Host.GeneratedClassContext.TemplateTypeName); + + var currentTargetWriterName = Context.TargetWriterName; + Context.TargetWriterName = TemplateWriterName; + + using (Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) + { + Accept(chunk.Children); + } + + Context.TargetWriterName = currentTargetWriterName; + + Writer.WriteEndMethodInvocation(false).WriteLine(); + } + + protected override void Visit(LiteralChunk chunk) + { + if (Context.Host.DesignTimeMode || string.IsNullOrEmpty(chunk.Text)) + { + // Skip generating the chunk if we're in design time or if the chunk is empty. + return; + } + + if (Context.Host.EnableInstrumentation) + { + Writer.WriteStartInstrumentationContext(Context, chunk.Association, isLiteral: true); + } + + if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + RenderPreWriteStart(); + } + + Writer.WriteStringLiteral(chunk.Text); + + if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + Writer.WriteEndMethodInvocation(); + } + + if (Context.Host.EnableInstrumentation) + { + Writer.WriteEndInstrumentationContext(Context); + } + } + + protected override void Visit(ExpressionBlockChunk chunk) + { + if (Context.Host.DesignTimeMode) + { + RenderDesignTimeExpressionBlockChunk(chunk); + } + else + { + RenderRuntimeExpressionBlockChunk(chunk); + } + } + + protected override void Visit(ExpressionChunk chunk) + { + Writer.Write(chunk.Code); + } + + protected override void Visit(StatementChunk chunk) + { + CreateStatementCodeMapping(chunk.Code, chunk); + Writer.WriteLine(); + } + + protected override void Visit(DynamicCodeAttributeChunk chunk) + { + if (Context.Host.DesignTimeMode) + { + // Render the children as is without wrapping them in calls to WriteAttribute + Accept(chunk.Children); + return; + } + + var currentRenderingMode = Context.ExpressionRenderingMode; + var currentTargetWriterName = Context.TargetWriterName; + + CSharpLineMappingWriter lineMappingWriter = null; + var code = chunk.Children.FirstOrDefault(); + if (code is ExpressionChunk || code is ExpressionBlockChunk) + { + // We only want to render the #line pragma if the attribute value will be in-lined. + // Ex: WriteAttributeValue("", 0, DateTime.Now, 0, 0, false) + // For non-inlined scenarios: WriteAttributeValue("", 0, (_) => ..., 0, 0, false) + // the line pragma will be generated inside the lambda. + lineMappingWriter = new CSharpLineMappingWriter(Writer, chunk.Start, Context.SourceFile); + } + + if (!string.IsNullOrEmpty(currentTargetWriterName)) + { + Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteAttributeValueToMethodName) + .Write(currentTargetWriterName) + .WriteParameterSeparator(); + } + else + { + Writer.WriteStartMethodInvocation(WriteAttributeValueMethodName); + } + + Context.TargetWriterName = ValueWriterName; + + if (code is ExpressionChunk || code is ExpressionBlockChunk) + { + Debug.Assert(lineMappingWriter != null); + + Writer + .WriteLocationTaggedString(chunk.Prefix) + .WriteParameterSeparator(); + + Context.ExpressionRenderingMode = ExpressionRenderingMode.InjectCode; + + Accept(code); + + Writer + .WriteParameterSeparator() + .Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .WriteBooleanLiteral(value: false) + .WriteEndMethodInvocation(); + + lineMappingWriter.Dispose(); + } + else + { + Writer + .WriteLocationTaggedString(chunk.Prefix) + .WriteParameterSeparator() + .WriteStartNewObject(Context.Host.GeneratedClassContext.TemplateTypeName); + + using (Writer.BuildLambda(endLine: false, parameterNames: ValueWriterName)) + { + Accept(chunk.Children); + } + + Writer + .WriteEndMethodInvocation(false) + .WriteParameterSeparator() + .Write(chunk.Start.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .WriteBooleanLiteral(false) + .WriteEndMethodInvocation(); + } + + Context.TargetWriterName = currentTargetWriterName; + Context.ExpressionRenderingMode = currentRenderingMode; + } + + protected override void Visit(LiteralCodeAttributeChunk chunk) + { + var visitChildren = chunk.Value == null; + + if (Context.Host.DesignTimeMode) + { + // Render the attribute without wrapping it in a call to WriteAttribute + if (visitChildren) + { + Accept(chunk.Children); + } + + return; + } + + if (!string.IsNullOrEmpty(Context.TargetWriterName)) + { + Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.WriteAttributeValueToMethodName) + .Write(Context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + Writer.WriteStartMethodInvocation(WriteAttributeValueMethodName); + } + + Writer + .WriteLocationTaggedString(chunk.Prefix) + .WriteParameterSeparator(); + + if (visitChildren) + { + var currentRenderingMode = Context.ExpressionRenderingMode; + Context.ExpressionRenderingMode = ExpressionRenderingMode.InjectCode; + + Accept(chunk.Children); + + Context.ExpressionRenderingMode = currentRenderingMode; + + Writer + .WriteParameterSeparator() + .Write(chunk.ValueLocation.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .WriteBooleanLiteral(false) + .WriteEndMethodInvocation(); + } + else + { + Writer + .WriteLocationTaggedString(chunk.Value) + .WriteParameterSeparator() + .Write(chunk.Association.Length.ToString(CultureInfo.CurrentCulture)) + .WriteParameterSeparator() + .WriteBooleanLiteral(true) + .WriteEndMethodInvocation(); + } + } + + protected override void Visit(CodeAttributeChunk chunk) + { + if (Context.Host.DesignTimeMode) + { + // Render the attribute without wrapping it in a "WriteAttribute" invocation + Accept(chunk.Children); + + return; + } + + if (!string.IsNullOrEmpty(Context.TargetWriterName)) + { + Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.BeginWriteAttributeToMethodName) + .Write(Context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.BeginWriteAttributeMethodName); + } + + var attributeCount = chunk.Children.Count(c => c is LiteralCodeAttributeChunk || c is DynamicCodeAttributeChunk); + + Writer.WriteStringLiteral(chunk.Attribute) + .WriteParameterSeparator() + .WriteLocationTaggedString(chunk.Prefix) + .WriteParameterSeparator() + .WriteLocationTaggedString(chunk.Suffix) + .WriteParameterSeparator() + .Write(attributeCount.ToString(CultureInfo.CurrentCulture)) + .WriteEndMethodInvocation(); + + Accept(chunk.Children); + + Writer.WriteMethodInvocation(Context.Host.GeneratedClassContext.EndWriteAttributeMethodName); + } + + protected override void Visit(SectionChunk chunk) + { + Writer.WriteStartMethodInvocation(Context.Host.GeneratedClassContext.DefineSectionMethodName) + .WriteStringLiteral(chunk.Name) + .WriteParameterSeparator(); + + var currentTargetWriterName = Context.TargetWriterName; + Context.TargetWriterName = TemplateWriterName; + + using (Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) + { + Accept(chunk.Children); + } + Context.TargetWriterName = currentTargetWriterName; + Writer.WriteEndMethodInvocation(); + } + + public void RenderDesignTimeExpressionBlockChunk(ExpressionBlockChunk chunk) + { + var firstChild = (ExpressionChunk)chunk.Children.FirstOrDefault(); + + if (firstChild != null) + { + var currentIndent = Writer.CurrentIndent; + var designTimeAssignment = "__o = "; + Writer.ResetIndent(); + + var documentLocation = firstChild.Association.Start; + // This is only here to enable accurate formatting by the C# editor. + Writer.WriteLineNumberDirective(documentLocation, Context.SourceFile); + + // We build the padding with an offset of the design time assignment statement. + Writer.Write(_paddingBuilder.BuildExpressionPadding((Span)firstChild.Association, designTimeAssignment.Length)) + .Write(designTimeAssignment); + + // We map the first line of code but do not write the line pragmas associated with it. + CreateRawCodeMapping(firstChild.Code, documentLocation); + + // Render all but the first child. + // The reason why we render the other children differently is because when formatting the C# code + // the formatter expects the start line to have the assignment statement on it. + Accept(chunk.Children.Skip(1).ToList()); + + Writer.WriteLine(";") + .WriteLine() + .WriteLineDefaultDirective() + .WriteLineHiddenDirective() + .SetIndent(currentIndent); + } + } + + public void RenderRuntimeExpressionBlockChunk(ExpressionBlockChunk chunk) + { + // For expression chunks, such as @value, @(value) etc, pick the first Code or Markup span + // from the expression (in this case "value") and use that to calculate the length. This works + // accurately for most parts. The scenarios that don't work are + // (a) Expressions with inline comments (e.g. @(a @* comment *@ b)) - these have multiple code spans + // (b) Expressions with inline templates (e.g. @Foo(@

Hello world

)). + // Tracked via https://github.com/aspnet/Razor/issues/153 + + var block = (Block)chunk.Association; + var contentSpan = block.Children + .OfType() + .FirstOrDefault(s => s.Kind == SpanKind.Code || s.Kind == SpanKind.Markup); + + if (Context.ExpressionRenderingMode == ExpressionRenderingMode.InjectCode) + { + Accept(chunk.Children); + } + else if (Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + if (contentSpan != null) + { + RenderRuntimeExpressionBlockChunkWithContentSpan(chunk, contentSpan); + } + else + { + if (!string.IsNullOrEmpty(Context.TargetWriterName)) + { + Writer + .WriteStartMethodInvocation(WriteToMethodName) + .Write(Context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + Writer.WriteStartMethodInvocation(WriteMethodName); + } + + Accept(chunk.Children); + + Writer.WriteEndMethodInvocation() + .WriteLine(); + } + } + } + + private void RenderRuntimeExpressionBlockChunkWithContentSpan(ExpressionBlockChunk chunk, Span contentSpan) + { + var generateInstrumentation = ShouldGenerateInstrumentationForExpressions(); + + if (generateInstrumentation) + { + Writer.WriteStartInstrumentationContext(Context, contentSpan, isLiteral: false); + } + + using (var mappingWriter = new CSharpLineMappingWriter(Writer, chunk.Start, Context.SourceFile)) + { + if (!string.IsNullOrEmpty(Context.TargetWriterName)) + { + var generatedStart = + WriteToMethodName.Length + + Context.TargetWriterName.Length + + 3; // 1 for the opening '(' and 2 for ', ' + + var padding = _paddingBuilder.BuildExpressionPadding(contentSpan, generatedStart); + + Writer + .Write(padding) + .WriteStartMethodInvocation(WriteToMethodName) + .Write(Context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + var generatedStart = + WriteMethodName.Length + + 1; // for the opening '(' + + var padding = _paddingBuilder.BuildExpressionPadding(contentSpan, generatedStart); + + Writer + .Write(padding) + .WriteStartMethodInvocation(WriteMethodName); + } + + Accept(chunk.Children); + + Writer.WriteEndMethodInvocation(); + } + + if (generateInstrumentation) + { + Writer.WriteEndInstrumentationContext(Context); + } + } + + public void CreateStatementCodeMapping(string code, Chunk chunk) + { + CreateCodeMapping(_paddingBuilder.BuildStatementPadding((Span)chunk.Association), code, chunk); + } + + public void CreateExpressionCodeMapping(string code, Chunk chunk) + { + CreateCodeMapping(_paddingBuilder.BuildExpressionPadding((Span)chunk.Association), code, chunk); + } + + public void CreateCodeMapping(string padding, string code, Chunk chunk) + { + using (CSharpLineMappingWriter mappingWriter = Writer.BuildLineMapping(chunk.Start, code.Length, Context.SourceFile)) + { + Writer.Write(padding); + + mappingWriter.MarkLineMappingStart(); + Writer.Write(code); + mappingWriter.MarkLineMappingEnd(); + } + } + + // Raw CodeMapping's do not write out line pragmas, they just map code. + public void CreateRawCodeMapping(string code, SourceLocation documentLocation) + { + using (new CSharpLineMappingWriter(Writer, documentLocation, code.Length)) + { + Writer.Write(code); + } + } + + private bool ShouldGenerateInstrumentationForExpressions() + { + // Only generate instrumentation for expression blocks if instrumentation is enabled and we're generating a + // "Write()" statement. + return Context.Host.EnableInstrumentation && + Context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput; + } + + private CSharpCodeWriter RenderPreWriteStart() + { + return RenderPreWriteStart(Writer, Context); + } + + public static CSharpCodeWriter RenderPreWriteStart(CSharpCodeWriter writer, CodeGeneratorContext context) + { + if (!string.IsNullOrEmpty(context.TargetWriterName)) + { + writer.WriteStartMethodInvocation(context.Host.GeneratedClassContext.WriteLiteralToMethodName) + .Write(context.TargetWriterName) + .WriteParameterSeparator(); + } + else + { + writer.WriteStartMethodInvocation(context.Host.GeneratedClassContext.WriteLiteralMethodName); + } + + return writer; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpDesignTimeCodeVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpDesignTimeCodeVisitor.cs new file mode 100644 index 0000000000..3b05fdbc0d --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpDesignTimeCodeVisitor.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CSharpDesignTimeCodeVisitor : CodeVisitor + { + private const string InheritsHelper = "__inheritsHelper"; + private const string DesignTimeHelperMethodName = "__RazorDesignTimeHelpers__"; + private const string TagHelperDirectiveSyntaxHelper = "__tagHelperDirectiveSyntaxHelper"; + private const int DisableVariableNamingWarnings = 219; + + private bool _initializedTagHelperDirectiveSyntaxHelper; + + public CSharpDesignTimeCodeVisitor( + CSharpCodeVisitor csharpCodeVisitor, + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + if (csharpCodeVisitor == null) + { + throw new ArgumentNullException(nameof(csharpCodeVisitor)); + } + + CSharpCodeVisitor = csharpCodeVisitor; + } + + public CSharpCodeVisitor CSharpCodeVisitor { get; } + + public void AcceptTree(ChunkTree tree) + { + if (Context.Host.DesignTimeMode) + { + using (Writer.BuildMethodDeclaration("private", "void", "@" + DesignTimeHelperMethodName)) + { + using (Writer.BuildDisableWarningScope(DisableVariableNamingWarnings)) + { + AcceptTreeCore(tree); + } + } + } + } + + protected virtual void AcceptTreeCore(ChunkTree tree) + { + Accept(tree.Chunks); + } + + protected override void Visit(SetBaseTypeChunk chunk) + { + Debug.Assert(Context.Host.DesignTimeMode); + + if (chunk.Start != SourceLocation.Undefined) + { + using (var lineMappingWriter = + Writer.BuildLineMapping(chunk.Start, chunk.TypeName.Length, Context.SourceFile)) + { + Writer.Indent(chunk.Start.CharacterIndex); + + lineMappingWriter.MarkLineMappingStart(); + Writer.Write(chunk.TypeName); + lineMappingWriter.MarkLineMappingEnd(); + + Writer.Write(" ").Write(InheritsHelper).Write(" = null;"); + } + } + } + + protected override void Visit(TagHelperPrefixDirectiveChunk chunk) + { + VisitTagHelperDirectiveChunk(chunk.Prefix, chunk); + } + + protected override void Visit(AddTagHelperChunk chunk) + { + VisitTagHelperDirectiveChunk(chunk.LookupText, chunk); + } + + protected override void Visit(RemoveTagHelperChunk chunk) + { + VisitTagHelperDirectiveChunk(chunk.LookupText, chunk); + } + + private void VisitTagHelperDirectiveChunk(string text, Chunk chunk) + { + // We should always be in design time mode because of the calling AcceptTree method verification. + Debug.Assert(Context.Host.DesignTimeMode); + + if (!_initializedTagHelperDirectiveSyntaxHelper) + { + _initializedTagHelperDirectiveSyntaxHelper = true; + Writer.WriteVariableDeclaration("string", TagHelperDirectiveSyntaxHelper, "null"); + } + + Writer.WriteStartAssignment(TagHelperDirectiveSyntaxHelper); + + // The parsing mechanism for a TagHelper directive chunk (CSharpCodeParser.TagHelperDirective()) + // removes quotes that surround the text. + CSharpCodeVisitor.CreateExpressionCodeMapping( + string.Format(CultureInfo.InvariantCulture, "\"{0}\"", text), + chunk); + + Writer.WriteLine(";"); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperAttributeValueVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperAttributeValueVisitor.cs new file mode 100644 index 0000000000..cc2f1eee48 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperAttributeValueVisitor.cs @@ -0,0 +1,162 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + /// + /// that writes code for a non- tag helper + /// bound attribute value. + /// + /// + /// Since attribute value is not written out as HTML, does not emit instrumentation. Further this + /// writes identical code at design- and runtime. + /// + public class CSharpTagHelperAttributeValueVisitor : CodeVisitor + { + private string _attributeTypeName; + private bool _firstChild; + + /// + /// Initializes a new instance of the class. + /// + /// The used to write code. + /// + /// A instance that contains information about the current code generation + /// process. + /// + /// + /// Full name of the property for which this + /// is writing the value. + /// + public CSharpTagHelperAttributeValueVisitor( + CSharpCodeWriter writer, + CodeGeneratorContext context, + string attributeTypeName) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _attributeTypeName = attributeTypeName; + } + + /// + /// Writes code for the given . + /// + /// The to render. + /// + /// Tracks code mappings for all children while writing. + /// + protected override void Visit(ParentChunk chunk) + { + // Line mappings are captured in RenderCode(), not this method. + _firstChild = true; + Accept(chunk.Children); + + if (_firstChild) + { + // Attribute value was empty. + Context.ErrorSink.OnError( + chunk.Association.Start, + RazorResources.TagHelpers_AttributeExpressionRequired, + chunk.Association.Length); + } + } + + /// + /// Writes code for the given . + /// + /// The to render. + protected override void Visit(ExpressionBlockChunk chunk) + { + Accept(chunk.Children); + } + + /// + /// Writes code for the given . + /// + /// The to render. + protected override void Visit(ExpressionChunk chunk) + { + RenderCode(chunk.Code, (Span)chunk.Association); + } + + /// + /// Writes code for the given . + /// + /// The to render. + protected override void Visit(LiteralChunk chunk) + { + RenderCode(chunk.Text, (Span)chunk.Association); + } + + /// + /// Writes code for the given . + /// + /// The to render. + /// + /// Unconditionally adds a to inform user of unexpected @section directive. + /// + protected override void Visit(SectionChunk chunk) + { + Context.ErrorSink.OnError( + chunk.Association.Start, + RazorResources.FormatTagHelpers_Directives_NotSupported_InAttributes( + SyntaxConstants.CSharp.SectionKeyword), + chunk.Association.Length); + } + + /// + /// Writes code for the given . + /// + /// The to render. + /// + /// Unconditionally adds a to inform user of unexpected code block. + /// + protected override void Visit(StatementChunk chunk) + { + Context.ErrorSink.OnError( + chunk.Association.Start, + RazorResources.TagHelpers_CodeBlocks_NotSupported_InAttributes, + chunk.Association.Length); + } + + /// + /// Writes code for the given . + /// + /// The to render. + /// + /// Unconditionally adds a to inform user of unexpected template e.g. + /// @<p>paragraph@</p>. + /// + protected override void Visit(TemplateChunk chunk) + { + Context.ErrorSink.OnError( + chunk.Association.Start, + RazorResources.FormatTagHelpers_InlineMarkupBlocks_NotSupported_InAttributes(_attributeTypeName), + chunk.Association.Length); + } + + // Tracks the code mapping and writes code for a leaf node in the attribute value Chunk tree. + private void RenderCode(string code, Span association) + { + _firstChild = false; + using (new CSharpLineMappingWriter(Writer, association.Start, code.Length)) + { + Writer.Write(code); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs new file mode 100644 index 0000000000..0d9436ea4b --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperFieldDeclarationVisitor.cs @@ -0,0 +1,123 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CSharpTagHelperFieldDeclarationVisitor : CodeVisitor + { + private readonly HashSet _declaredTagHelpers; + private readonly GeneratedTagHelperContext _tagHelperContext; + private bool _foundTagHelpers; + + public CSharpTagHelperFieldDeclarationVisitor(CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _declaredTagHelpers = new HashSet(StringComparer.Ordinal); + _tagHelperContext = Context.Host.GeneratedClassContext.GeneratedTagHelperContext; + } + + protected override void Visit(TagHelperChunk chunk) + { + // We only want to setup tag helper manager fields if there are tag helpers, and only once + if (!_foundTagHelpers) + { + _foundTagHelpers = true; + + // We want to hide declared TagHelper fields so they cannot be stepped over via a debugger. + Writer.WriteLineHiddenDirective(); + + // Runtime fields aren't useful during design time. + if (!Context.Host.DesignTimeMode) + { + // Need to disable the warning "X is assigned to but never used." for the value buffer since + // whether it's used depends on how a TagHelper is used. + Writer.WritePragma("warning disable 0414"); + WritePrivateField(_tagHelperContext.TagHelperContentTypeName, + CSharpTagHelperCodeRenderer.StringValueBufferVariableName, + value: null); + Writer.WritePragma("warning restore 0414"); + + WritePrivateField(_tagHelperContext.ExecutionContextTypeName, + CSharpTagHelperCodeRenderer.ExecutionContextVariableName, + value: null); + + Writer + .Write("private ") + .WriteVariableDeclaration( + _tagHelperContext.RunnerTypeName, + CSharpTagHelperCodeRenderer.RunnerVariableName, + value: null); + + Writer.Write("private ") + .Write(_tagHelperContext.ScopeManagerTypeName) + .Write(" ") + .WriteStartAssignment(CSharpTagHelperCodeRenderer.ScopeManagerVariableName) + .WriteStartNewObject(_tagHelperContext.ScopeManagerTypeName) + .WriteEndMethodInvocation(); + } + } + + foreach (var descriptor in chunk.Descriptors) + { + if (!_declaredTagHelpers.Contains(descriptor.TypeName)) + { + _declaredTagHelpers.Add(descriptor.TypeName); + + WritePrivateField(descriptor.TypeName, + CSharpTagHelperCodeRenderer.GetVariableName(descriptor), + value: null); + } + } + + // We need to dive deeper to ensure we pick up any nested tag helpers. + Accept(chunk.Children); + } + + public override void Accept(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + var parentChunk = chunk as ParentChunk; + + // If we're any ParentChunk other than TagHelperChunk then we want to dive into its Children + // to search for more TagHelperChunk chunks. This if-statement enables us to not override + // each of the special ParentChunk types and then dive into their children. + if (parentChunk != null && !(parentChunk is TagHelperChunk)) + { + Accept(parentChunk.Children); + } + else + { + // If we're a TagHelperChunk or any other non ParentChunk we ".Accept" it. This ensures + // that our overridden Visit(TagHelperChunk) method gets called and is not skipped over. + // If we're a non ParentChunk or a TagHelperChunk then we want to just invoke the Visit + // method for that given chunk (base.Accept indirectly calls the Visit method). + base.Accept(chunk); + } + } + + private void WritePrivateField(string type, string name, string value) + { + Writer.Write("private ") + .WriteVariableDeclaration(type, name, value); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperRunnerInitializationVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperRunnerInitializationVisitor.cs new file mode 100644 index 0000000000..ec885464fb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTagHelperRunnerInitializationVisitor.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + /// + /// The that generates the code to initialize the TagHelperRunner. + /// + public class CSharpTagHelperRunnerInitializationVisitor : CodeVisitor + { + private readonly GeneratedTagHelperContext _tagHelperContext; + private bool _foundTagHelpers; + + /// + /// Creates a new instance of . + /// + /// The used to generate code. + /// The . + public CSharpTagHelperRunnerInitializationVisitor(CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _tagHelperContext = Context.Host.GeneratedClassContext.GeneratedTagHelperContext; + } + + /// + public override void Accept(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + // If at any ParentChunk other than a TagHelperChunk, then dive into its Children to search for more + // TagHelperChunk nodes. This method avoids overriding each of the ParentChunk-specific Visit() methods to + // dive into Children. + var parentChunk = chunk as ParentChunk; + if (parentChunk != null && !(parentChunk is TagHelperChunk)) + { + Accept(parentChunk.Children); + } + else + { + // If at a TagHelperChunk or any non-ParentChunk, "Accept()" it. This ensures the Visit(TagHelperChunk) + // method below is called. + base.Accept(chunk); + } + } + + /// + /// Writes the TagHelperRunner initialization code to the Writer. + /// + /// The . + protected override void Visit(TagHelperChunk chunk) + { + if (!_foundTagHelpers && !Context.Host.DesignTimeMode) + { + _foundTagHelpers = true; + + Writer + .WriteStartAssignment(CSharpTagHelperCodeRenderer.RunnerVariableName) + .Write(CSharpTagHelperCodeRenderer.RunnerVariableName) + .Write(" ?? ") + .WriteStartNewObject(_tagHelperContext.RunnerTypeName) + .WriteEndMethodInvocation(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTypeMemberVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTypeMemberVisitor.cs new file mode 100644 index 0000000000..77656f9aa5 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpTypeMemberVisitor.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CSharpTypeMemberVisitor : CodeVisitor + { + private CSharpCodeVisitor _csharpCodeVisitor; + + public CSharpTypeMemberVisitor(CSharpCodeVisitor csharpCodeVisitor, + CSharpCodeWriter writer, + CodeGeneratorContext context) + : base(writer, context) + { + if (csharpCodeVisitor == null) + { + throw new ArgumentNullException(nameof(csharpCodeVisitor)); + } + + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + _csharpCodeVisitor = csharpCodeVisitor; + } + + protected override void Visit(TypeMemberChunk chunk) + { + if (!string.IsNullOrEmpty(chunk.Code)) + { + _csharpCodeVisitor.CreateCodeMapping(string.Empty, chunk.Code, chunk); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpUsingVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpUsingVisitor.cs new file mode 100644 index 0000000000..e776e6c7ce --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CSharpUsingVisitor.cs @@ -0,0 +1,117 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CSharpUsingVisitor : CodeVisitor + { + private static readonly string[] TagHelpersRuntimeNamespaces = new[] + { + "Microsoft.AspNet.Razor.TagHelpers", + "Microsoft.AspNet.Razor.Runtime.TagHelpers" + }; + + private bool _foundTagHelpers; + + public CSharpUsingVisitor(CSharpCodeWriter writer, CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + ImportedUsings = new HashSet(StringComparer.Ordinal); + } + + public HashSet ImportedUsings { get; set; } + + /// + public override void Accept(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + // If at any ParentChunk other than a TagHelperChunk, then dive into its Children to search for more + // TagHelperChunk or UsingChunk nodes. This method avoids overriding each of the ParentChunk-specific + // Visit() methods to dive into Children. + var parentChunk = chunk as ParentChunk; + if (parentChunk != null && !(parentChunk is TagHelperChunk)) + { + Accept(parentChunk.Children); + } + else + { + // If at a TagHelperChunk or any non-ParentChunk (e.g. UsingChunk), "Accept()" it. This ensures the + // Visit(UsingChunk) and Visit(TagHelperChunk) methods below are called. + base.Accept(chunk); + } + } + + protected override void Visit(UsingChunk chunk) + { + var documentContent = ((Span)chunk.Association).Content.Trim(); + var mapSemicolon = false; + + if (documentContent.LastOrDefault() == ';') + { + mapSemicolon = true; + } + + ImportedUsings.Add(chunk.Namespace); + + // Depending on if the user has a semicolon in their @using statement we have to conditionally decide + // to include the semicolon in the line mapping. + using (Writer.BuildLineMapping(chunk.Start, documentContent.Length, Context.SourceFile)) + { + Writer.WriteUsing(chunk.Namespace, endLine: false); + + if (mapSemicolon) + { + Writer.Write(";"); + } + } + + if (!mapSemicolon) + { + Writer.WriteLine(";"); + } + } + + protected override void Visit(TagHelperChunk chunk) + { + if (Context.Host.DesignTimeMode) + { + return; + } + + if (!_foundTagHelpers) + { + _foundTagHelpers = true; + + foreach (var tagHelperRuntimeNamespace in TagHelpersRuntimeNamespaces) + { + if (ImportedUsings.Add(tagHelperRuntimeNamespace)) + { + // If we find TagHelpers then we need to add the TagHelper runtime namespaces to our list of + // usings. + Writer.WriteUsing(tagHelperRuntimeNamespace); + } + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/ChunkVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/ChunkVisitor.cs new file mode 100644 index 0000000000..1f79be0b17 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/ChunkVisitor.cs @@ -0,0 +1,140 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public abstract class ChunkVisitor : IChunkVisitor + where TWriter : CodeWriter + { + public ChunkVisitor(TWriter writer, CodeGeneratorContext context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + Writer = writer; + Context = context; + } + + protected TWriter Writer { get; private set; } + protected CodeGeneratorContext Context { get; private set; } + + public void Accept(IList chunks) + { + if (chunks == null) + { + throw new ArgumentNullException(nameof(chunks)); + } + + foreach (Chunk chunk in chunks) + { + Accept(chunk); + } + } + + public virtual void Accept(Chunk chunk) + { + if (chunk == null) + { + throw new ArgumentNullException(nameof(chunk)); + } + + if (chunk is LiteralChunk) + { + Visit((LiteralChunk)chunk); + } + else if (chunk is ExpressionBlockChunk) + { + Visit((ExpressionBlockChunk)chunk); + } + else if (chunk is ExpressionChunk) + { + Visit((ExpressionChunk)chunk); + } + else if (chunk is StatementChunk) + { + Visit((StatementChunk)chunk); + } + else if (chunk is TagHelperChunk) + { + Visit((TagHelperChunk)chunk); + } + else if (chunk is TagHelperPrefixDirectiveChunk) + { + Visit((TagHelperPrefixDirectiveChunk)chunk); + } + else if (chunk is AddTagHelperChunk) + { + Visit((AddTagHelperChunk)chunk); + } + else if (chunk is RemoveTagHelperChunk) + { + Visit((RemoveTagHelperChunk)chunk); + } + else if (chunk is TypeMemberChunk) + { + Visit((TypeMemberChunk)chunk); + } + else if (chunk is UsingChunk) + { + Visit((UsingChunk)chunk); + } + else if (chunk is SetBaseTypeChunk) + { + Visit((SetBaseTypeChunk)chunk); + } + else if (chunk is DynamicCodeAttributeChunk) + { + Visit((DynamicCodeAttributeChunk)chunk); + } + else if (chunk is LiteralCodeAttributeChunk) + { + Visit((LiteralCodeAttributeChunk)chunk); + } + else if (chunk is CodeAttributeChunk) + { + Visit((CodeAttributeChunk)chunk); + } + else if (chunk is SectionChunk) + { + Visit((SectionChunk)chunk); + } + else if (chunk is TemplateChunk) + { + Visit((TemplateChunk)chunk); + } + else if (chunk is ParentChunk) + { + Visit((ParentChunk)chunk); + } + } + + protected abstract void Visit(LiteralChunk chunk); + protected abstract void Visit(ExpressionChunk chunk); + protected abstract void Visit(StatementChunk chunk); + protected abstract void Visit(TagHelperChunk chunk); + protected abstract void Visit(TagHelperPrefixDirectiveChunk chunk); + protected abstract void Visit(AddTagHelperChunk chunk); + protected abstract void Visit(RemoveTagHelperChunk chunk); + protected abstract void Visit(UsingChunk chunk); + protected abstract void Visit(ParentChunk chunk); + protected abstract void Visit(DynamicCodeAttributeChunk chunk); + protected abstract void Visit(LiteralCodeAttributeChunk chunk); + protected abstract void Visit(CodeAttributeChunk chunk); + protected abstract void Visit(SectionChunk chunk); + protected abstract void Visit(TypeMemberChunk chunk); + protected abstract void Visit(SetBaseTypeChunk chunk); + protected abstract void Visit(TemplateChunk chunk); + protected abstract void Visit(ExpressionBlockChunk chunk); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CodeVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CodeVisitor.cs new file mode 100644 index 0000000000..c55ada4e65 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/CodeVisitor.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public class CodeVisitor : ChunkVisitor + where TWriter : CodeWriter + { + public CodeVisitor(TWriter writer, CodeGeneratorContext context) + : base(writer, context) + { + if (writer == null) + { + throw new ArgumentNullException(nameof(writer)); + } + + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + } + + protected override void Visit(LiteralChunk chunk) + { + } + protected override void Visit(ExpressionBlockChunk chunk) + { + } + protected override void Visit(ExpressionChunk chunk) + { + } + protected override void Visit(StatementChunk chunk) + { + } + protected override void Visit(UsingChunk chunk) + { + } + protected override void Visit(ParentChunk chunk) + { + } + protected override void Visit(DynamicCodeAttributeChunk chunk) + { + } + protected override void Visit(TagHelperChunk chunk) + { + } + protected override void Visit(TagHelperPrefixDirectiveChunk chunk) + { + } + protected override void Visit(AddTagHelperChunk chunk) + { + } + protected override void Visit(RemoveTagHelperChunk chunk) + { + } + protected override void Visit(LiteralCodeAttributeChunk chunk) + { + } + protected override void Visit(CodeAttributeChunk chunk) + { + } + protected override void Visit(SectionChunk chunk) + { + } + protected override void Visit(TypeMemberChunk chunk) + { + } + protected override void Visit(SetBaseTypeChunk chunk) + { + } + protected override void Visit(TemplateChunk chunk) + { + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/IChunkVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/IChunkVisitor.cs new file mode 100644 index 0000000000..f7b84c6da8 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CodeGenerators/Visitors/IChunkVisitor.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Chunks; + +namespace Microsoft.AspNet.Razor.CodeGenerators.Visitors +{ + public interface IChunkVisitor + { + void Accept(IList chunks); + void Accept(Chunk chunk); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/CommonResources.resx b/src/Microsoft.AspNet.Razor.VSRC1/CommonResources.resx new file mode 100644 index 0000000000..7cc5d5ecd7 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/CommonResources.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Value cannot be null or an empty string. + + + Value must be between {0} and {1}. + + + Value must be a value from the "{0}" enumeration. + + + Value must be greater than {0}. + + + Value must be greater than or equal to {0}. + + + Value must be less than {0}. + + + Value must be less than or equal to {0}. + + + Value cannot be an empty string. It must either be null or a non-empty string. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/ITagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/ITagHelperDescriptorResolver.cs new file mode 100644 index 0000000000..7bf86b1f25 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/ITagHelperDescriptorResolver.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// Contract used to resolve s. + /// + public interface ITagHelperDescriptorResolver + { + /// + /// Resolves s based on the given . + /// + /// + /// used to resolve descriptors for the Razor page. + /// + /// An of s based + /// on the given . + IEnumerable Resolve(TagHelperDescriptorResolutionContext resolutionContext); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperAttributeDescriptor.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperAttributeDescriptor.cs new file mode 100644 index 0000000000..1634e2d7d1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperAttributeDescriptor.cs @@ -0,0 +1,150 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// A metadata class describing a tag helper attribute. + /// + public class TagHelperAttributeDescriptor + { + private string _typeName; + private string _name; + private string _propertyName; + + /// + /// Instantiates a new instance of the class. + /// + public TagHelperAttributeDescriptor() + { + } + + // Internal for testing i.e. for easy TagHelperAttributeDescriptor creation when PropertyInfo is available. + internal TagHelperAttributeDescriptor(string name, PropertyInfo propertyInfo) + { + Name = name; + PropertyName = propertyInfo.Name; + TypeName = propertyInfo.PropertyType.FullName; + } + + /// + /// Gets an indication whether this is used for dictionary indexer + /// assignments. + /// + /// + /// If true this should be associated with all HTML + /// attributes that have names starting with . Otherwise this + /// is used for property assignment and is only associated with an + /// HTML attribute that has the exact . + /// + /// + /// HTML attribute names are matched case-insensitively, regardless of . + /// + public bool IsIndexer { get; set; } + + /// + /// Gets or sets an indication whether this property is of type or, if + /// is true, whether the indexer's value is of type . + /// + /// + /// If true the is for . This causes the Razor parser + /// to allow empty values for HTML attributes matching this . If + /// false empty values for such matching attributes lead to errors. + /// + public bool IsStringProperty { get; set; } + + /// + /// The HTML attribute name or, if is true, the prefix for matching attribute + /// names. + /// + public string Name + { + get + { + return _name; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _name = value; + } + } + + + /// + /// The name of the CLR property that corresponds to the HTML attribute. + /// + public string PropertyName + { + get + { + return _propertyName; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _propertyName = value; + } + } + + /// + /// The full name of the named (see ) property's or, if + /// is true, the full name of the indexer's value . + /// + public string TypeName + { + get + { + return _typeName; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _typeName = value; + IsStringProperty = string.Equals(TypeName, typeof(string).FullName, StringComparison.Ordinal); + } + } + + /// + /// The that contains design time information about + /// this attribute. + /// + public TagHelperAttributeDesignTimeDescriptor DesignTimeDescriptor { get; set; } + + /// + /// Determines whether HTML attribute matches this + /// . + /// + /// Name of the HTML attribute to check. + /// + /// true if this matches . + /// false otherwise. + /// + public bool IsNameMatch(string name) + { + if (IsIndexer) + { + return name.StartsWith(Name, StringComparison.OrdinalIgnoreCase); + } + else + { + return string.Equals(name, Name, StringComparison.OrdinalIgnoreCase); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperAttributeDesignTimeDescriptor.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperAttributeDesignTimeDescriptor.cs new file mode 100644 index 0000000000..29298cac99 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperAttributeDesignTimeDescriptor.cs @@ -0,0 +1,21 @@ +// 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.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// A metadata class containing information about tag helper use. + /// + public class TagHelperAttributeDesignTimeDescriptor + { + /// + /// A summary of how to use a tag helper. + /// + public string Summary { get; set; } + + /// + /// Remarks about how to use a tag helper. + /// + public string Remarks { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptor.cs new file mode 100644 index 0000000000..1a98a9a18f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptor.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// A metadata class describing a tag helper. + /// + public class TagHelperDescriptor + { + private string _prefix = string.Empty; + private string _tagName; + private string _typeName; + private string _assemblyName; + private IEnumerable _attributes = + Enumerable.Empty(); + private IEnumerable _requiredAttributes = Enumerable.Empty(); + + /// + /// Text used as a required prefix when matching HTML start and end tags in the Razor source to available + /// tag helpers. + /// + public string Prefix + { + get + { + return _prefix; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _prefix = value; + } + } + + /// + /// The tag name that the tag helper should target. + /// + public string TagName + { + get + { + return _tagName; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _tagName = value; + } + } + + /// + /// The full tag name that is required for the tag helper to target an HTML element. + /// + /// This is equivalent to and concatenated. + public string FullTagName + { + get + { + return Prefix + TagName; + } + } + + /// + /// The full name of the tag helper class. + /// + public string TypeName + { + get + { + return _typeName; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _typeName = value; + } + } + + /// + /// The name of the assembly containing the tag helper class. + /// + public string AssemblyName + { + get + { + return _assemblyName; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _assemblyName = value; + } + } + + /// + /// The list of attributes the tag helper expects. + /// + public IEnumerable Attributes + { + get + { + return _attributes; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _attributes = value; + } + } + + /// + /// The list of required attribute names the tag helper expects to target an element. + /// + /// + /// * at the end of an attribute name acts as a prefix match. + /// + public IEnumerable RequiredAttributes + { + get + { + return _requiredAttributes; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _requiredAttributes = value; + } + } + + /// + /// Get the names of elements allowed as children. + /// + /// null indicates all children are allowed. + public IEnumerable AllowedChildren { get; set; } + + /// + /// Get the name of the HTML element required as the immediate parent. + /// + /// null indicates no restriction on parent tag. + public string RequiredParent { get; set; } + + /// + /// The expected tag structure. + /// + /// + /// If and no other tag helpers applying to the same element specify + /// their the behavior is used: + /// + /// + /// <my-tag-helper></my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// Otherwise, if another tag helper applying to the same element does specify their behavior, that behavior + /// is used. + /// + /// + /// If HTML elements can be written in the following formats: + /// + /// <my-tag-helper> + /// <!-- OR --> + /// <my-tag-helper /> + /// + /// + /// + public TagStructure TagStructure { get; set; } + + /// + /// The that contains design time information about this + /// tag helper. + /// + public TagHelperDesignTimeDescriptor DesignTimeDescriptor { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorComparer.cs new file mode 100644 index 0000000000..62f57d9e9f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorComparer.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// An used to check equality between + /// two s. + /// + public class TagHelperDescriptorComparer : IEqualityComparer + { + /// + /// A default instance of the . + /// + public static readonly TagHelperDescriptorComparer Default = new TagHelperDescriptorComparer(); + + /// + /// Initializes a new instance. + /// + protected TagHelperDescriptorComparer() + { + } + + /// + /// + /// Determines equality based on , + /// , , + /// , , + /// and . + /// Ignores because it can be inferred directly from + /// and . + /// + public virtual bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + return descriptorX != null && + string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) && + string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) && + string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) && + string.Equals( + descriptorX.RequiredParent, + descriptorY.RequiredParent, + StringComparison.OrdinalIgnoreCase) && + Enumerable.SequenceEqual( + descriptorX.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase), + descriptorY.RequiredAttributes.OrderBy(attribute => attribute, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase) && + (descriptorX.AllowedChildren == descriptorY.AllowedChildren || + (descriptorX.AllowedChildren != null && + descriptorY.AllowedChildren != null && + Enumerable.SequenceEqual( + descriptorX.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase), + descriptorY.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase), + StringComparer.OrdinalIgnoreCase))) && + descriptorX.TagStructure == descriptorY.TagStructure; + } + + /// + public virtual int GetHashCode(TagHelperDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.TagName, StringComparer.OrdinalIgnoreCase); + hashCodeCombiner.Add(descriptor.AssemblyName, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.RequiredParent, StringComparer.OrdinalIgnoreCase); + hashCodeCombiner.Add(descriptor.TagStructure); + + var attributes = descriptor.RequiredAttributes.OrderBy( + attribute => attribute, + StringComparer.OrdinalIgnoreCase); + foreach (var attribute in attributes) + { + hashCodeCombiner.Add(attribute, StringComparer.OrdinalIgnoreCase); + } + + if (descriptor.AllowedChildren != null) + { + var allowedChildren = descriptor.AllowedChildren.OrderBy(child => child, StringComparer.OrdinalIgnoreCase); + foreach (var child in allowedChildren) + { + hashCodeCombiner.Add(child, StringComparer.OrdinalIgnoreCase); + } + } + + return hashCodeCombiner.CombinedHash; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorProvider.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorProvider.cs new file mode 100644 index 0000000000..f50a63f007 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorProvider.cs @@ -0,0 +1,155 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// Enables retrieval of 's. + /// + public class TagHelperDescriptorProvider + { + public const string ElementCatchAllTarget = "*"; + + public static readonly string RequiredAttributeWildcardSuffix = "*"; + + private IDictionary> _registrations; + private string _tagHelperPrefix; + + /// + /// Instantiates a new instance of the . + /// + /// The descriptors that the will pull from. + public TagHelperDescriptorProvider(IEnumerable descriptors) + { + _registrations = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // Populate our registrations + foreach (var descriptor in descriptors) + { + Register(descriptor); + } + } + + /// + /// Gets all tag helpers that match the given . + /// + /// The name of the HTML tag to match. Providing a '*' tag name + /// retrieves catch-all s (descriptors that target every tag). + /// Attributes the HTML element must contain to match. + /// The parent tag name of the given tag. + /// s that apply to the given . + /// Will return an empty if no s are + /// found. + public IEnumerable GetDescriptors( + string tagName, + IEnumerable attributeNames, + string parentTagName) + { + if (!string.IsNullOrEmpty(_tagHelperPrefix) && + (tagName.Length <= _tagHelperPrefix.Length || + !tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase))) + { + // The tagName doesn't have the tag helper prefix, we can short circuit. + return Enumerable.Empty(); + } + + HashSet catchAllDescriptors; + IEnumerable descriptors; + + // Ensure there's a HashSet to use. + if (!_registrations.TryGetValue(ElementCatchAllTarget, out catchAllDescriptors)) + { + descriptors = new HashSet(TagHelperDescriptorComparer.Default); + } + else + { + descriptors = catchAllDescriptors; + } + + // If we have a tag name associated with the requested name, we need to combine matchingDescriptors + // with all the catch-all descriptors. + HashSet matchingDescriptors; + if (_registrations.TryGetValue(tagName, out matchingDescriptors)) + { + descriptors = matchingDescriptors.Concat(descriptors); + } + + var applicableDescriptors = ApplyRequiredAttributes(descriptors, attributeNames); + applicableDescriptors = ApplyParentTagFilter(applicableDescriptors, parentTagName); + + return applicableDescriptors; + } + + private IEnumerable ApplyParentTagFilter( + IEnumerable descriptors, + string parentTagName) + { + return descriptors.Where(descriptor => + descriptor.RequiredParent == null || + string.Equals(parentTagName, descriptor.RequiredParent, StringComparison.OrdinalIgnoreCase)); + } + + private IEnumerable ApplyRequiredAttributes( + IEnumerable descriptors, + IEnumerable attributeNames) + { + return descriptors.Where( + descriptor => + { + foreach (var requiredAttribute in descriptor.RequiredAttributes) + { + // '*' at the end of a required attribute indicates: apply to attributes prefixed with the + // required attribute value. + if (requiredAttribute.EndsWith( + RequiredAttributeWildcardSuffix, + StringComparison.OrdinalIgnoreCase)) + { + var prefix = requiredAttribute.Substring(0, requiredAttribute.Length - 1); + + if (!attributeNames.Any( + attributeName => + attributeName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && + !string.Equals(attributeName, prefix, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + } + else if (!attributeNames.Contains(requiredAttribute, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + + return true; + }); + } + + private void Register(TagHelperDescriptor descriptor) + { + HashSet descriptorSet; + + if (_tagHelperPrefix == null) + { + _tagHelperPrefix = descriptor.Prefix; + } + + var registrationKey = + string.Equals(descriptor.TagName, ElementCatchAllTarget, StringComparison.Ordinal) ? + ElementCatchAllTarget : + descriptor.FullTagName; + + // Ensure there's a HashSet to add the descriptor to. + if (!_registrations.TryGetValue(registrationKey, out descriptorSet)) + { + descriptorSet = new HashSet(TagHelperDescriptorComparer.Default); + _registrations[registrationKey] = descriptorSet; + } + + descriptorSet.Add(descriptor); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorResolutionContext.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorResolutionContext.cs new file mode 100644 index 0000000000..880f6c937e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDescriptorResolutionContext.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// Contains information needed to resolve s. + /// + public class TagHelperDescriptorResolutionContext + { + // Internal for testing purposes + internal TagHelperDescriptorResolutionContext(IEnumerable directiveDescriptors) + : this(directiveDescriptors, new ErrorSink()) + { + } + + /// + /// Instantiates a new instance of . + /// + /// s used to resolve + /// s. + /// Used to aggregate s. + public TagHelperDescriptorResolutionContext( + IEnumerable directiveDescriptors, + ErrorSink errorSink) + { + if (directiveDescriptors == null) + { + throw new ArgumentNullException(nameof(directiveDescriptors)); + } + + if (errorSink == null) + { + throw new ArgumentNullException(nameof(errorSink)); + } + + DirectiveDescriptors = new List(directiveDescriptors); + ErrorSink = errorSink; + } + + /// + /// s used to resolve s. + /// + public IList DirectiveDescriptors { get; private set; } + + /// + /// Used to aggregate s. + /// + public ErrorSink ErrorSink { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDesignTimeDescriptor.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDesignTimeDescriptor.cs new file mode 100644 index 0000000000..5cdff88a41 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDesignTimeDescriptor.cs @@ -0,0 +1,29 @@ +// 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.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// A metadata class containing design time information about a tag helper. + /// + public class TagHelperDesignTimeDescriptor + { + /// + /// A summary of how to use a tag helper. + /// + public string Summary { get; set; } + + /// + /// Remarks about how to use a tag helper. + /// + public string Remarks { get; set; } + + /// + /// The HTML element a tag helper may output. + /// + /// + /// In IDEs supporting IntelliSense, may override the HTML information provided at design time. + /// + public string OutputElementHint { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDirectiveDescriptor.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDirectiveDescriptor.cs new file mode 100644 index 0000000000..b372457b03 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDirectiveDescriptor.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// Contains information needed to resolve s. + /// + public class TagHelperDirectiveDescriptor + { + private string _directiveText; + + /// + /// A used to find tag helper s. + /// + public string DirectiveText + { + get + { + return _directiveText; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _directiveText = value; + } + } + + /// + /// The of the directive. + /// + public SourceLocation Location { get; set; } = SourceLocation.Zero; + + /// + /// The of this directive. + /// + public TagHelperDirectiveType DirectiveType { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDirectiveType.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDirectiveType.cs new file mode 100644 index 0000000000..d2cec61c63 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TagHelperDirectiveType.cs @@ -0,0 +1,26 @@ +// 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.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// The type of tag helper directive. + /// + public enum TagHelperDirectiveType + { + /// + /// An @addTagHelper directive. + /// + AddTagHelper, + + /// + /// A @removeTagHelper directive. + /// + RemoveTagHelper, + + /// + /// A @tagHelperPrefix directive. + /// + TagHelperPrefix + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TypeBasedTagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TypeBasedTagHelperDescriptorComparer.cs new file mode 100644 index 0000000000..d89e54ad7b --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Compilation/TagHelpers/TypeBasedTagHelperDescriptorComparer.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Compilation.TagHelpers +{ + /// + /// An that checks equality between two + /// s using only their s and + /// s. + /// + /// + /// This class is intended for scenarios where Reflection-based information is all important i.e. + /// , , and related + /// properties are not relevant. + /// + public class TypeBasedTagHelperDescriptorComparer : IEqualityComparer + { + /// + /// A default instance of the . + /// + public static readonly TypeBasedTagHelperDescriptorComparer Default = + new TypeBasedTagHelperDescriptorComparer(); + + private TypeBasedTagHelperDescriptorComparer() + { + } + + /// + /// + /// Determines equality based on and + /// . + /// + public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) + { + if (descriptorX == descriptorY) + { + return true; + } + + return descriptorX != null && + string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal) && + string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal); + } + + /// + public int GetHashCode(TagHelperDescriptor descriptor) + { + if (descriptor == null) + { + throw new ArgumentNullException(nameof(descriptor)); + } + + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(descriptor.AssemblyName, StringComparer.Ordinal); + hashCodeCombiner.Add(descriptor.TypeName, StringComparer.Ordinal); + + return hashCodeCombiner; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/DocumentParseCompleteEventArgs.cs b/src/Microsoft.AspNet.Razor.VSRC1/DocumentParseCompleteEventArgs.cs new file mode 100644 index 0000000000..898d7b0828 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/DocumentParseCompleteEventArgs.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor +{ + /// + /// Arguments for the DocumentParseComplete event in RazorEditorParser + /// + public class DocumentParseCompleteEventArgs : EventArgs + { + /// + /// Indicates if the tree structure has actually changed since the previous re-parse. + /// + public bool TreeStructureChanged { get; set; } + + /// + /// The results of the chunk generation and parsing + /// + public GeneratorResults GeneratorResults { get; set; } + + /// + /// The TextChange which triggered the re-parse + /// + public TextChange SourceChange { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/AutoCompleteEditHandler.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/AutoCompleteEditHandler.cs new file mode 100644 index 0000000000..115882ec45 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/AutoCompleteEditHandler.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Parser.SyntaxTree +{ + public class AutoCompleteEditHandler : SpanEditHandler + { + private static readonly int TypeHashCode = typeof(AutoCompleteEditHandler).GetHashCode(); + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public AutoCompleteEditHandler(Func> tokenizer) + : base(tokenizer) + { + } + + public AutoCompleteEditHandler(Func> tokenizer, bool autoCompleteAtEndOfSpan) + : this(tokenizer) + { + AutoCompleteAtEndOfSpan = autoCompleteAtEndOfSpan; + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public AutoCompleteEditHandler(Func> tokenizer, AcceptedCharacters accepted) + : base(tokenizer, accepted) + { + } + + public bool AutoCompleteAtEndOfSpan { get; } + + public string AutoCompleteString { get; set; } + + protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, normalizedChange)) || IsAtEndOfFirstLine(target, normalizedChange)) && + normalizedChange.IsInsert && + ParserHelpers.IsNewLine(normalizedChange.NewText) && + AutoCompleteString != null) + { + return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock; + } + return PartialParseResult.Rejected; + } + + public override string ToString() + { + return base.ToString() + ",AutoComplete:[" + (AutoCompleteString ?? "") + "]" + (AutoCompleteAtEndOfSpan ? ";AtEnd" : ";AtEOL"); + } + + public override bool Equals(object obj) + { + var other = obj as AutoCompleteEditHandler; + return base.Equals(other) && + string.Equals(other.AutoCompleteString, AutoCompleteString, StringComparison.Ordinal) && + AutoCompleteAtEndOfSpan == other.AutoCompleteAtEndOfSpan; + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties but Equals also checks the type. + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(TypeHashCode); + hashCodeCombiner.Add(AutoCompleteAtEndOfSpan); + + return hashCodeCombiner.CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/BackgroundParser.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/BackgroundParser.cs new file mode 100644 index 0000000000..9ecb8d9779 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/BackgroundParser.cs @@ -0,0 +1,493 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Utils; + +namespace Microsoft.AspNet.Razor.Editor +{ + internal class BackgroundParser : IDisposable + { + private MainThreadState _main; + private BackgroundThread _bg; + + public BackgroundParser(RazorEngineHost host, string fileName) + { + _main = new MainThreadState(fileName); + _bg = new BackgroundThread(_main, host, fileName); + + _main.ResultsReady += (sender, args) => OnResultsReady(args); + } + + /// + /// Fired on the main thread. + /// + public event EventHandler ResultsReady; + + public bool IsIdle + { + get { return _main.IsIdle; } + } + + public void Start() + { + _bg.Start(); + } + + public void Cancel() + { + _main.Cancel(); + } + + public void QueueChange(TextChange change) + { + _main.QueueChange(change); + } + + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_main", Justification = "MainThreadState is disposed when the background thread shuts down")] + public void Dispose() + { + _main.Cancel(); + } + + public IDisposable SynchronizeMainThreadState() + { + return _main.Lock(); + } + + protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args) + { + var handler = ResultsReady; + if (handler != null) + { + handler(this, args); + } + } + + internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable changes) + { + return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None); + } + + internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable changes, CancellationToken cancelToken) + { + // Apply all the pending changes to the original tree + // PERF: If this becomes a bottleneck, we can probably do it the other way around, + // i.e. visit the tree and find applicable changes for each node. + foreach (TextChange change in changes) + { + cancelToken.ThrowIfCancellationRequested(); + var changeOwner = leftTree.LocateOwner(change); + + // Apply the change to the tree + if (changeOwner == null) + { + return true; + } + var result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true); + changeOwner.ReplaceWith(result.EditedSpan); + } + + // Now compare the trees + var treesDifferent = !leftTree.EquivalentTo(rightTree); + return treesDifferent; + } + + private abstract class ThreadStateBase + { +#if DEBUG + private int _id = -1; +#endif + protected ThreadStateBase() + { + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")] + [Conditional("DEBUG")] + protected void SetThreadId(int id) + { +#if DEBUG + _id = id; +#endif + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")] + [Conditional("DEBUG")] + protected void EnsureOnThread() + { +#if DEBUG + Debug.Assert(_id != -1, "SetThreadId was never called!"); + Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!"); +#endif + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")] + [Conditional("DEBUG")] + protected void EnsureNotOnThread() + { +#if DEBUG + Debug.Assert(_id != -1, "SetThreadId was never called!"); + Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!"); +#endif + } + } + + private class MainThreadState : ThreadStateBase, IDisposable + { + private readonly CancellationTokenSource _cancelSource = new CancellationTokenSource(); + private readonly ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false); + private CancellationTokenSource _currentParcelCancelSource; + + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Field is used in debug code and may be used later")] + private string _fileName; + private readonly object _stateLock = new object(); + private IList _changes = new List(); + + public MainThreadState(string fileName) + { + _fileName = fileName; + + SetThreadId(Thread.CurrentThread.ManagedThreadId); + } + + public event EventHandler ResultsReady; + + public CancellationToken CancelToken + { + get { return _cancelSource.Token; } + } + + public bool IsIdle + { + get + { + lock (_stateLock) + { + return _currentParcelCancelSource == null; + } + } + } + + public void Cancel() + { + EnsureOnThread(); + _cancelSource.Cancel(); + } + + public IDisposable Lock() + { + Monitor.Enter(_stateLock); + return new DisposableAction(() => Monitor.Exit(_stateLock)); + } + + public void QueueChange(TextChange change) + { + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_QueuingParse(Path.GetFileName(_fileName), change)); + EnsureOnThread(); + lock (_stateLock) + { + // CurrentParcel token source is not null ==> There's a parse underway + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Cancel(); + } + + _changes.Add(change); + _hasParcel.Set(); + } + } + + public WorkParcel GetParcel() + { + EnsureNotOnThread(); // Only the background thread can get a parcel + _hasParcel.Wait(_cancelSource.Token); + _hasParcel.Reset(); + lock (_stateLock) + { + // Create a cancellation source for this parcel + _currentParcelCancelSource = new CancellationTokenSource(); + + var changes = _changes; + _changes = new List(); + return new WorkParcel(changes, _currentParcelCancelSource.Token); + } + } + + public void ReturnParcel(DocumentParseCompleteEventArgs args) + { + lock (_stateLock) + { + // Clear the current parcel cancellation source + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Dispose(); + _currentParcelCancelSource = null; + } + + // If there are things waiting to be parsed, just don't fire the event because we're already out of date + if (_changes.Any()) + { + return; + } + } + var handler = ResultsReady; + if (handler != null) + { + handler(this, args); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Dispose(); + _currentParcelCancelSource = null; + } + _cancelSource.Dispose(); + _hasParcel.Dispose(); + } + } + } + + private class BackgroundThread : ThreadStateBase + { + private MainThreadState _main; + private Thread _backgroundThread; + private CancellationToken _shutdownToken; + private RazorEngineHost _host; + private string _fileName; + private Block _currentParseTree; + private IList _previouslyDiscarded = new List(); + + public BackgroundThread(MainThreadState main, RazorEngineHost host, string fileName) + { + // Run on MAIN thread! + _main = main; + _backgroundThread = new Thread(WorkerLoop); + _shutdownToken = _main.CancelToken; + _host = host; + _fileName = fileName; + + SetThreadId(_backgroundThread.ManagedThreadId); + } + + // **** ANY THREAD **** + public void Start() + { + _backgroundThread.Start(); + } + + // **** BACKGROUND THREAD **** + private void WorkerLoop() + { + long? elapsedMs = null; + var fileNameOnly = Path.GetFileName(_fileName); +#if EDITOR_TRACING + var sw = new Stopwatch(); +#endif + + try + { + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_BackgroundThreadStart(fileNameOnly)); + EnsureOnThread(); + +#if DOTNET5_4 + var spinWait = new SpinWait(); +#endif + + while (!_shutdownToken.IsCancellationRequested) + { + // Grab the parcel of work to do + var parcel = _main.GetParcel(); + if (parcel.Changes.Any()) + { + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_ChangesArrived(fileNameOnly, parcel.Changes.Count)); + try + { + DocumentParseCompleteEventArgs args = null; + using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken)) + { + if (!linkedCancel.IsCancellationRequested) + { + // Collect ALL changes +#if EDITOR_TRACING + if (_previouslyDiscarded != null && _previouslyDiscarded.Any()) + { + RazorEditorTrace.TraceLine(RazorResources.Trace_CollectedDiscardedChanges, fileNameOnly, _previouslyDiscarded.Count); + } +#endif + List allChanges; + + if (_previouslyDiscarded != null) + { + allChanges = Enumerable.Concat(_previouslyDiscarded, parcel.Changes).ToList(); + } + else + { + allChanges = parcel.Changes.ToList(); + } + + var finalChange = allChanges.Last(); +#if EDITOR_TRACING + sw.Start(); +#endif + var results = ParseChange(finalChange.NewBuffer, linkedCancel.Token); +#if EDITOR_TRACING + sw.Stop(); + elapsedMs = sw.ElapsedMilliseconds; + sw.Reset(); +#endif + RazorEditorTrace.TraceLine( + RazorResources.FormatTrace_ParseComplete( + fileNameOnly, + elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?")); + + if (results != null && !linkedCancel.IsCancellationRequested) + { + // Clear discarded changes list + _previouslyDiscarded = null; + + // Take the current tree and check for differences +#if EDITOR_TRACING + sw.Start(); +#endif + var treeStructureChanged = _currentParseTree == null || TreesAreDifferent(_currentParseTree, results.Document, allChanges, parcel.CancelToken); +#if EDITOR_TRACING + sw.Stop(); + elapsedMs = sw.ElapsedMilliseconds; + sw.Reset(); +#endif + _currentParseTree = results.Document; + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_TreesCompared( + fileNameOnly, + elapsedMs.HasValue ? elapsedMs.Value.ToString(CultureInfo.InvariantCulture) : "?", + treeStructureChanged)); + + // Build Arguments + args = new DocumentParseCompleteEventArgs() + { + GeneratorResults = results, + SourceChange = finalChange, + TreeStructureChanged = treeStructureChanged + }; + } + else + { + // Parse completed but we were cancelled in the mean time. Add these to the discarded changes set + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_ChangesDiscarded(fileNameOnly, allChanges.Count)); + _previouslyDiscarded = allChanges; + } + +#if CHECK_TREE + if (args != null) + { + // Rewind the buffer and sanity check the line mappings + finalChange.NewBuffer.Position = 0; + var lineCount = finalChange.NewBuffer.ReadToEnd().Split(new string[] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.None).Count(); + Debug.Assert( + !args.GeneratorResults.DesignTimeLineMappings.Any(pair => pair.Value.StartLine > lineCount), + "Found a design-time line mapping referring to a line outside the source file!"); + Debug.Assert( + !args.GeneratorResults.Document.Flatten().Any(span => span.Start.LineIndex > lineCount), + "Found a span with a line number outside the source file"); + Debug.Assert( + !args.GeneratorResults.Document.Flatten().Any(span => span.Start.AbsoluteIndex > parcel.NewBuffer.Length), + "Found a span with an absolute offset outside the source file"); + } +#endif + } + } + if (args != null) + { + _main.ReturnParcel(args); + } + } + catch (OperationCanceledException) + { + } + } + else + { + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_NoChangesArrived(fileNameOnly)); +#if DOTNET5_4 + // This does the equivalent of thread.yield under the covers. + spinWait.SpinOnce(); +#else + // No Yield in CoreCLR + + Thread.Yield(); +#endif + } + } + } + catch (OperationCanceledException) + { + // Do nothing. Just shut down. + } + finally + { + RazorEditorTrace.TraceLine(RazorResources.FormatTrace_BackgroundThreadShutdown(fileNameOnly)); + + // Clean up main thread resources + _main.Dispose(); + } + } + + private GeneratorResults ParseChange(ITextBuffer buffer, CancellationToken token) + { + EnsureOnThread(); + + // Create a template engine + var engine = new RazorTemplateEngine(_host); + + // Seek the buffer to the beginning + buffer.Position = 0; + + try + { + return engine.GenerateCode( + input: buffer, + className: null, + rootNamespace: null, + sourceFileName: _fileName, + cancelToken: token); + } + catch (OperationCanceledException) + { + return null; + } + } + } + + private class WorkParcel + { + public WorkParcel(IList changes, CancellationToken cancelToken) + { + Changes = changes; + CancelToken = cancelToken; + } + + public CancellationToken CancelToken { get; private set; } + public IList Changes { get; private set; } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/EditResult.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/EditResult.cs new file mode 100644 index 0000000000..f491f3ddf4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/EditResult.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Editor +{ + public class EditResult + { + public EditResult(PartialParseResult result, SpanBuilder editedSpan) + { + Result = result; + EditedSpan = editedSpan; + } + + public PartialParseResult Result { get; set; } + public SpanBuilder EditedSpan { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/EditorHints.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/EditorHints.cs new file mode 100644 index 0000000000..53f8c63982 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/EditorHints.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.Editor +{ + /// + /// Used within . + /// + [Flags] + public enum EditorHints + { + /// + /// The default (Markup or Code) editor behavior for Statement completion should be used. + /// Editors can always use the default behavior, even if the span is labeled with a different CompletionType. + /// + None = 0, // 0000 0000 + + /// + /// Indicates that Virtual Path completion should be used for this span if the editor supports it. + /// Editors need not support this mode of completion, and will use the default () behavior + /// if they do not support it. + /// + VirtualPath = 1, // 0000 0001 + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/ImplicitExpressionEditHandler.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/ImplicitExpressionEditHandler.cs new file mode 100644 index 0000000000..16deff101a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/ImplicitExpressionEditHandler.cs @@ -0,0 +1,327 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Editor +{ + public class ImplicitExpressionEditHandler : SpanEditHandler + { + private readonly ISet _keywords; + private readonly IReadOnlyCollection _readOnlyKeywords; + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public ImplicitExpressionEditHandler(Func> tokenizer, ISet keywords, bool acceptTrailingDot) + : base(tokenizer) + { + _keywords = keywords ?? new HashSet(); + + // HashSet implements IReadOnlyCollection as of 4.6, but does not for 4.5.1. If the runtime cast + // succeeds, avoid creating a new collection. + _readOnlyKeywords = (_keywords as IReadOnlyCollection) ?? _keywords.ToArray(); + + AcceptTrailingDot = acceptTrailingDot; + } + + public bool AcceptTrailingDot { get; } + + public IReadOnlyCollection Keywords + { + get + { + return _readOnlyKeywords; + } + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0};ImplicitExpression[{1}];K{2}", base.ToString(), AcceptTrailingDot ? "ATD" : "RTD", Keywords.Count); + } + + public override bool Equals(object obj) + { + var other = obj as ImplicitExpressionEditHandler; + return base.Equals(other) && + _keywords.SetEquals(other._keywords) && + AcceptTrailingDot == other.AcceptTrailingDot; + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties and base has none. + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Keywords); + hashCodeCombiner.Add(AcceptTrailingDot); + + return hashCodeCombiner; + } + + protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + if (AcceptedCharacters == AcceptedCharacters.Any) + { + return PartialParseResult.Rejected; + } + + // In some editors intellisense insertions are handled as "dotless commits". If an intellisense selection is confirmed + // via something like '.' a dotless commit will append a '.' and then insert the remaining intellisense selection prior + // to the appended '.'. This 'if' statement attempts to accept the intermediate steps of a dotless commit via + // intellisense. It will accept two cases: + // 1. '@foo.' -> '@foobaz.'. + // 2. '@foobaz..' -> '@foobaz.bar.'. Includes Sub-cases '@foobaz()..' -> '@foobaz().bar.' etc. + // The key distinction being the double '.' in the second case. + if (IsDotlessCommitInsertion(target, normalizedChange)) + { + return HandleDotlessCommitInsertion(target); + } + + if (IsAcceptableReplace(target, normalizedChange)) + { + return HandleReplacement(target, normalizedChange); + } + var changeRelativePosition = normalizedChange.OldPosition - target.Start.AbsoluteIndex; + + // Get the edit context + char? lastChar = null; + if (changeRelativePosition > 0 && target.Content.Length > 0) + { + lastChar = target.Content[changeRelativePosition - 1]; + } + + // Don't support 0->1 length edits + if (lastChar == null) + { + return PartialParseResult.Rejected; + } + + // Accepts cases when insertions are made at the end of a span or '.' is inserted within a span. + if (IsAcceptableInsertion(target, normalizedChange)) + { + // Handle the insertion + return HandleInsertion(target, lastChar.Value, normalizedChange); + } + + if (IsAcceptableDeletion(target, normalizedChange)) + { + return HandleDeletion(target, lastChar.Value, normalizedChange); + } + + return PartialParseResult.Rejected; + } + + // A dotless commit is the process of inserting a '.' with an intellisense selection. + private static bool IsDotlessCommitInsertion(Span target, TextChange change) + { + return IsNewDotlessCommitInsertion(target, change) || IsSecondaryDotlessCommitInsertion(target, change); + } + + // Completing 'DateTime' in intellisense with a '.' could result in: '@DateT' -> '@DateT.' -> '@DateTime.' which is accepted. + private static bool IsNewDotlessCommitInsertion(Span target, TextChange change) + { + return !IsAtEndOfSpan(target, change) && + change.NewPosition > 0 && + change.NewLength > 0 && + target.Content.Last() == '.' && + ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false) && + (change.OldLength == 0 || ParserHelpers.IsIdentifier(change.OldText, requireIdentifierStart: false)); + } + + // Once a dotless commit has been performed you then have something like '@DateTime.'. This scenario is used to detect the + // situation when you try to perform another dotless commit resulting in a textchange with '..'. Completing 'DateTime.Now' + // in intellisense with a '.' could result in: '@DateTime.' -> '@DateTime..' -> '@DateTime.Now.' which is accepted. + private static bool IsSecondaryDotlessCommitInsertion(Span target, TextChange change) + { + // Do not need to worry about other punctuation, just looking for double '.' (after change) + return change.NewLength == 1 && + !string.IsNullOrEmpty(target.Content) && + target.Content.Last() == '.' && + change.NewText == "." && + change.OldLength == 0; + } + + private static bool IsAcceptableReplace(Span target, TextChange change) + { + return IsEndReplace(target, change) || + (change.IsReplace && RemainingIsWhitespace(target, change)); + } + + private static bool IsAcceptableDeletion(Span target, TextChange change) + { + return IsEndDeletion(target, change) || + (change.IsDelete && RemainingIsWhitespace(target, change)); + } + + // Acceptable insertions can occur at the end of a span or when a '.' is inserted within a span. + private static bool IsAcceptableInsertion(Span target, TextChange change) + { + return change.IsInsert && + (IsAcceptableEndInsertion(target, change) || + IsAcceptableInnerInsertion(target, change)); + } + + // Accepts character insertions at the end of spans. AKA: '@foo' -> '@fooo' or '@foo' -> '@foo ' etc. + private static bool IsAcceptableEndInsertion(Span target, TextChange change) + { + Debug.Assert(change.IsInsert); + + return IsAtEndOfSpan(target, change) || + RemainingIsWhitespace(target, change); + } + + // Accepts '.' insertions in the middle of spans. Ex: '@foo.baz.bar' -> '@foo..baz.bar' + // This is meant to allow intellisense when editing a span. + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "target", Justification = "The 'target' parameter is used in Debug to validate that the function is called in the correct context.")] + private static bool IsAcceptableInnerInsertion(Span target, TextChange change) + { + Debug.Assert(change.IsInsert); + + // Ensure that we're actually inserting in the middle of a span and not at the end. + // This case will fail if the IsAcceptableEndInsertion does not capture an end insertion correctly. + Debug.Assert(!IsAtEndOfSpan(target, change)); + + return change.NewPosition > 0 && + change.NewText == "."; + } + + private static bool RemainingIsWhitespace(Span target, TextChange change) + { + var offset = (change.OldPosition - target.Start.AbsoluteIndex) + change.OldLength; + return string.IsNullOrWhiteSpace(target.Content.Substring(offset)); + } + + private PartialParseResult HandleDotlessCommitInsertion(Span target) + { + var result = PartialParseResult.Accepted; + if (!AcceptTrailingDot && target.Content.LastOrDefault() == '.') + { + result |= PartialParseResult.Provisional; + } + return result; + } + + private PartialParseResult HandleReplacement(Span target, TextChange change) + { + // Special Case for IntelliSense commits. + // When IntelliSense commits, we get two changes (for example user typed "Date", then committed "DateTime" by pressing ".") + // 1. Insert "." at the end of this span + // 2. Replace the "Date." at the end of the span with "DateTime." + // We need partial parsing to accept case #2. + var oldText = GetOldText(target, change); + + var result = PartialParseResult.Rejected; + if (EndsWithDot(oldText) && EndsWithDot(change.NewText)) + { + result = PartialParseResult.Accepted; + if (!AcceptTrailingDot) + { + result |= PartialParseResult.Provisional; + } + } + return result; + } + + private PartialParseResult HandleDeletion(Span target, char previousChar, TextChange change) + { + // What's left after deleting? + if (previousChar == '.') + { + return TryAcceptChange(target, change, PartialParseResult.Accepted | PartialParseResult.Provisional); + } + else if (ParserHelpers.IsIdentifierPart(previousChar)) + { + return TryAcceptChange(target, change); + } + else + { + return PartialParseResult.Rejected; + } + } + + private PartialParseResult HandleInsertion(Span target, char previousChar, TextChange change) + { + // What are we inserting after? + if (previousChar == '.') + { + return HandleInsertionAfterDot(target, change); + } + else if (ParserHelpers.IsIdentifierPart(previousChar) || previousChar == ')' || previousChar == ']') + { + return HandleInsertionAfterIdPart(target, change); + } + else + { + return PartialParseResult.Rejected; + } + } + + private PartialParseResult HandleInsertionAfterIdPart(Span target, TextChange change) + { + // If the insertion is a full identifier part, accept it + if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false)) + { + return TryAcceptChange(target, change); + } + else if (EndsWithDot(change.NewText)) + { + // Accept it, possibly provisionally + var result = PartialParseResult.Accepted; + if (!AcceptTrailingDot) + { + result |= PartialParseResult.Provisional; + } + return TryAcceptChange(target, change, result); + } + else + { + return PartialParseResult.Rejected; + } + } + + private static bool EndsWithDot(string content) + { + return (content.Length == 1 && content[0] == '.') || + (content[content.Length - 1] == '.' && + content.Take(content.Length - 1).All(ParserHelpers.IsIdentifierPart)); + } + + private PartialParseResult HandleInsertionAfterDot(Span target, TextChange change) + { + // If the insertion is a full identifier or another dot, accept it + if (ParserHelpers.IsIdentifier(change.NewText) || change.NewText == ".") + { + return TryAcceptChange(target, change); + } + return PartialParseResult.Rejected; + } + + private PartialParseResult TryAcceptChange(Span target, TextChange change, PartialParseResult acceptResult = PartialParseResult.Accepted) + { + var content = change.ApplyChange(target); + if (StartsWithKeyword(content)) + { + return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged; + } + + return acceptResult; + } + + private bool StartsWithKeyword(string newContent) + { + using (var reader = new StringReader(newContent)) + { + return _keywords.Contains(reader.ReadWhile(ParserHelpers.IsIdentifierPart)); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/RazorEditorTrace.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/RazorEditorTrace.cs new file mode 100644 index 0000000000..80fa645819 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/RazorEditorTrace.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +#if NET451 +using System.Globalization; +#endif + +namespace Microsoft.AspNet.Razor.Editor +{ + internal static class RazorEditorTrace + { + private static bool? _enabled; + + private static bool IsEnabled() + { + if (_enabled == null) + { + bool enabled; + if (Boolean.TryParse(Environment.GetEnvironmentVariable("RAZOR_EDITOR_TRACE"), out enabled)) + { +#if NET451 + // No Trace in CoreCLR + + Trace.WriteLine(RazorResources.FormatTrace_Startup( + enabled ? RazorResources.Trace_Enabled : RazorResources.Trace_Disabled)); +#endif + _enabled = enabled; + } + else + { + _enabled = false; + } + } + return _enabled.Value; + } + + [Conditional("EDITOR_TRACING")] + public static void TraceLine(string format, params object[] args) + { + if (IsEnabled()) + { +#if NET451 + // No Trace in CoreCLR + + Trace.WriteLine(RazorResources.FormatTrace_Format( + string.Format(CultureInfo.CurrentCulture, format, args))); +#endif + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/SingleLineMarkupEditHandler.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/SingleLineMarkupEditHandler.cs new file mode 100644 index 0000000000..b2b7289aeb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/SingleLineMarkupEditHandler.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Editor +{ + public class SingleLineMarkupEditHandler : SpanEditHandler + { + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SingleLineMarkupEditHandler(Func> tokenizer) + : base(tokenizer) + { + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SingleLineMarkupEditHandler(Func> tokenizer, AcceptedCharacters accepted) + : base(tokenizer, accepted) + { + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Editor/SpanEditHandler.cs b/src/Microsoft.AspNet.Razor.VSRC1/Editor/SpanEditHandler.cs new file mode 100644 index 0000000000..3931c2262b --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Editor/SpanEditHandler.cs @@ -0,0 +1,185 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Editor +{ + // Manages edits to a span + public class SpanEditHandler + { + private static readonly int TypeHashCode = typeof(SpanEditHandler).GetHashCode(); + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SpanEditHandler(Func> tokenizer) + : this(tokenizer, AcceptedCharacters.Any) + { + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SpanEditHandler(Func> tokenizer, AcceptedCharacters accepted) + { + AcceptedCharacters = accepted; + Tokenizer = tokenizer; + } + + public AcceptedCharacters AcceptedCharacters { get; set; } + + /// + /// Provides a set of hints to editors which may be manipulating the document in which this span is located. + /// + public EditorHints EditorHints { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public Func> Tokenizer { get; set; } + + public static SpanEditHandler CreateDefault() + { + return CreateDefault(s => Enumerable.Empty()); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public static SpanEditHandler CreateDefault(Func> tokenizer) + { + return new SpanEditHandler(tokenizer); + } + + public virtual EditResult ApplyChange(Span target, TextChange change) + { + return ApplyChange(target, change, force: false); + } + + public virtual EditResult ApplyChange(Span target, TextChange change, bool force) + { + var result = PartialParseResult.Accepted; + var normalized = change.Normalize(); + if (!force) + { + result = CanAcceptChange(target, normalized); + } + + // If the change is accepted then apply the change + if ((result & PartialParseResult.Accepted) == PartialParseResult.Accepted) + { + return new EditResult(result, UpdateSpan(target, normalized)); + } + return new EditResult(result, new SpanBuilder(target)); + } + + public virtual bool OwnsChange(Span target, TextChange change) + { + var end = target.Start.AbsoluteIndex + target.Length; + var changeOldEnd = change.OldPosition + change.OldLength; + return change.OldPosition >= target.Start.AbsoluteIndex && + (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharacters.None)); + } + + protected virtual PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + return PartialParseResult.Rejected; + } + + protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange) + { + var newContent = normalizedChange.ApplyChange(target); + var newSpan = new SpanBuilder(target); + newSpan.ClearSymbols(); + foreach (ISymbol sym in Tokenizer(newContent)) + { + sym.OffsetStart(target.Start); + newSpan.Accept(sym); + } + if (target.Next != null) + { + var newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); + target.Next.ChangeStart(newEnd); + } + return newSpan; + } + + protected internal static bool IsAtEndOfFirstLine(Span target, TextChange change) + { + var endOfFirstLine = target.Content.IndexOfAny(new char[] { (char)0x000d, (char)0x000a, (char)0x2028, (char)0x2029 }); + return (endOfFirstLine == -1 || (change.OldPosition - target.Start.AbsoluteIndex) <= endOfFirstLine); + } + + /// + /// Returns true if the specified change is an insertion of text at the end of this span. + /// + protected internal static bool IsEndInsertion(Span target, TextChange change) + { + return change.IsInsert && IsAtEndOfSpan(target, change); + } + + /// + /// Returns true if the specified change is an insertion of text at the end of this span. + /// + protected internal static bool IsEndDeletion(Span target, TextChange change) + { + return change.IsDelete && IsAtEndOfSpan(target, change); + } + + /// + /// Returns true if the specified change is a replacement of text at the end of this span. + /// + protected internal static bool IsEndReplace(Span target, TextChange change) + { + return change.IsReplace && IsAtEndOfSpan(target, change); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method should only be used on Spans")] + protected internal static bool IsAtEndOfSpan(Span target, TextChange change) + { + return (change.OldPosition + change.OldLength) == (target.Start.AbsoluteIndex + target.Length); + } + + /// + /// Returns the old text referenced by the change. + /// + /// + /// If the content has already been updated by applying the change, this data will be _invalid_ + /// + protected internal static string GetOldText(Span target, TextChange change) + { + return target.Content.Substring(change.OldPosition - target.Start.AbsoluteIndex, change.OldLength); + } + + // Is the specified span to the right of this span and immediately adjacent? + internal static bool IsAdjacentOnRight(Span target, Span other) + { + return target.Start.AbsoluteIndex < other.Start.AbsoluteIndex && target.Start.AbsoluteIndex + target.Length == other.Start.AbsoluteIndex; + } + + // Is the specified span to the left of this span and immediately adjacent? + internal static bool IsAdjacentOnLeft(Span target, Span other) + { + return other.Start.AbsoluteIndex < target.Start.AbsoluteIndex && other.Start.AbsoluteIndex + other.Length == target.Start.AbsoluteIndex; + } + + public override string ToString() + { + return GetType().Name + ";Accepts:" + AcceptedCharacters + ((EditorHints == EditorHints.None) ? string.Empty : (";Hints: " + EditorHints.ToString())); + } + + public override bool Equals(object obj) + { + var other = obj as SpanEditHandler; + return other != null && + GetType() == other.GetType() && + AcceptedCharacters == other.AcceptedCharacters && + EditorHints == other.EditorHints; + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties but Equals also checks the type. + return TypeHashCode; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/ErrorSink.cs b/src/Microsoft.AspNet.Razor.VSRC1/ErrorSink.cs new file mode 100644 index 0000000000..8177f6ccaf --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/ErrorSink.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor +{ + /// + /// Used to manage s encountered during the Razor parsing phase. + /// + public class ErrorSink + { + private readonly List _errors; + + /// + /// Instantiates a new instance of . + /// + public ErrorSink() + { + _errors = new List(); + } + + /// + /// s collected. + /// + public IEnumerable Errors + { + get + { + return _errors; + } + } + + /// + /// Tracks the given . + /// + /// The to track. + public void OnError(RazorError error) + { + _errors.Add(error); + } + + /// + /// Creates and tracks a new . + /// + /// of the error. + /// A message describing the error. + /// The length of the error. + public void OnError(SourceLocation location, string message, int length) + { + _errors.Add(new RazorError(message, location, length)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/GlobalSuppressions.cs b/src/Microsoft.AspNet.Razor.VSRC1/GlobalSuppressions.cs new file mode 100644 index 0000000000..c67d845ecb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/GlobalSuppressions.cs @@ -0,0 +1,22 @@ +// 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. + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Error List, point to "Suppress Message(s)", and click +// "In Project Suppression File". +// You do not need to add suppressions to this file manually. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "br", Scope = "resource", Target = "Microsoft.AspNet.Razor.Resources.RazorResources.resources", Justification = "Resource is referencing html tag")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.AspNet.Razor.Tokenizer", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.AspNet.Razor.Text", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.AspNet.Razor.Parser", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.AspNet.Razor.Editor", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "Microsoft.AspNet.Razor", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] diff --git a/src/Microsoft.AspNet.Razor.VSRC1/HashCodeCombiner.cs b/src/Microsoft.AspNet.Razor.VSRC1/HashCodeCombiner.cs new file mode 100644 index 0000000000..4df8b46b05 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/HashCodeCombiner.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Internal +{ + internal struct HashCodeCombiner + { + private long _combinedHash64; + + public int CombinedHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { return _combinedHash64.GetHashCode(); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private HashCodeCombiner(long seed) + { + _combinedHash64 = seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(IEnumerable e) + { + if (e == null) + { + Add(0); + } + else + { + var count = 0; + foreach (object o in e) + { + Add(o); + count++; + } + Add(count); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator int(HashCodeCombiner self) + { + return self.CombinedHash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(string s) + { + var hashCode = (s != null) ? s.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(object o) + { + var hashCode = (o != null) ? o.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TValue value, IEqualityComparer comparer) + { + var hashCode = value != null ? comparer.GetHashCode(value) : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(0x1505L); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Microsoft.AspNet.Razor.VSRC1.xproj b/src/Microsoft.AspNet.Razor.VSRC1/Microsoft.AspNet.Razor.VSRC1.xproj new file mode 100644 index 0000000000..dcbe59743f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Microsoft.AspNet.Razor.VSRC1.xproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + ab5abc37-201b-41ff-9faf-e948b0d33f5a + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/BalancingModes.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/BalancingModes.cs new file mode 100644 index 0000000000..ab2b13dbc4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/BalancingModes.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Parser +{ + [Flags] + public enum BalancingModes + { + None = 0, + BacktrackOnFailure = 1, + NoErrorOnFailure = 2, + AllowCommentsAndTemplates = 4, + AllowEmbeddedTransitions = 8 + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Directives.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Directives.cs new file mode 100644 index 0000000000..c241e160d3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Directives.cs @@ -0,0 +1,360 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser + { + private void SetupDirectives() + { + MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword); + MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword); + MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword); + MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword); + MapDirectives(FunctionsDirective, SyntaxConstants.CSharp.FunctionsKeyword); + MapDirectives(SectionDirective, SyntaxConstants.CSharp.SectionKeyword); + } + + protected virtual void TagHelperPrefixDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + prefix => new TagHelperPrefixDirectiveChunkGenerator(prefix)); + } + + protected virtual void AddTagHelperDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.AddTagHelperKeyword, + lookupText => + new AddOrRemoveTagHelperChunkGenerator(removeTagHelperDescriptors: false, lookupText: lookupText)); + } + + protected virtual void RemoveTagHelperDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.RemoveTagHelperKeyword, + lookupText => + new AddOrRemoveTagHelperChunkGenerator(removeTagHelperDescriptors: true, lookupText: lookupText)); + } + + protected virtual void SectionDirective() + { + var nested = Context.IsWithin(BlockType.Section); + var errorReported = false; + + // Set the block and span type + Context.CurrentBlock.Type = BlockType.Section; + + // Verify we're on "section" and accept + AssertDirective(SyntaxConstants.CSharp.SectionKeyword); + var startLocation = CurrentLocation; + AcceptAndMoveNext(); + + if (nested) + { + Context.OnError( + startLocation, + RazorResources.FormatParseError_Sections_Cannot_Be_Nested(RazorResources.SectionExample_CS), + Span.GetContent().Value.Length); + errorReported = true; + } + + var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + // Get the section name + var sectionName = string.Empty; + if (!Required(CSharpSymbolType.Identifier, + errorIfNotFound: true, + errorBase: RazorResources.FormatParseError_Unexpected_Character_At_Section_Name_Start)) + { + if (!errorReported) + { + errorReported = true; + } + + PutCurrentBack(); + PutBack(whitespace); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false)); + } + else + { + Accept(whitespace); + sectionName = CurrentSymbol.Content; + AcceptAndMoveNext(); + } + Context.CurrentBlock.ChunkGenerator = new SectionChunkGenerator(sectionName); + + var errorLocation = CurrentLocation; + whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + // Get the starting brace + var sawStartingBrace = At(CSharpSymbolType.LeftBrace); + if (!sawStartingBrace) + { + if (!errorReported) + { + errorReported = true; + Context.OnError( + errorLocation, + RazorResources.ParseError_MissingOpenBraceAfterSection, + length: 1 /* { */); + } + + PutCurrentBack(); + PutBack(whitespace); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false)); + Optional(CSharpSymbolType.NewLine); + Output(SpanKind.MetaCode); + CompleteBlock(); + return; + } + else + { + Accept(whitespace); + } + + // Set up edit handler + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString, autoCompleteAtEndOfSpan: true); + + Span.EditHandler = editHandler; + Span.Accept(CurrentSymbol); + + // Output Metacode then switch to section parser + Output(SpanKind.MetaCode); + SectionBlock("{", "}", caseSensitive: true); + + Span.ChunkGenerator = SpanChunkGenerator.Null; + // Check for the terminating "}" + if (!Optional(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.OnError( + CurrentLocation, + RazorResources.FormatParseError_Expected_X(Language.GetSample(CSharpSymbolType.RightBrace)), + length: 1 /* } */); + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true); + Output(SpanKind.MetaCode); + return; + } + + protected virtual void FunctionsDirective() + { + // Set the block type + Context.CurrentBlock.Type = BlockType.Functions; + + // Verify we're on "functions" and accept + AssertDirective(SyntaxConstants.CSharp.FunctionsKeyword); + var block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + if (!At(CSharpSymbolType.LeftBrace)) + { + Context.OnError( + CurrentLocation, + RazorResources.FormatParseError_Expected_X(Language.GetSample(CSharpSymbolType.LeftBrace)), + length: 1 /* { */); + CompleteBlock(); + Output(SpanKind.MetaCode); + return; + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + // Capture start point and continue + var blockStart = CurrentLocation; + AcceptAndMoveNext(); + + // Output what we've seen and continue + Output(SpanKind.MetaCode); + + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + Span.EditHandler = editHandler; + + Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, blockStart); + Span.ChunkGenerator = new TypeMemberChunkGenerator(); + if (!At(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.OnError( + blockStart, + RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, "}", "{"), + length: 1 /* } */); + CompleteBlock(); + Output(SpanKind.Code); + } + else + { + Output(SpanKind.Code); + Assert(CSharpSymbolType.RightBrace); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + AcceptAndMoveNext(); + CompleteBlock(); + Output(SpanKind.MetaCode); + } + } + + protected virtual void InheritsDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(SyntaxConstants.CSharp.InheritsKeyword); + AcceptAndMoveNext(); + + InheritsDirectiveCore(); + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "directive", Justification = "This only occurs in Release builds, where this method is empty by design")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")] + [Conditional("DEBUG")] + protected void AssertDirective(string directive) + { + Assert(CSharpSymbolType.Identifier); + Debug.Assert(string.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal)); + } + + protected void InheritsDirectiveCore() + { + BaseTypeDirective( + RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, + baseType => new SetBaseTypeChunkGenerator(baseType)); + } + + protected void BaseTypeDirective(string noTypeNameError, Func createChunkGenerator) + { + var keywordStartLocation = Span.Start; + + // Set the block type + Context.CurrentBlock.Type = BlockType.Directive; + + var keywordLength = Span.GetContent().Value.Length; + + // Accept whitespace + var remainingWhitespace = AcceptSingleWhiteSpaceCharacter(); + + if (Span.Symbols.Count > 1) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Output(SpanKind.MetaCode); + + if (remainingWhitespace != null) + { + Accept(remainingWhitespace); + } + + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (EndOfFile || At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine)) + { + Context.OnError( + keywordStartLocation, + noTypeNameError, + keywordLength); + } + + // Parse to the end of the line + AcceptUntil(CSharpSymbolType.NewLine); + if (!Context.DesignTimeMode) + { + // We want the newline to be treated as code, but it causes issues at design-time. + Optional(CSharpSymbolType.NewLine); + } + + // Pull out the type name + string baseType = Span.GetContent(); + + // Set up chunk generation + Span.ChunkGenerator = createChunkGenerator(baseType.Trim()); + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code, AcceptedCharacters.AnyExceptNewline); + } + + private void TagHelperDirective(string keyword, Func buildChunkGenerator) + { + AssertDirective(keyword); + var keywordStartLocation = CurrentLocation; + + // Accept the directive name + AcceptAndMoveNext(); + + // Set the block type + Context.CurrentBlock.Type = BlockType.Directive; + + var keywordLength = Span.GetContent().Value.Length; + + var foundWhitespace = At(CSharpSymbolType.WhiteSpace); + AcceptWhile(CSharpSymbolType.WhiteSpace); + + // If we found whitespace then any content placed within the whitespace MAY cause a destructive change + // to the document. We can't accept it. + Output(SpanKind.MetaCode, foundWhitespace ? AcceptedCharacters.None : AcceptedCharacters.AnyExceptNewline); + + if (EndOfFile || At(CSharpSymbolType.NewLine)) + { + Context.OnError( + keywordStartLocation, + RazorResources.FormatParseError_DirectiveMustHaveValue(keyword), + keywordLength); + } + else + { + // Need to grab the current location before we accept until the end of the line. + var startLocation = CurrentLocation; + + // Parse to the end of the line. Essentially accepts anything until end of line, comments, invalid code + // etc. + AcceptUntil(CSharpSymbolType.NewLine); + + // Pull out the value minus the spaces at the end + var rawValue = Span.GetContent().Value.TrimEnd(); + var startsWithQuote = rawValue.StartsWith("\"", StringComparison.OrdinalIgnoreCase); + + // If the value starts with a quote then we should generate appropriate C# code to colorize the value. + if (startsWithQuote) + { + // Set up chunk generation + // The generated chunk of this chunk generator is picked up by CSharpDesignTimeHelpersVisitor which + // renders the C# to colorize the user provided value. We trim the quotes around the user's value + // so when we render the code we can project the users value into double quotes to not invoke C# + // IntelliSense. + Span.ChunkGenerator = buildChunkGenerator(rawValue.Trim('"')); + } + + // We expect the directive to be surrounded in quotes. + // The format for tag helper directives are: @directivename "SomeValue" + if (!startsWithQuote || + !rawValue.EndsWith("\"", StringComparison.OrdinalIgnoreCase)) + { + Context.OnError( + startLocation, + RazorResources.FormatParseError_DirectiveMustBeSurroundedByQuotes(keyword), + rawValue.Length); + } + } + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code, AcceptedCharacters.AnyExceptNewline); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Expressions.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Expressions.cs new file mode 100644 index 0000000000..656f6c37e7 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Expressions.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser + { + private void SetUpExpressions() + { + MapExpressionKeyword(AwaitExpression, CSharpKeyword.Await); + } + + private void AwaitExpression(bool topLevel) + { + // Ensure that we're on the await statement (only runs in debug) + Assert(CSharpKeyword.Await); + + // Accept the "await" and move on + AcceptAndMoveNext(); + + // Accept 1 or more spaces between the await and the following code. + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + // Top level basically indicates if we're within an expression or statement. + // Ex: topLevel true = @await Foo() | topLevel false = @{ await Foo(); } + // Note that in this case @{ @await Foo() } top level is true for await. + // Therefore, if we're top level then we want to act like an implicit expression, + // otherwise just act as whatever we're contained in. + if (topLevel) + { + // Setup the Span to be an async implicit expression (an implicit expresison that allows spaces). + // Spaces are allowed because of "@await Foo()". + AsyncImplicitExpression(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Statements.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Statements.cs new file mode 100644 index 0000000000..953c7f10ae --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.Statements.cs @@ -0,0 +1,745 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser + { + private static readonly Func IsValidStatementSpacingSymbol = + IsSpacingToken(includeNewLines: true, includeComments: true); + + private void SetUpKeywords() + { + MapKeywords(ConditionalBlock, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock); + MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default); + MapKeywords(IfStatement, CSharpKeyword.If); + MapKeywords(TryStatement, CSharpKeyword.Try); + MapKeywords(UsingKeyword, CSharpKeyword.Using); + MapKeywords(DoStatement, CSharpKeyword.Do); + MapKeywords(ReservedDirective, CSharpKeyword.Namespace, CSharpKeyword.Class); + } + + protected virtual void ReservedDirective(bool topLevel) + { + Context.OnError( + CurrentLocation, + RazorResources.FormatParseError_ReservedWord(CurrentSymbol.Content), + CurrentSymbol.Content.Length); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Context.CurrentBlock.Type = BlockType.Directive; + CompleteBlock(); + Output(SpanKind.MetaCode); + } + + private void KeywordBlock(bool topLevel) + { + HandleKeyword(topLevel, () => + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + ImplicitExpression(); + }); + } + + private void CaseStatement(bool topLevel) + { + Assert(CSharpSymbolType.Keyword); + Debug.Assert(CurrentSymbol.Keyword != null && + (CurrentSymbol.Keyword.Value == CSharpKeyword.Case || + CurrentSymbol.Keyword.Value == CSharpKeyword.Default)); + AcceptUntil(CSharpSymbolType.Colon); + Optional(CSharpSymbolType.Colon); + } + + private void DoStatement(bool topLevel) + { + Assert(CSharpKeyword.Do); + UnconditionalBlock(); + WhileClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void WhileClause() + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + IEnumerable ws = SkipToNextImportantToken(); + + if (At(CSharpKeyword.While)) + { + Accept(ws); + Assert(CSharpKeyword.While); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (AcceptCondition() && Optional(CSharpSymbolType.Semicolon)) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + } + else + { + PutCurrentBack(); + PutBack(ws); + } + } + + private void UsingKeyword(bool topLevel) + { + Assert(CSharpKeyword.Using); + var block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (At(CSharpSymbolType.LeftParenthesis)) + { + // using ( ==> Using Statement + UsingStatement(block); + } + else if (At(CSharpSymbolType.Identifier) || At(CSharpKeyword.Static)) + { + // using Identifier ==> Using Declaration + if (!topLevel) + { + Context.OnError( + block.Start, + RazorResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock, + block.Name.Length); + StandardStatement(); + } + else + { + UsingDeclaration(); + } + } + + if (topLevel) + { + CompleteBlock(); + } + } + + private void UsingDeclaration() + { + // Set block type to directive + Context.CurrentBlock.Type = BlockType.Directive; + + if (At(CSharpSymbolType.Identifier)) + { + // non-static using + NamespaceOrTypeName(); + var whitespace = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpSymbolType.Assign)) + { + // Alias + Accept(whitespace); + Assert(CSharpSymbolType.Assign); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // One more namespace or type name + NamespaceOrTypeName(); + } + else + { + PutCurrentBack(); + PutBack(whitespace); + } + } + else if (At(CSharpKeyword.Static)) + { + // static using + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + NamespaceOrTypeName(); + } + + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.AnyExceptNewline; + Span.ChunkGenerator = new AddImportChunkGenerator( + Span.GetContent(symbols => symbols.Skip(1))); + + // Optional ";" + if (EnsureCurrent()) + { + Optional(CSharpSymbolType.Semicolon); + } + } + + protected bool NamespaceOrTypeName() + { + if (Optional(CSharpSymbolType.Identifier) || Optional(CSharpSymbolType.Keyword)) + { + Optional(CSharpSymbolType.QuestionMark); // Nullable + if (Optional(CSharpSymbolType.DoubleColon)) + { + if (!Optional(CSharpSymbolType.Identifier)) + { + Optional(CSharpSymbolType.Keyword); + } + } + if (At(CSharpSymbolType.LessThan)) + { + TypeArgumentList(); + } + if (Optional(CSharpSymbolType.Dot)) + { + NamespaceOrTypeName(); + } + while (At(CSharpSymbolType.LeftBracket)) + { + Balance(BalancingModes.None); + Optional(CSharpSymbolType.RightBracket); + } + return true; + } + else + { + return false; + } + } + + private void TypeArgumentList() + { + Assert(CSharpSymbolType.LessThan); + Balance(BalancingModes.None); + Optional(CSharpSymbolType.GreaterThan); + } + + private void UsingStatement(Block block) + { + Assert(CSharpSymbolType.LeftParenthesis); + + // Parse condition + if (AcceptCondition()) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse code block + ExpectCodeBlock(block); + } + } + + private void TryStatement(bool topLevel) + { + Assert(CSharpKeyword.Try); + UnconditionalBlock(); + AfterTryClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void IfStatement(bool topLevel) + { + Assert(CSharpKeyword.If); + ConditionalBlock(topLevel: false); + AfterIfClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void AfterTryClause() + { + // Grab whitespace + var whitespace = SkipToNextImportantToken(); + + // Check for a catch or finally part + if (At(CSharpKeyword.Catch)) + { + Accept(whitespace); + Assert(CSharpKeyword.Catch); + FilterableCatchBlock(); + AfterTryClause(); + } + else if (At(CSharpKeyword.Finally)) + { + Accept(whitespace); + Assert(CSharpKeyword.Finally); + UnconditionalBlock(); + } + else + { + // Return whitespace and end the block + PutCurrentBack(); + PutBack(whitespace); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + } + + private void AfterIfClause() + { + // Grab whitespace and razor comments + IEnumerable ws = SkipToNextImportantToken(); + + // Check for an else part + if (At(CSharpKeyword.Else)) + { + Accept(ws); + Assert(CSharpKeyword.Else); + ElseClause(); + } + else + { + // No else, return whitespace + PutCurrentBack(); + PutBack(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + } + + private void ElseClause() + { + if (!At(CSharpKeyword.Else)) + { + return; + } + var block = new Block(CurrentSymbol); + + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpKeyword.If)) + { + // ElseIf + block.Name = SyntaxConstants.CSharp.ElseIfKeyword; + ConditionalBlock(block); + AfterIfClause(); + } + else if (!EndOfFile) + { + // Else + ExpectCodeBlock(block); + } + } + + private void ExpectCodeBlock(Block block) + { + if (!EndOfFile) + { + // Check for "{" to make sure we're at a block + if (!At(CSharpSymbolType.LeftBrace)) + { + Context.OnError( + CurrentLocation, + RazorResources.FormatParseError_SingleLine_ControlFlowStatements_Not_Allowed( + Language.GetSample(CSharpSymbolType.LeftBrace), + CurrentSymbol.Content), + CurrentSymbol.Content.Length); + } + + // Parse the statement and then we're done + Statement(block); + } + } + + private void UnconditionalBlock() + { + Assert(CSharpSymbolType.Keyword); + var block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ExpectCodeBlock(block); + } + + private void FilterableCatchBlock() + { + Assert(CSharpKeyword.Catch); + + var block = new Block(CurrentSymbol); + + // Accept "catch" + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingSymbol); + + // Parse the catch condition if present. If not present, let the C# compiler complain. + if (AcceptCondition()) + { + AcceptWhile(IsValidStatementSpacingSymbol); + + if (At(CSharpKeyword.When)) + { + // Accept "when". + AcceptAndMoveNext(); + AcceptWhile(IsValidStatementSpacingSymbol); + + // Parse the filter condition if present. If not present, let the C# compiler complain. + if (!AcceptCondition()) + { + // Incomplete condition. + return; + } + + AcceptWhile(IsValidStatementSpacingSymbol); + } + + ExpectCodeBlock(block); + } + } + + private void ConditionalBlock(bool topLevel) + { + Assert(CSharpSymbolType.Keyword); + var block = new Block(CurrentSymbol); + ConditionalBlock(block); + if (topLevel) + { + CompleteBlock(); + } + } + + private void ConditionalBlock(Block block) + { + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse the condition, if present (if not present, we'll let the C# compiler complain) + if (AcceptCondition()) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ExpectCodeBlock(block); + } + } + + private bool AcceptCondition() + { + if (At(CSharpSymbolType.LeftParenthesis)) + { + var complete = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + if (!complete) + { + AcceptUntil(CSharpSymbolType.NewLine); + } + else + { + Optional(CSharpSymbolType.RightParenthesis); + } + return complete; + } + return true; + } + + private void Statement() + { + Statement(null); + } + + private void Statement(Block block) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + + // Accept whitespace but always keep the last whitespace node so we can put it back if necessary + var lastWhitespace = AcceptWhiteSpaceInLines(); + Debug.Assert(lastWhitespace == null || (lastWhitespace.Start.AbsoluteIndex + lastWhitespace.Content.Length == CurrentLocation.AbsoluteIndex)); + + if (EndOfFile) + { + if (lastWhitespace != null) + { + Accept(lastWhitespace); + } + return; + } + + var type = CurrentSymbol.Type; + var loc = CurrentLocation; + + var isSingleLineMarkup = type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.Colon); + var isMarkup = isSingleLineMarkup || + type == CSharpSymbolType.LessThan || + (type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan)); + + if (Context.DesignTimeMode || !isMarkup) + { + // CODE owns whitespace, MARKUP owns it ONLY in DesignTimeMode. + if (lastWhitespace != null) + { + Accept(lastWhitespace); + } + } + else + { + var nextSymbol = Lookahead(1); + + // MARKUP owns whitespace EXCEPT in DesignTimeMode. + PutCurrentBack(); + + // Don't putback the whitespace if it precedes a '' tag. + if (nextSymbol != null && !nextSymbol.Content.Equals(SyntaxConstants.TextTagName)) + { + PutBack(lastWhitespace); + } + } + + if (isMarkup) + { + if (type == CSharpSymbolType.Transition && !isSingleLineMarkup) + { + Context.OnError( + loc, + RazorResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start, + length: 1 /* @ */); + } + + // Markup block + Output(SpanKind.Code); + if (Context.DesignTimeMode && CurrentSymbol != null && (CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition)) + { + PutCurrentBack(); + } + OtherParserBlock(); + } + else + { + // What kind of statement is this? + HandleStatement(block, type); + } + } + + private void HandleStatement(Block block, CSharpSymbolType type) + { + switch (type) + { + case CSharpSymbolType.RazorCommentTransition: + Output(SpanKind.Code); + RazorComment(); + Statement(block); + break; + case CSharpSymbolType.LeftBrace: + // Verbatim Block + block = block ?? new Block(RazorResources.BlockName_Code, CurrentLocation); + AcceptAndMoveNext(); + CodeBlock(block); + break; + case CSharpSymbolType.Keyword: + // Keyword block + HandleKeyword(false, StandardStatement); + break; + case CSharpSymbolType.Transition: + // Embedded Expression block + EmbeddedExpression(); + break; + case CSharpSymbolType.RightBrace: + // Possible end of Code Block, just run the continuation + break; + case CSharpSymbolType.Comment: + AcceptAndMoveNext(); + break; + default: + // Other statement + StandardStatement(); + break; + } + } + + private void EmbeddedExpression() + { + // First, verify the type of the block + Assert(CSharpSymbolType.Transition); + var transition = CurrentSymbol; + NextToken(); + + if (At(CSharpSymbolType.Transition)) + { + // Escaped "@" + Output(SpanKind.Code); + + // Output "@" as hidden span + Accept(transition); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.Code); + + Assert(CSharpSymbolType.Transition); + AcceptAndMoveNext(); + StandardStatement(); + } + else + { + // Throw errors as necessary, but continue parsing + if (At(CSharpSymbolType.LeftBrace)) + { + Context.OnError( + CurrentLocation, + RazorResources.ParseError_Unexpected_Nested_CodeBlock, + length: 1 /* { */); + } + + // @( or @foo - Nested expression, parse a child block + PutCurrentBack(); + PutBack(transition); + + // Before exiting, add a marker span if necessary + AddMarkerSymbolIfNecessary(); + + NestedBlock(); + } + } + + private void StandardStatement() + { + while (!EndOfFile) + { + var bookmark = CurrentLocation.AbsoluteIndex; + IEnumerable read = ReadWhile(sym => sym.Type != CSharpSymbolType.Semicolon && + sym.Type != CSharpSymbolType.RazorCommentTransition && + sym.Type != CSharpSymbolType.Transition && + sym.Type != CSharpSymbolType.LeftBrace && + sym.Type != CSharpSymbolType.LeftParenthesis && + sym.Type != CSharpSymbolType.LeftBracket && + sym.Type != CSharpSymbolType.RightBrace); + if (At(CSharpSymbolType.LeftBrace) || At(CSharpSymbolType.LeftParenthesis) || At(CSharpSymbolType.LeftBracket)) + { + Accept(read); + if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure)) + { + Optional(CSharpSymbolType.RightBrace); + } + else + { + // Recovery + AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace); + return; + } + } + else if (At(CSharpSymbolType.Transition) && (NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon))) + { + Accept(read); + Output(SpanKind.Code); + Template(); + } + else if (At(CSharpSymbolType.RazorCommentTransition)) + { + Accept(read); + RazorComment(); + } + else if (At(CSharpSymbolType.Semicolon)) + { + Accept(read); + AcceptAndMoveNext(); + return; + } + else if (At(CSharpSymbolType.RightBrace)) + { + Accept(read); + return; + } + else + { + Context.Source.Position = bookmark; + NextToken(); + AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace); + return; + } + } + } + + private void CodeBlock(Block block) + { + CodeBlock(true, block); + } + + private void CodeBlock(bool acceptTerminatingBrace, Block block) + { + EnsureCurrent(); + while (!EndOfFile && !At(CSharpSymbolType.RightBrace)) + { + // Parse a statement, then return here + Statement(); + EnsureCurrent(); + } + + if (EndOfFile) + { + Context.OnError( + block.Start, + RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, '}', '{'), + length: 1 /* { OR } */); + } + else if (acceptTerminatingBrace) + { + Assert(CSharpSymbolType.RightBrace); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + AcceptAndMoveNext(); + } + } + + private void HandleKeyword(bool topLevel, Action fallback) + { + Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword != null); + Action handler; + if (_keywordParsers.TryGetValue(CurrentSymbol.Keyword.Value, out handler)) + { + handler(topLevel); + } + else + { + fallback(); + } + } + + private IEnumerable SkipToNextImportantToken() + { + while (!EndOfFile) + { + IEnumerable ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpSymbolType.RazorCommentTransition)) + { + Accept(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + RazorComment(); + } + else + { + return ws; + } + } + return Enumerable.Empty(); + } + + // Common code for Parsers, but FxCop REALLY doesn't like it in the base class.. moving it here for now. + protected override void OutputSpanBeforeRazorComment() + { + AddMarkerSymbolIfNecessary(); + Output(SpanKind.Code); + } + + protected class Block + { + public Block(string name, SourceLocation start) + { + Name = name; + Start = start; + } + + public Block(CSharpSymbol symbol) + : this(GetName(symbol), symbol.Start) + { + } + + public string Name { get; set; } + public SourceLocation Start { get; set; } + + private static string GetName(CSharpSymbol sym) + { + if (sym.Type == CSharpSymbolType.Keyword) + { + return CSharpLanguageCharacteristics.GetKeyword(sym.Keyword.Value); + } + return sym.Content; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.cs new file mode 100644 index 0000000000..8aaa6ae0f1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpCodeParser.cs @@ -0,0 +1,662 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser : TokenizerBackedParser + { + internal static readonly int UsingKeywordLength = 5; // using + + internal static ISet DefaultKeywords = new HashSet() + { + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + SyntaxConstants.CSharp.AddTagHelperKeyword, + SyntaxConstants.CSharp.RemoveTagHelperKeyword, + "if", + "do", + "try", + "for", + "foreach", + "while", + "switch", + "lock", + "using", + "section", + "inherits", + "functions", + "namespace", + "class", + }; + + private Dictionary _directiveParsers = new Dictionary(); + private Dictionary> _keywordParsers = new Dictionary>(); + + public CSharpCodeParser() + { + Keywords = new HashSet(); + SetUpKeywords(); + SetupDirectives(); + SetUpExpressions(); + } + + protected internal ISet Keywords { get; private set; } + + public bool IsNested { get; set; } + + protected override ParserBase OtherParser + { + get { return Context.MarkupParser; } + } + + protected override LanguageCharacteristics Language + { + get { return CSharpLanguageCharacteristics.Instance; } + } + + protected void MapDirectives(Action handler, params string[] directives) + { + foreach (string directive in directives) + { + _directiveParsers.Add(directive, handler); + Keywords.Add(directive); + } + } + + protected bool TryGetDirectiveHandler(string directive, out Action handler) + { + return _directiveParsers.TryGetValue(directive, out handler); + } + + private void MapExpressionKeyword(Action handler, CSharpKeyword keyword) + { + _keywordParsers.Add(keyword, handler); + + // Expression keywords don't belong in the regular keyword list + } + + private void MapKeywords(Action handler, params CSharpKeyword[] keywords) + { + MapKeywords(handler, topLevel: true, keywords: keywords); + } + + private void MapKeywords(Action handler, bool topLevel, params CSharpKeyword[] keywords) + { + foreach (CSharpKeyword keyword in keywords) + { + _keywordParsers.Add(keyword, handler); + if (topLevel) + { + Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword)); + } + } + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")] + [Conditional("DEBUG")] + internal void Assert(CSharpKeyword expectedKeyword) + { + Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == expectedKeyword); + } + + protected internal bool At(CSharpKeyword keyword) + { + return At(CSharpSymbolType.Keyword) && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == keyword; + } + + protected internal bool AcceptIf(CSharpKeyword keyword) + { + if (At(keyword)) + { + AcceptAndMoveNext(); + return true; + } + return false; + } + + protected static Func IsSpacingToken(bool includeNewLines, bool includeComments) + { + return sym => sym.Type == CSharpSymbolType.WhiteSpace || + (includeNewLines && sym.Type == CSharpSymbolType.NewLine) || + (includeComments && sym.Type == CSharpSymbolType.Comment); + } + + public override void ParseBlock() + { + using (PushSpanConfig(DefaultSpanConfig)) + { + if (Context == null) + { + throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set); + } + + // Unless changed, the block is a statement block + using (Context.StartBlock(BlockType.Statement)) + { + NextToken(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + var current = CurrentSymbol; + if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter) + { + var split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition); + current = split.Item1; + Context.Source.Position = split.Item2.Start.AbsoluteIndex; + NextToken(); + } + else if (At(CSharpSymbolType.Transition)) + { + NextToken(); + } + + // Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason + if (current.Type == CSharpSymbolType.Transition) + { + if (Span.Symbols.Count > 0) + { + Output(SpanKind.Code); + } + AtTransition(current); + } + else + { + // No "@" => Jump straight to AfterTransition + AfterTransition(); + } + Output(SpanKind.Code); + } + } + } + + private void DefaultSpanConfig(SpanBuilder span) + { + span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + span.ChunkGenerator = new StatementChunkGenerator(); + } + + private void AtTransition(CSharpSymbol current) + { + Debug.Assert(current.Type == CSharpSymbolType.Transition); + Accept(current); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + + // Output the "@" span and continue here + Output(SpanKind.Transition); + AfterTransition(); + } + + private void AfterTransition() + { + using (PushSpanConfig(DefaultSpanConfig)) + { + EnsureCurrent(); + try + { + // What type of block is this? + if (!EndOfFile) + { + if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis) + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + ExplicitExpression(); + return; + } + else if (CurrentSymbol.Type == CSharpSymbolType.Identifier) + { + Action handler; + if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler)) + { + Span.ChunkGenerator = SpanChunkGenerator.Null; + handler(); + return; + } + else + { + if (CurrentSymbol.Content.Equals(SyntaxConstants.CSharp.HelperKeyword)) + { + Context.OnError( + CurrentLocation, + RazorResources.FormatParseError_HelperDirectiveNotAvailable(SyntaxConstants.CSharp.HelperKeyword), + CurrentSymbol.Content.Length); + } + + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + ImplicitExpression(); + return; + } + } + else if (CurrentSymbol.Type == CSharpSymbolType.Keyword) + { + KeywordBlock(topLevel: true); + return; + } + else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace) + { + VerbatimBlock(); + return; + } + } + + // Invalid character + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + AddMarkerSymbolIfNecessary(); + Span.ChunkGenerator = new ExpressionChunkGenerator(); + Span.EditHandler = new ImplicitExpressionEditHandler( + Language.TokenizeString, + DefaultKeywords, + acceptTrailingDot: IsNested) + { + AcceptedCharacters = AcceptedCharacters.NonWhiteSpace + }; + if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine)) + { + Context.OnError( + CurrentLocation, + RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS, + CurrentSymbol.Content.Length); + } + else if (EndOfFile) + { + Context.OnError( + CurrentLocation, + RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock, + length: 1 /* end of file */); + } + else + { + Context.OnError( + CurrentLocation, + RazorResources.FormatParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS( + CurrentSymbol.Content), + CurrentSymbol.Content.Length); + } + } + finally + { + // Always put current character back in the buffer for the next parser. + PutCurrentBack(); + } + } + } + + private void VerbatimBlock() + { + Assert(CSharpSymbolType.LeftBrace); + var block = new Block(RazorResources.BlockName_Code, CurrentLocation); + AcceptAndMoveNext(); + + // Set up the "{" span and output + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode); + + // Set up auto-complete and parse the code block + var editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + Span.EditHandler = editHandler; + CodeBlock(false, block); + + Span.ChunkGenerator = new StatementChunkGenerator(); + AddMarkerSymbolIfNecessary(); + if (!At(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + } + Output(SpanKind.Code); + + if (Optional(CSharpSymbolType.RightBrace)) + { + // Set up the "}" span + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + } + + if (!IsNested) + { + EnsureCurrent(); + if (At(CSharpSymbolType.NewLine) || + (At(CSharpSymbolType.WhiteSpace) && NextIs(CSharpSymbolType.NewLine))) + { + Context.NullGenerateWhitespaceAndNewLine = true; + } + } + + Output(SpanKind.MetaCode); + } + + private void ImplicitExpression() + { + ImplicitExpression(AcceptedCharacters.NonWhiteSpace); + } + + // Async implicit expressions include the "await" keyword and therefore need to allow spaces to + // separate the "await" and the following code. + private void AsyncImplicitExpression() + { + ImplicitExpression(AcceptedCharacters.AnyExceptNewline); + } + + private void ImplicitExpression(AcceptedCharacters acceptedCharacters) + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.ChunkGenerator = new ExpressionChunkGenerator(); + + using (PushSpanConfig(span => + { + span.EditHandler = new ImplicitExpressionEditHandler( + Language.TokenizeString, + Keywords, + acceptTrailingDot: IsNested); + span.EditHandler.AcceptedCharacters = acceptedCharacters; + span.ChunkGenerator = new ExpressionChunkGenerator(); + })) + { + do + { + if (AtIdentifier(allowKeywords: true)) + { + AcceptAndMoveNext(); + } + } + while (MethodCallOrArrayIndex(acceptedCharacters)); + + PutCurrentBack(); + Output(SpanKind.Code); + } + } + + private bool MethodCallOrArrayIndex(AcceptedCharacters acceptedCharacters) + { + if (!EndOfFile) + { + if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || CurrentSymbol.Type == CSharpSymbolType.LeftBracket) + { + // If we end within "(", whitespace is fine + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + + CSharpSymbolType right; + bool success; + + using (PushSpanConfig((span, prev) => + { + prev(span); + span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + })) + { + right = Language.FlipBracket(CurrentSymbol.Type); + success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + } + + if (!success) + { + AcceptUntil(CSharpSymbolType.LessThan); + } + if (At(right)) + { + AcceptAndMoveNext(); + + // At the ending brace, restore the initial accepted characters. + Span.EditHandler.AcceptedCharacters = acceptedCharacters; + } + return MethodCallOrArrayIndex(acceptedCharacters); + } + if (At(CSharpSymbolType.QuestionMark)) + { + var next = Lookahead(count: 1); + + if (next != null) + { + if (next.Type == CSharpSymbolType.Dot) + { + // Accept null conditional dot operator (?.). + AcceptAndMoveNext(); + AcceptAndMoveNext(); + + // If the next piece after the ?. is a keyword or identifier then we want to continue. + return At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword); + } + else if (next.Type == CSharpSymbolType.LeftBracket) + { + // We're at the ? for a null conditional bracket operator (?[). + AcceptAndMoveNext(); + + // Accept the [ and any content inside (it will attempt to balance). + return MethodCallOrArrayIndex(acceptedCharacters); + } + } + } + else if (At(CSharpSymbolType.Dot)) + { + var dot = CurrentSymbol; + if (NextToken()) + { + if (At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword)) + { + // Accept the dot and return to the start + Accept(dot); + return true; // continue + } + else + { + // Put the symbol back + PutCurrentBack(); + } + } + if (!IsNested) + { + // Put the "." back + PutBack(dot); + } + else + { + Accept(dot); + } + } + else if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine)) + { + PutCurrentBack(); + } + } + + // Implicit Expression is complete + return false; + } + + protected void CompleteBlock() + { + CompleteBlock(insertMarkerIfNecessary: true); + } + + protected void CompleteBlock(bool insertMarkerIfNecessary) + { + CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary); + } + + protected void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine) + { + if (insertMarkerIfNecessary && Context.LastAcceptedCharacters != AcceptedCharacters.Any) + { + AddMarkerSymbolIfNecessary(); + } + + EnsureCurrent(); + + // Read whitespace, but not newlines + // If we're not inserting a marker span, we don't need to capture whitespace + if (!Context.WhiteSpaceIsSignificantToAncestorBlock && + Context.CurrentBlock.Type != BlockType.Expression && + captureWhitespaceToEndOfLine && + !Context.DesignTimeMode && + !IsNested) + { + CaptureWhitespaceAtEndOfCodeOnlyLine(); + } + else + { + PutCurrentBack(); + } + } + + private void CaptureWhitespaceAtEndOfCodeOnlyLine() + { + IEnumerable ws = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace); + if (At(CSharpSymbolType.NewLine)) + { + Accept(ws); + AcceptAndMoveNext(); + PutCurrentBack(); + } + else + { + PutCurrentBack(); + PutBack(ws); + } + } + + private void ConfigureExplicitExpressionSpan(SpanBuilder sb) + { + sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + sb.ChunkGenerator = new ExpressionChunkGenerator(); + } + + private void ExplicitExpression() + { + var block = new Block(RazorResources.BlockName_ExplicitExpression, CurrentLocation); + Assert(CSharpSymbolType.LeftParenthesis); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode); + using (PushSpanConfig(ConfigureExplicitExpressionSpan)) + { + var success = Balance( + BalancingModes.BacktrackOnFailure | BalancingModes.NoErrorOnFailure | BalancingModes.AllowCommentsAndTemplates, + CSharpSymbolType.LeftParenthesis, + CSharpSymbolType.RightParenthesis, + block.Start); + + if (!success) + { + AcceptUntil(CSharpSymbolType.LessThan); + Context.OnError( + block.Start, + RazorResources.FormatParseError_Expected_EndOfBlock_Before_EOF(block.Name, ")", "("), + length: 1 /* ( */); + } + + // If necessary, put an empty-content marker symbol here + if (Span.Symbols.Count == 0) + { + Accept(new CSharpSymbol(CurrentLocation, string.Empty, CSharpSymbolType.Unknown)); + } + + // Output the content span and then capture the ")" + Output(SpanKind.Code); + } + Optional(CSharpSymbolType.RightParenthesis); + if (!EndOfFile) + { + PutCurrentBack(); + } + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + CompleteBlock(insertMarkerIfNecessary: false); + Output(SpanKind.MetaCode); + } + + private void Template() + { + if (Context.IsWithin(BlockType.Template)) + { + Context.OnError( + CurrentLocation, + RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested, + length: 1 /* @ */); + } + Output(SpanKind.Code); + using (Context.StartBlock(BlockType.Template)) + { + Context.CurrentBlock.ChunkGenerator = new TemplateBlockChunkGenerator(); + PutCurrentBack(); + OtherParserBlock(); + } + } + + private void OtherParserBlock() + { + ParseWithOtherParser(p => p.ParseBlock()); + } + + private void SectionBlock(string left, string right, bool caseSensitive) + { + ParseWithOtherParser(p => p.ParseSection(Tuple.Create(left, right), caseSensitive)); + } + + private void NestedBlock() + { + Output(SpanKind.Code); + var wasNested = IsNested; + IsNested = true; + using (PushSpanConfig()) + { + ParseBlock(); + } + Initialize(Span); + IsNested = wasNested; + NextToken(); + } + + protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions) + { + // No embedded transitions in C#, so ignore that param + return allowTemplatesAndComments + && ((Language.IsTransition(CurrentSymbol) + && NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon)) + || Language.IsCommentStart(CurrentSymbol)); + } + + protected override void HandleEmbeddedTransition() + { + if (Language.IsTransition(CurrentSymbol)) + { + PutCurrentBack(); + Template(); + } + else if (Language.IsCommentStart(CurrentSymbol)) + { + RazorComment(); + } + } + + private void ParseWithOtherParser(Action parseAction) + { + // When transitioning to the HTML parser we no longer want to act as if we're in a nested C# state. + // For instance, if
@hello.
is in a nested C# block we don't want the trailing '.' to be handled + // as C#; it should be handled as a period because it's wrapped in markup. + var wasNested = IsNested; + IsNested = false; + using (PushSpanConfig()) + { + Context.SwitchActiveParser(); + parseAction(Context.MarkupParser); + Context.SwitchActiveParser(); + } + Initialize(Span); + IsNested = wasNested; + NextToken(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpLanguageCharacteristics.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpLanguageCharacteristics.cs new file mode 100644 index 0000000000..b22fbc277f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CSharpLanguageCharacteristics.cs @@ -0,0 +1,194 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public class CSharpLanguageCharacteristics : LanguageCharacteristics + { + private static readonly CSharpLanguageCharacteristics _instance = new CSharpLanguageCharacteristics(); + + private static Dictionary _symbolSamples = new Dictionary() + { + { CSharpSymbolType.Arrow, "->" }, + { CSharpSymbolType.Minus, "-" }, + { CSharpSymbolType.Decrement, "--" }, + { CSharpSymbolType.MinusAssign, "-=" }, + { CSharpSymbolType.NotEqual, "!=" }, + { CSharpSymbolType.Not, "!" }, + { CSharpSymbolType.Modulo, "%" }, + { CSharpSymbolType.ModuloAssign, "%=" }, + { CSharpSymbolType.AndAssign, "&=" }, + { CSharpSymbolType.And, "&" }, + { CSharpSymbolType.DoubleAnd, "&&" }, + { CSharpSymbolType.LeftParenthesis, "(" }, + { CSharpSymbolType.RightParenthesis, ")" }, + { CSharpSymbolType.Star, "*" }, + { CSharpSymbolType.MultiplyAssign, "*=" }, + { CSharpSymbolType.Comma, "," }, + { CSharpSymbolType.Dot, "." }, + { CSharpSymbolType.Slash, "/" }, + { CSharpSymbolType.DivideAssign, "/=" }, + { CSharpSymbolType.DoubleColon, "::" }, + { CSharpSymbolType.Colon, ":" }, + { CSharpSymbolType.Semicolon, ";" }, + { CSharpSymbolType.QuestionMark, "?" }, + { CSharpSymbolType.NullCoalesce, "??" }, + { CSharpSymbolType.RightBracket, "]" }, + { CSharpSymbolType.LeftBracket, "[" }, + { CSharpSymbolType.XorAssign, "^=" }, + { CSharpSymbolType.Xor, "^" }, + { CSharpSymbolType.LeftBrace, "{" }, + { CSharpSymbolType.OrAssign, "|=" }, + { CSharpSymbolType.DoubleOr, "||" }, + { CSharpSymbolType.Or, "|" }, + { CSharpSymbolType.RightBrace, "}" }, + { CSharpSymbolType.Tilde, "~" }, + { CSharpSymbolType.Plus, "+" }, + { CSharpSymbolType.PlusAssign, "+=" }, + { CSharpSymbolType.Increment, "++" }, + { CSharpSymbolType.LessThan, "<" }, + { CSharpSymbolType.LessThanEqual, "<=" }, + { CSharpSymbolType.LeftShift, "<<" }, + { CSharpSymbolType.LeftShiftAssign, "<<=" }, + { CSharpSymbolType.Assign, "=" }, + { CSharpSymbolType.Equals, "==" }, + { CSharpSymbolType.GreaterThan, ">" }, + { CSharpSymbolType.GreaterThanEqual, ">=" }, + { CSharpSymbolType.RightShift, ">>" }, + { CSharpSymbolType.RightShiftAssign, ">>>" }, + { CSharpSymbolType.Hash, "#" }, + { CSharpSymbolType.Transition, "@" }, + }; + + private CSharpLanguageCharacteristics() + { + } + + public static CSharpLanguageCharacteristics Instance + { + get { return _instance; } + } + + public override CSharpTokenizer CreateTokenizer(ITextDocument source) + { + return new CSharpTokenizer(source); + } + + protected override CSharpSymbol CreateSymbol(SourceLocation location, string content, CSharpSymbolType type, IEnumerable errors) + { + return new CSharpSymbol(location, content, type, errors); + } + + public override string GetSample(CSharpSymbolType type) + { + return GetSymbolSample(type); + } + + public override CSharpSymbol CreateMarkerSymbol(SourceLocation location) + { + return new CSharpSymbol(location, string.Empty, CSharpSymbolType.Unknown); + } + + public override CSharpSymbolType GetKnownSymbolType(KnownSymbolType type) + { + switch (type) + { + case KnownSymbolType.Identifier: + return CSharpSymbolType.Identifier; + case KnownSymbolType.Keyword: + return CSharpSymbolType.Keyword; + case KnownSymbolType.NewLine: + return CSharpSymbolType.NewLine; + case KnownSymbolType.WhiteSpace: + return CSharpSymbolType.WhiteSpace; + case KnownSymbolType.Transition: + return CSharpSymbolType.Transition; + case KnownSymbolType.CommentStart: + return CSharpSymbolType.RazorCommentTransition; + case KnownSymbolType.CommentStar: + return CSharpSymbolType.RazorCommentStar; + case KnownSymbolType.CommentBody: + return CSharpSymbolType.RazorComment; + default: + return CSharpSymbolType.Unknown; + } + } + + public override CSharpSymbolType FlipBracket(CSharpSymbolType bracket) + { + switch (bracket) + { + case CSharpSymbolType.LeftBrace: + return CSharpSymbolType.RightBrace; + case CSharpSymbolType.LeftBracket: + return CSharpSymbolType.RightBracket; + case CSharpSymbolType.LeftParenthesis: + return CSharpSymbolType.RightParenthesis; + case CSharpSymbolType.LessThan: + return CSharpSymbolType.GreaterThan; + case CSharpSymbolType.RightBrace: + return CSharpSymbolType.LeftBrace; + case CSharpSymbolType.RightBracket: + return CSharpSymbolType.LeftBracket; + case CSharpSymbolType.RightParenthesis: + return CSharpSymbolType.LeftParenthesis; + case CSharpSymbolType.GreaterThan: + return CSharpSymbolType.LessThan; + default: +#if NET451 + // No Debug.Fail + Debug.Fail("FlipBracket must be called with a bracket character"); +#else + Debug.Assert(false, "FlipBracket must be called with a bracket character"); +#endif + return CSharpSymbolType.Unknown; + } + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "C# Keywords are lower-case")] + public static string GetKeyword(CSharpKeyword keyword) + { + return keyword.ToString().ToLowerInvariant(); + } + + public static string GetSymbolSample(CSharpSymbolType type) + { + string sample; + if (!_symbolSamples.TryGetValue(type, out sample)) + { + switch (type) + { + case CSharpSymbolType.Identifier: + return RazorResources.CSharpSymbol_Identifier; + case CSharpSymbolType.Keyword: + return RazorResources.CSharpSymbol_Keyword; + case CSharpSymbolType.IntegerLiteral: + return RazorResources.CSharpSymbol_IntegerLiteral; + case CSharpSymbolType.NewLine: + return RazorResources.CSharpSymbol_Newline; + case CSharpSymbolType.WhiteSpace: + return RazorResources.CSharpSymbol_Whitespace; + case CSharpSymbolType.Comment: + return RazorResources.CSharpSymbol_Comment; + case CSharpSymbolType.RealLiteral: + return RazorResources.CSharpSymbol_RealLiteral; + case CSharpSymbolType.CharacterLiteral: + return RazorResources.CSharpSymbol_CharacterLiteral; + case CSharpSymbolType.StringLiteral: + return RazorResources.CSharpSymbol_StringLiteral; + default: + return RazorResources.Symbol_Unknown; + } + } + return sample; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/CallbackVisitor.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CallbackVisitor.cs new file mode 100644 index 0000000000..68081e1dcc --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/CallbackVisitor.cs @@ -0,0 +1,97 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Parser +{ + public class CallbackVisitor : ParserVisitor + { + private Action _spanCallback; + private Action _errorCallback; + private Action _endBlockCallback; + private Action _startBlockCallback; + private Action _completeCallback; + + public CallbackVisitor(Action spanCallback) + : this(spanCallback, _ => + { + }) + { + } + + public CallbackVisitor(Action spanCallback, Action errorCallback) + : this(spanCallback, errorCallback, _ => + { + }, _ => + { + }) + { + } + + public CallbackVisitor(Action spanCallback, Action errorCallback, Action startBlockCallback, Action endBlockCallback) + : this(spanCallback, errorCallback, startBlockCallback, endBlockCallback, () => + { + }) + { + } + + public CallbackVisitor(Action spanCallback, Action errorCallback, Action startBlockCallback, Action endBlockCallback, Action completeCallback) + { + _spanCallback = spanCallback; + _errorCallback = errorCallback; + _startBlockCallback = startBlockCallback; + _endBlockCallback = endBlockCallback; + _completeCallback = completeCallback; + } + + public SynchronizationContext SynchronizationContext { get; set; } + + public override void VisitStartBlock(Block block) + { + base.VisitStartBlock(block); + RaiseCallback(SynchronizationContext, block.Type, _startBlockCallback); + } + + public override void VisitSpan(Span span) + { + base.VisitSpan(span); + RaiseCallback(SynchronizationContext, span, _spanCallback); + } + + public override void VisitEndBlock(Block block) + { + base.VisitEndBlock(block); + RaiseCallback(SynchronizationContext, block.Type, _endBlockCallback); + } + + public override void VisitError(RazorError err) + { + base.VisitError(err); + RaiseCallback(SynchronizationContext, err, _errorCallback); + } + + public override void OnComplete() + { + base.OnComplete(); + RaiseCallback(SynchronizationContext, null, _ => _completeCallback()); + } + + private static void RaiseCallback(SynchronizationContext syncContext, T param, Action callback) + { + if (callback != null) + { + if (syncContext != null) + { + syncContext.Post(state => callback((T)state), param); + } + else + { + callback(param); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/ConditionalAttributeCollapser.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/ConditionalAttributeCollapser.cs new file mode 100644 index 0000000000..73be90198c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/ConditionalAttributeCollapser.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer; + +namespace Microsoft.AspNet.Razor.Parser +{ + internal class ConditionalAttributeCollapser : MarkupRewriter + { + public ConditionalAttributeCollapser(Action markupSpanFactory) : base(markupSpanFactory) + { + } + + protected override bool CanRewrite(Block block) + { + var gen = block.ChunkGenerator as AttributeBlockChunkGenerator; + return gen != null && block.Children.Any() && block.Children.All(IsLiteralAttributeValue); + } + + protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) + { + // Collect the content of this node + var content = string.Concat(block.Children.Cast().Select(s => s.Content)); + + // Create a new span containing this content + var span = new SpanBuilder(); + span.EditHandler = new SpanEditHandler(HtmlTokenizer.Tokenize); + FillSpan(span, block.Children.Cast().First().Start, content); + return span.Build(); + } + + private bool IsLiteralAttributeValue(SyntaxTreeNode node) + { + if (node.IsBlock) + { + return false; + } + var span = node as Span; + Debug.Assert(span != null); + + var litGen = span.ChunkGenerator as LiteralAttributeChunkGenerator; + + return span != null && + ((litGen != null && litGen.ValueGenerator == null) || + span.ChunkGenerator == SpanChunkGenerator.Null || + span.ChunkGenerator is MarkupChunkGenerator); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/HtmlLanguageCharacteristics.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/HtmlLanguageCharacteristics.cs new file mode 100644 index 0000000000..dfb1c5eed1 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/HtmlLanguageCharacteristics.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public class HtmlLanguageCharacteristics : LanguageCharacteristics + { + private static readonly HtmlLanguageCharacteristics _instance = new HtmlLanguageCharacteristics(); + + private HtmlLanguageCharacteristics() + { + } + + public static HtmlLanguageCharacteristics Instance + { + get { return _instance; } + } + + public override string GetSample(HtmlSymbolType type) + { + switch (type) + { + case HtmlSymbolType.Text: + return RazorResources.HtmlSymbol_Text; + case HtmlSymbolType.WhiteSpace: + return RazorResources.HtmlSymbol_WhiteSpace; + case HtmlSymbolType.NewLine: + return RazorResources.HtmlSymbol_NewLine; + case HtmlSymbolType.OpenAngle: + return "<"; + case HtmlSymbolType.Bang: + return "!"; + case HtmlSymbolType.ForwardSlash: + return "/"; + case HtmlSymbolType.QuestionMark: + return "?"; + case HtmlSymbolType.DoubleHyphen: + return "--"; + case HtmlSymbolType.LeftBracket: + return "["; + case HtmlSymbolType.CloseAngle: + return ">"; + case HtmlSymbolType.RightBracket: + return "]"; + case HtmlSymbolType.Equals: + return "="; + case HtmlSymbolType.DoubleQuote: + return "\""; + case HtmlSymbolType.SingleQuote: + return "'"; + case HtmlSymbolType.Transition: + return "@"; + case HtmlSymbolType.Colon: + return ":"; + case HtmlSymbolType.RazorComment: + return RazorResources.HtmlSymbol_RazorComment; + case HtmlSymbolType.RazorCommentStar: + return "*"; + case HtmlSymbolType.RazorCommentTransition: + return "@"; + default: + return RazorResources.Symbol_Unknown; + } + } + + public override HtmlTokenizer CreateTokenizer(ITextDocument source) + { + return new HtmlTokenizer(source); + } + + public override HtmlSymbolType FlipBracket(HtmlSymbolType bracket) + { + switch (bracket) + { + case HtmlSymbolType.LeftBracket: + return HtmlSymbolType.RightBracket; + case HtmlSymbolType.OpenAngle: + return HtmlSymbolType.CloseAngle; + case HtmlSymbolType.RightBracket: + return HtmlSymbolType.LeftBracket; + case HtmlSymbolType.CloseAngle: + return HtmlSymbolType.OpenAngle; + default: +#if NET451 + // No Debug.Fail in CoreCLR + + Debug.Fail("FlipBracket must be called with a bracket character"); +#else + Debug.Assert(false, "FlipBracket must be called with a bracket character"); +#endif + return HtmlSymbolType.Unknown; + } + } + + public override HtmlSymbol CreateMarkerSymbol(SourceLocation location) + { + return new HtmlSymbol(location, string.Empty, HtmlSymbolType.Unknown); + } + + public override HtmlSymbolType GetKnownSymbolType(KnownSymbolType type) + { + switch (type) + { + case KnownSymbolType.CommentStart: + return HtmlSymbolType.RazorCommentTransition; + case KnownSymbolType.CommentStar: + return HtmlSymbolType.RazorCommentStar; + case KnownSymbolType.CommentBody: + return HtmlSymbolType.RazorComment; + case KnownSymbolType.Identifier: + return HtmlSymbolType.Text; + case KnownSymbolType.Keyword: + return HtmlSymbolType.Text; + case KnownSymbolType.NewLine: + return HtmlSymbolType.NewLine; + case KnownSymbolType.Transition: + return HtmlSymbolType.Transition; + case KnownSymbolType.WhiteSpace: + return HtmlSymbolType.WhiteSpace; + default: + return HtmlSymbolType.Unknown; + } + } + + protected override HtmlSymbol CreateSymbol(SourceLocation location, string content, HtmlSymbolType type, IEnumerable errors) + { + return new HtmlSymbol(location, content, type, errors); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor.VSRC1/Parser/HtmlMarkupParser.Block.cs new file mode 100644 index 0000000000..0f6a6d52d3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Parser/HtmlMarkupParser.Block.cs @@ -0,0 +1,1163 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class HtmlMarkupParser + { + private const string ScriptTagName = "script"; + + private SourceLocation _lastTagStart = SourceLocation.Zero; + private HtmlSymbol _bufferedOpenAngle; + + public override void ParseBlock() + { + if (Context == null) + { + throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set); + } + + using (PushSpanConfig(DefaultMarkupSpan)) + { + using (Context.StartBlock(BlockType.Markup)) + { + if (!NextToken()) + { + return; + } + + AcceptWhile(IsSpacingToken(includeNewLines: true)); + + if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle) + { + // "<" => Implicit Tag Block + TagBlock(new Stack>()); + } + else if (CurrentSymbol.Type == HtmlSymbolType.Transition) + { + // "@" => Explicit Tag/Single Line Block OR Template + Output(SpanKind.Markup); + + // Definitely have a transition span + Assert(HtmlSymbolType.Transition); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.Transition); + if (At(HtmlSymbolType.Transition)) + { + Span.ChunkGenerator = SpanChunkGenerator.Null; + AcceptAndMoveNext(); + Output(SpanKind.MetaCode); + } + AfterTransition(); + } + else + { + Context.OnError( + CurrentSymbol.Start, + RazorResources.ParseError_MarkupBlock_Must_Start_With_Tag, + CurrentSymbol.Content.Length); + } + Output(SpanKind.Markup); + } + } + } + + private void DefaultMarkupSpan(SpanBuilder span) + { + span.ChunkGenerator = new MarkupChunkGenerator(); + span.EditHandler = new SpanEditHandler(Language.TokenizeString, AcceptedCharacters.Any); + } + + private void AfterTransition() + { + // "@:" => Explicit Single Line Block + if (CurrentSymbol.Type == HtmlSymbolType.Text && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == ':') + { + // Split the token + Tuple split = Language.SplitSymbol(CurrentSymbol, 1, HtmlSymbolType.Colon); + + // The first part (left) is added to this span and we return a MetaCode span + Accept(split.Item1); + Span.ChunkGenerator = SpanChunkGenerator.Null; + Output(SpanKind.MetaCode); + if (split.Item2 != null) + { + Accept(split.Item2); + } + NextToken(); + SingleLineMarkup(); + } + else if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle) + { + TagBlock(new Stack>()); + } + } + + private void SingleLineMarkup() + { + // Parse until a newline, it's that simple! + // First, signal to code parser that whitespace is significant to us. + var old = Context.WhiteSpaceIsSignificantToAncestorBlock; + Context.WhiteSpaceIsSignificantToAncestorBlock = true; + Span.EditHandler = new SingleLineMarkupEditHandler(Language.TokenizeString); + SkipToAndParseCode(HtmlSymbolType.NewLine); + if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine) + { + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + PutCurrentBack(); + Context.WhiteSpaceIsSignificantToAncestorBlock = old; + Output(SpanKind.Markup); + } + + private void TagBlock(Stack> tags) + { + // Skip Whitespace and Text + var complete = false; + do + { + SkipToAndParseCode(HtmlSymbolType.OpenAngle); + + // Output everything prior to the OpenAngle into a markup span + Output(SpanKind.Markup); + + // Do not want to start a new tag block if we're at the end of the file. + IDisposable tagBlockWrapper = null; + try + { + var atSpecialTag = AtSpecialTag; + + if (!EndOfFile && !atSpecialTag) + { + // Start a Block tag. This is used to wrap things like

or etc. + tagBlockWrapper = Context.StartBlock(BlockType.Tag); + } + + if (EndOfFile) + { + EndTagBlock(tags, complete: true); + } + else + { + _bufferedOpenAngle = null; + _lastTagStart = CurrentLocation; + Assert(HtmlSymbolType.OpenAngle); + _bufferedOpenAngle = CurrentSymbol; + var tagStart = CurrentLocation; + if (!NextToken()) + { + Accept(_bufferedOpenAngle); + EndTagBlock(tags, complete: false); + } + else + { + complete = AfterTagStart(tagStart, tags, atSpecialTag, tagBlockWrapper); + } + } + + if (complete) + { + // Completed tags have no accepted characters inside of blocks. + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + // Output the contents of the tag into its own markup span. + Output(SpanKind.Markup); + } + finally + { + // Will be null if we were at end of file or special tag when initially created. + if (tagBlockWrapper != null) + { + // End tag block + tagBlockWrapper.Dispose(); + } + } + } + while (tags.Count > 0); + + EndTagBlock(tags, complete); + } + + private bool AfterTagStart(SourceLocation tagStart, + Stack> tags, + bool atSpecialTag, + IDisposable tagBlockWrapper) + { + if (!EndOfFile) + { + switch (CurrentSymbol.Type) + { + case HtmlSymbolType.ForwardSlash: + // End Tag + return EndTag(tagStart, tags, tagBlockWrapper); + case HtmlSymbolType.Bang: + // Comment, CDATA, DOCTYPE, or a parser-escaped HTML tag. + if (atSpecialTag) + { + Accept(_bufferedOpenAngle); + return BangTag(); + } + else + { + goto default; + } + case HtmlSymbolType.QuestionMark: + // XML PI + Accept(_bufferedOpenAngle); + return XmlPI(); + default: + // Start Tag + return StartTag(tags, tagBlockWrapper); + } + } + if (tags.Count == 0) + { + Context.OnError( + CurrentLocation, + RazorResources.ParseError_OuterTagMissingName, + length: 1 /* end of file */); + } + return false; + } + + private bool XmlPI() + { + // Accept "?" + Assert(HtmlSymbolType.QuestionMark); + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.QuestionMark, HtmlSymbolType.CloseAngle); + } + + private bool BangTag() + { + // Accept "!" + Assert(HtmlSymbolType.Bang); + + if (AcceptAndMoveNext()) + { + if (CurrentSymbol.Type == HtmlSymbolType.DoubleHyphen) + { + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.DoubleHyphen, HtmlSymbolType.CloseAngle); + } + else if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) + { + if (AcceptAndMoveNext()) + { + return CData(); + } + } + else + { + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.CloseAngle); + } + } + + return false; + } + + private bool CData() + { + if (CurrentSymbol.Type == HtmlSymbolType.Text && string.Equals(CurrentSymbol.Content, "cdata", StringComparison.OrdinalIgnoreCase)) + { + if (AcceptAndMoveNext()) + { + if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) + { + return AcceptUntilAll(HtmlSymbolType.RightBracket, HtmlSymbolType.RightBracket, HtmlSymbolType.CloseAngle); + } + } + } + + return false; + } + + private bool EndTag(SourceLocation tagStart, + Stack> tags, + IDisposable tagBlockWrapper) + { + // Accept "/" and move next + Assert(HtmlSymbolType.ForwardSlash); + var forwardSlash = CurrentSymbol; + if (!NextToken()) + { + Accept(_bufferedOpenAngle); + Accept(forwardSlash); + return false; + } + else + { + var tagName = string.Empty; + HtmlSymbol bangSymbol = null; + + if (At(HtmlSymbolType.Bang)) + { + bangSymbol = CurrentSymbol; + + var nextSymbol = Lookahead(count: 1); + + if (nextSymbol != null && nextSymbol.Type == HtmlSymbolType.Text) + { + tagName = "!" + nextSymbol.Content; + } + } + else if (At(HtmlSymbolType.Text)) + { + tagName = CurrentSymbol.Content; + } + + var matched = RemoveTag(tags, tagName, tagStart); + + if (tags.Count == 0 && + // Note tagName may contain a '!' escape character. This ensures doesn't match here. + // tags are treated like any other escaped HTML end tag. + string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) && + matched) + { + return EndTextTag(forwardSlash, tagBlockWrapper); + } + Accept(_bufferedOpenAngle); + Accept(forwardSlash); + + OptionalBangEscape(); + + AcceptUntil(HtmlSymbolType.CloseAngle); + + // Accept the ">" + return Optional(HtmlSymbolType.CloseAngle); + } + } + + private void RecoverTextTag() + { + // We don't want to skip-to and parse because there shouldn't be anything in the body of text tags. + AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.NewLine); + + // Include the close angle in the text tag block if it's there, otherwise just move on + Optional(HtmlSymbolType.CloseAngle); + } + + private bool EndTextTag(HtmlSymbol solidus, IDisposable tagBlockWrapper) + { + Accept(_bufferedOpenAngle); + Accept(solidus); + + var textLocation = CurrentLocation; + Assert(HtmlSymbolType.Text); + AcceptAndMoveNext(); + + var seenCloseAngle = Optional(HtmlSymbolType.CloseAngle); + + if (!seenCloseAngle) + { + Context.OnError( + textLocation, + RazorResources.ParseError_TextTagCannotContainAttributes, + length: 4 /* text */); + + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + RecoverTextTag(); + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Span.ChunkGenerator = SpanChunkGenerator.Null; + + CompleteTagBlockWithSpan(tagBlockWrapper, Span.EditHandler.AcceptedCharacters, SpanKind.Transition); + + return seenCloseAngle; + } + + // Special tags include + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 active parser must be the same as either the markup or code parser. + + + code + This is a literal used when composing ParserError_* messages. Most blocks are named by the keyword that starts them, for example "if". However, for those without keywords, a (localizable) name must be used. This literal is ALWAYS used mid-sentence, thus should not be capitalized. + + + explicit expression + This is a literal used when composing ParserError_* messages. Most blocks are named by the keyword that starts them, for example "if". However, for those without keywords, a (localizable) name must be used. This literal is ALWAYS used mid-sentence, thus should not be capitalized. + + + The "CancelBacktrack" method can be called only while in a look-ahead process started with the "BeginLookahead" method. + + + "EndBlock" was called without a matching call to "StartBlock". + + + The "@" character must be followed by a ":", "(", or a C# identifier. If you intended to switch to markup, use an HTML start tag, for example: + +@if(isLoggedIn) { + <p>Hello, @user!</p> +} + + + End of file was reached before the end of the block comment. All comments started with "/*" sequence must be terminated with a matching "*/" sequence. + + + An opening "{0}" is missing the corresponding closing "{1}". + + + The {0} block is missing a closing "{1}" character. Make sure you have a matching "{1}" character for all the "{2}" characters within this block, and that none of the "{1}" characters are being interpreted as markup. + + + Expected "{0}". + + + Inline markup blocks (@<p>Content</p>) cannot be nested. Only one level of inline markup is allowed. + + + Markup in a code block must start with a tag and all start tags must be matched with end tags. Do not use unclosed tags like "<br>". Instead use self-closing tags like "<br/>". + + + The "{0}" element was not closed. All elements must be either self-closing or have a matching end tag. + + + Sections cannot be empty. The "@section" keyword must be followed by a block of markup surrounded by "{}". For example: + +@section Sidebar { + <!-- Markup and text goes here --> +} + + + Namespace imports and type aliases cannot be placed within code blocks. They must immediately follow an "@" character in markup. It is recommended that you put them at the top of the page, as in the following example: + +@using System.Drawing; +@{ + // OK here to use types from System.Drawing in the page. +} + + + Expected a "{0}" but found a "{1}". Block statements must be enclosed in "{{" and "}}". You cannot use single-statement control-flow statements in CSHTML pages. For example, the following is not allowed: + +@if(isLoggedIn) + <p>Hello, @user</p> + +Instead, wrap the contents of the block in "{{}}": + +@if(isLoggedIn) {{ + <p>Hello, @user</p> +}} + {0} is only ever a single character + + + Encountered end tag "{0}" with no matching start tag. Are your start/end tags properly balanced? + + + Unexpected {0} after section keyword. Section names must start with an "_" or alphabetic character, and the remaining characters must be either "_" or alphanumeric. + + + "{0}" is not valid at the start of a code block. Only identifiers, keywords, comments, "(" and "{{" are valid. + "{{" is an escape sequence for string.Format, when outputted to the user it will be displayed as "{" + + + End of file or an unexpected character was reached before the "{0}" tag could be parsed. Elements inside markup blocks must be complete. They must either be self-closing ("<br />") or have matching end tags ("<p>Hello</p>"). If you intended to display a "<" character, use the "&lt;" HTML entity. + + + Unterminated string literal. Strings that start with a quotation mark (") must be terminated before the end of the line. However, strings that start with @ and a quotation mark (@") can span multiple lines. + + + @section Header { ... } + In CSHTML, the @section keyword is case-sensitive and lowercase (as with all C# keywords) + + + "<text>" and "</text>" tags cannot contain attributes. + + + A space or line break was encountered after the "@" character. Only valid identifiers, keywords, comments, "(" and "{" are valid at the start of a code block and they must occur immediately following "@" with no space in between. + + + The 'inherits' keyword must be followed by a type name on the same line. + + + Outer tag is missing a name. The first character of a markup block must be an HTML tag with a valid name. + + + End of file was reached before the end of the block comment. All comments that start with the "@*" sequence must be terminated with a matching "*@" sequence. + + + "{0}" character + + + end of file + + + space or line break + + + End-of-file was found after the "@" character. "@" must be followed by a valid code block. If you want to output an "@", escape it using the sequence: "@@" + + + The {0} property of the {1} structure cannot be null. + + + Parser was started with a null Context property. The Context property must be set BEFORE calling any methods on the parser. + + + "{0}" is a reserved word and cannot be used in implicit expressions. An explicit expression ("@()") must be used. + + + Cannot resume this symbol. Only the symbol immediately preceding the current one can be resumed. + + + Cannot finish span, there is no current block. Call StartBlock at least once before finishing a span + + + Cannot complete the tree, there are still open blocks. + + + Cannot complete the tree, StartBlock must be called at least once. + + + Cannot complete action, the parser has finished. Only CompleteParse can be called to extract the final parser results after the parser has finished + + + Block cannot be built because a Type has not been specified in the BlockBuilder + + + <<character literal>> + + + <<comment>> + + + <<identifier>> + + + <<integer literal>> + + + <<keyword>> + + + <<newline sequence>> + + + <<real literal>> + + + <<string literal>> + + + <<white space>> + + + <<unknown>> + + + In order to put a symbol back, it must have been the symbol which ended at the current position. The specified symbol ends at {0}, but the current position is {1} + + + Unexpected "{" after "@" character. Once inside the body of a code block (@if {}, @{}, etc.) you do not need to use "@{" to switch to code. + + + line break + + + <<newline sequence>> + + + <<razor comment>> + + + <<text>> + + + <<white space>> + + + The parser provided to the ParserContext was not a Markup Parser. + + + Cannot use built-in RazorComment handler, language characteristics does not define the CommentStart, CommentStar and CommentBody known symbol types or parser does not override TokenizerBackedParser.OutputSpanBeforeRazorComment + + + [BG][{0}] Shutdown + + + [BG][{0}] Startup + + + [BG][{0}] {1} changes arrived + + + [BG][{0}] Discarded {1} changes + + + [BG][{0}] Collecting {1} discarded changes + + + Disabled + + + [P][{0}] {3} Change in {2} milliseconds: {1} + + + [P][{0}] Received Change: {1} + + + Enabled + + + [Razor] {0} + + + [BG][{0}] no changes arrived? + + + [BG][{0}] Parse Complete in {1} milliseconds + + + [M][{0}] Queuing Parse for: {1} + + + [Razor] Editor Tracing {0} + + + [BG][{0}] Trees Compared in {1} milliseconds. Different = {2} + + + Section blocks ("{0}") cannot be nested. Only one level of section blocks are allowed. + + + Tag Helper '{0}'s attributes must have names. + + + The tag helper '{0}' must not have C# in the element's attribute declaration area. + + + Directive '{0}' must have a value. + + + Directive '{0}'s value must be surrounded in double quotes. + + + Found a malformed '{0}' tag helper. Tag helpers must have a start and end tag or be self closing. + + + Missing close angle for tag helper '{0}'. + + + TagHelper attributes must be well-formed. + + + The tag helper attribute '{0}' in element '{1}' is missing a key. The syntax is '<{1} {0}{{ key }}="value">'. + + + Non-string tag helper attribute values must not be empty. Add an expression to this attribute value. + + + Code blocks (e.g. @{{var variable = 23;}}) must not appear in non-string tag helper attribute values. + Already in an expression (code) context. If necessary an explicit expression (e.g. @(@readonly)) may be used. + + + @'{0}' directives must not appear in non-string tag helper attribute values. + + + Inline markup blocks (e.g. @<p>content</p>) must not appear in non-string tag helper attribute values. + Expected a '{0}' attribute value, not a string. + + + Attribute '{0}' on tag helper element '{1}' requires a value. Tag helper bound attributes of type '{2}' cannot be empty or contain only whitespace. + + + Cannot perform '{1}' operations on '{0}' instances with different file paths. + + + Tag helpers '{0}' and '{1}' targeting element '{2}' must not expect different {3} values. + + + Found an end tag (</{0}>) for tag helper '{1}' with tag structure that disallows an end tag ('{2}'). + + + The parent <{0}> tag helper does not allow non-tag content. Only child tag helper(s) targeting tag name(s) '{1}' are allowed. + + + The <{0}> tag is not allowed by parent <{1}> tag helper. Only child tags with name(s) '{2}' are allowed. + + + The {0} directive is not supported. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.VSRC1/RazorTemplateEngine.cs b/src/Microsoft.AspNet.Razor.VSRC1/RazorTemplateEngine.cs new file mode 100644 index 0000000000..b89ae9c0f4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/RazorTemplateEngine.cs @@ -0,0 +1,454 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using Microsoft.AspNet.Razor.Chunks.Generators; +using Microsoft.AspNet.Razor.CodeGenerators; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor +{ + ///

+ /// Entry-point to the Razor Template Engine + /// + public class RazorTemplateEngine + { + private const int BufferSize = 1024; + public static readonly string DefaultClassName = "Template"; + public static readonly string DefaultNamespace = string.Empty; + + /// + /// Constructs a new RazorTemplateEngine with the specified host + /// + /// + /// The host which defines the environment in which the generated template code will live. + /// + public RazorTemplateEngine(RazorEngineHost host) + { + if (host == null) + { + throw new ArgumentNullException(nameof(host)); + } + + Host = host; + } + + /// + /// The RazorEngineHost which defines the environment in which the generated template code will live + /// + public RazorEngineHost Host { get; } + + public ParserResults ParseTemplate(ITextBuffer input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return ParseTemplate(input, cancelToken: null); + } + + /// + /// Parses the template specified by the TextBuffer and returns it's result + /// + /// + /// + /// IMPORTANT: This does NOT need to be called before GeneratedCode! GenerateCode will automatically + /// parse the document first. + /// + /// + /// The cancel token provided can be used to cancel the parse. However, please note + /// that the parse occurs _synchronously_, on the callers thread. This parameter is + /// provided so that if the caller is in a background thread with a CancellationToken, + /// it can pass it along to the parser. + /// + /// + /// The input text to parse. + /// A token used to cancel the parser. + /// The resulting parse tree. + [SuppressMessage( + "Microsoft.Reliability", "CA2000:Dispose objects before losing scope", + Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " + + "we don't want to dispose it")] + public ParserResults ParseTemplate(ITextBuffer input, CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return ParseTemplateCore(input.ToDocument(), sourceFileName: null, cancelToken: cancelToken); + } + + // See ParseTemplate(ITextBuffer, CancellationToken?), + // this overload simply wraps a TextReader in a TextBuffer (see ITextBuffer and BufferingTextReader) + public ParserResults ParseTemplate(TextReader input, string sourceFileName) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return ParseTemplateCore(new SeekableTextReader(input), sourceFileName, cancelToken: null); + } + + [SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " + + "we don't want to dispose it")] + public ParserResults ParseTemplate(TextReader input, CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return ParseTemplateCore(new SeekableTextReader(input), sourceFileName: null, cancelToken: cancelToken); + } + + protected internal virtual ParserResults ParseTemplateCore( + ITextDocument input, + string sourceFileName, + CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + // Construct the parser + var parser = CreateParser(sourceFileName); + Debug.Assert(parser != null); + return parser.Parse(input); + } + + public GeneratorResults GenerateCode(ITextBuffer input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCode(input, className: null, rootNamespace: null, sourceFileName: null, cancelToken: null); + } + + public GeneratorResults GenerateCode(ITextBuffer input, CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCode( + input, + className: null, + rootNamespace: null, + sourceFileName: null, + cancelToken: cancelToken); + } + + public GeneratorResults GenerateCode( + ITextBuffer input, + string className, + string rootNamespace, + string sourceFileName) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCode(input, className, rootNamespace, sourceFileName, cancelToken: null); + } + + /// + /// Parses the template specified by the TextBuffer, generates code for it, and returns the constructed code. + /// + /// + /// + /// The cancel token provided can be used to cancel the parse. However, please note + /// that the parse occurs _synchronously_, on the callers thread. This parameter is + /// provided so that if the caller is in a background thread with a CancellationToken, + /// it can pass it along to the parser. + /// + /// + /// The className, rootNamespace and sourceFileName parameters are optional and override the default + /// specified by the Host. For example, the WebPageRazorHost in System.Web.WebPages.Razor configures the + /// Class Name, Root Namespace and Source File Name based on the virtual path of the page being compiled. + /// However, the built-in RazorEngineHost class uses constant defaults, so the caller will likely want to + /// change them using these parameters. + /// + /// + /// The input text to parse. + /// A token used to cancel the parser. + /// + /// The name of the generated class, overriding whatever is specified in the Host. The default value (defined + /// in the Host) can be used by providing null for this argument. + /// + /// The namespace in which the generated class will reside, overriding whatever is + /// specified in the Host. The default value (defined in the Host) can be used by providing null for this + /// argument. + /// + /// + /// The file name to use in line pragmas, usually the original Razor file, overriding whatever is specified in + /// the Host. The default value (defined in the Host) can be used by providing null for this argument. + /// + /// The resulting parse tree AND generated code. + [SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " + + "we don't want to dispose it")] + public GeneratorResults GenerateCode( + ITextBuffer input, + string className, + string rootNamespace, + string sourceFileName, + CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCodeCore( + input.ToDocument(), + className, + rootNamespace, + sourceFileName, + checksum: null, + cancelToken: cancelToken); + } + + // See GenerateCode override which takes ITextBuffer, and BufferingTextReader for details. + public GeneratorResults GenerateCode(TextReader input) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCode(input, className: null, rootNamespace: null, sourceFileName: null, cancelToken: null); + } + + public GeneratorResults GenerateCode(TextReader input, CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCode( + input, + className: null, + rootNamespace: null, + sourceFileName: null, + cancelToken: cancelToken); + } + + public GeneratorResults GenerateCode( + TextReader input, + string className, + + string rootNamespace, string sourceFileName) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCode(input, className, rootNamespace, sourceFileName, cancelToken: null); + } + + /// + /// Parses the contents specified by the and returns the generated code. + /// + /// A that represents the contents to be parsed. + /// The name of the generated class. When null, defaults to + /// (Host.DefaultClassName). + /// The namespace in which the generated class will reside. When null, + /// defaults to (Host.DefaultNamespace). + /// + /// The file name to use in line pragmas, usually the original Razor file. + /// + /// A that represents the results of parsing the content. + /// + /// This overload calculates the checksum of the contents of prior to code + /// generation. The checksum is used for producing the #pragma checksum line pragma required for + /// debugging. + /// + public GeneratorResults GenerateCode( + Stream inputStream, + string className, + string rootNamespace, + string sourceFileName) + { + if (inputStream == null) + { + throw new ArgumentNullException(nameof(inputStream)); + } + + MemoryStream memoryStream = null; + string checksum = null; + try + { + if (!Host.DesignTimeMode) + { + // We don't need to calculate the checksum in design time. + if (!inputStream.CanSeek) + { + memoryStream = new MemoryStream(); + inputStream.CopyTo(memoryStream); + + // We don't have to dispose the input stream since it is owned externally. + inputStream = memoryStream; + } + + inputStream.Position = 0; + checksum = ComputeChecksum(inputStream); + inputStream.Position = 0; + } + + using (var reader = + new StreamReader( + inputStream, + Encoding.UTF8, + detectEncodingFromByteOrderMarks: true, + bufferSize: BufferSize, + leaveOpen: true)) + { + var seekableStream = new SeekableTextReader(reader); + return GenerateCodeCore( + seekableStream, + className, + rootNamespace, + sourceFileName, + checksum, + cancelToken: null); + } + } + finally + { + if (memoryStream != null) + { + memoryStream.Dispose(); + } + } + } + + [SuppressMessage( + "Microsoft.Reliability", + "CA2000:Dispose objects before losing scope", + Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " + + "we don't want to dispose it")] + public GeneratorResults GenerateCode( + TextReader input, + string className, + string rootNamespace, + string sourceFileName, + CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + return GenerateCodeCore( + new SeekableTextReader(input), + className, + rootNamespace, + sourceFileName, + checksum: null, + cancelToken: cancelToken); + } + + protected internal virtual GeneratorResults GenerateCodeCore( + ITextDocument input, + string className, + string rootNamespace, + string sourceFileName, + string checksum, + CancellationToken? cancelToken) + { + if (input == null) + { + throw new ArgumentNullException(nameof(input)); + } + + className = (className ?? Host.DefaultClassName) ?? DefaultClassName; + rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace; + + // Run the parser + var parser = CreateParser(sourceFileName); + Debug.Assert(parser != null); + var results = parser.Parse(input); + + // Generate code + var chunkGenerator = CreateChunkGenerator(className, rootNamespace, sourceFileName); + chunkGenerator.DesignTimeMode = Host.DesignTimeMode; + chunkGenerator.Visit(results); + + var codeGeneratorContext = new CodeGeneratorContext(chunkGenerator.Context, results.ErrorSink); + codeGeneratorContext.Checksum = checksum; + var codeGenerator = CreateCodeGenerator(codeGeneratorContext); + var codeGeneratorResult = codeGenerator.Generate(); + + // Collect results and return + return new GeneratorResults(results, codeGeneratorResult, codeGeneratorContext.ChunkTreeBuilder.ChunkTree); + } + + protected internal virtual RazorChunkGenerator CreateChunkGenerator( + string className, + string rootNamespace, + string sourceFileName) + { + return Host.DecorateChunkGenerator( + Host.CodeLanguage.CreateChunkGenerator(className, rootNamespace, sourceFileName, Host)); + } + + protected internal virtual RazorParser CreateParser(string sourceFileName) + { + var codeParser = Host.CodeLanguage.CreateCodeParser(); + var markupParser = Host.CreateMarkupParser(); + + var parser = new RazorParser( + Host.DecorateCodeParser(codeParser), + Host.DecorateMarkupParser(markupParser), + Host.TagHelperDescriptorResolver) + { + DesignTimeMode = Host.DesignTimeMode + }; + + return Host.DecorateRazorParser(parser, sourceFileName); + } + + protected internal virtual CodeGenerator CreateCodeGenerator(CodeGeneratorContext context) + { + return Host.DecorateCodeGenerator(Host.CodeLanguage.CreateCodeGenerator(context), context); + } + + private static string ComputeChecksum(Stream inputStream) + { + byte[] hashedBytes; + using (var hashAlgorithm = SHA1.Create()) + { + hashedBytes = hashAlgorithm.ComputeHash(inputStream); + } + + var fileHashBuilder = new StringBuilder(hashedBytes.Length * 2); + foreach (var value in hashedBytes) + { + fileHashBuilder.Append(value.ToString("x2")); + } + return fileHashBuilder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/SourceLocation.cs b/src/Microsoft.AspNet.Razor.VSRC1/SourceLocation.cs new file mode 100644 index 0000000000..318f6cd1aa --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/SourceLocation.cs @@ -0,0 +1,254 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor +{ + /// + /// A location in a Razor file. + /// +#if NET451 + // No Serializable attribute in CoreCLR (no need for it anymore?) + [Serializable] +#endif + public struct SourceLocation : IEquatable, IComparable + { + /// + /// An undefined . + /// + public static readonly SourceLocation Undefined = + new SourceLocation(absoluteIndex: -1, lineIndex: -1, characterIndex: -1); + + /// + /// A with , , and + /// initialized to 0. + /// + public static readonly SourceLocation Zero = + new SourceLocation(absoluteIndex: 0, lineIndex: 0, characterIndex: 0); + + /// + /// Initializes a new instance of . + /// + /// The absolute index. + /// The line index. + /// The character index. + public SourceLocation(int absoluteIndex, int lineIndex, int characterIndex) + : this(filePath: null, absoluteIndex: absoluteIndex, lineIndex: lineIndex, characterIndex: characterIndex) + { + } + + /// + /// Initializes a new instance of . + /// + /// The file path. + /// The absolute index. + /// The line index. + /// The character index. + public SourceLocation(string filePath, int absoluteIndex, int lineIndex, int characterIndex) + { + FilePath = filePath; + AbsoluteIndex = absoluteIndex; + LineIndex = lineIndex; + CharacterIndex = characterIndex; + } + + /// + /// Path of the file. + /// + /// When null, the parser assumes the location is in the file currently being processed. + /// + public string FilePath { get; set; } + + /// Set property is only accessible for deserialization purposes. + public int AbsoluteIndex { get; set; } + + /// + /// Gets the 1-based index of the line referred to by this Source Location. + /// + /// Set property is only accessible for deserialization purposes. + public int LineIndex { get; set; } + + /// Set property is only accessible for deserialization purposes. + public int CharacterIndex { get; set; } + + /// + public override string ToString() + { + return string.Format( + CultureInfo.CurrentCulture, + "({0}:{1},{2})", + AbsoluteIndex, + LineIndex, + CharacterIndex); + } + + /// + public override bool Equals(object obj) + { + return obj is SourceLocation && + Equals((SourceLocation)obj); + } + + /// + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(FilePath, StringComparer.Ordinal); + hashCodeCombiner.Add(AbsoluteIndex); + + return hashCodeCombiner; + } + + /// + public bool Equals(SourceLocation other) + { + // LineIndex and CharacterIndex can be calculated from AbsoluteIndex and the document content. + return CompareTo(other) == 0; + } + + /// + public int CompareTo(SourceLocation other) + { + var filePathOrdinal = string.Compare(FilePath, other.FilePath, StringComparison.Ordinal); + if (filePathOrdinal != 0) + { + return filePathOrdinal; + } + + return AbsoluteIndex.CompareTo(other.AbsoluteIndex); + } + + /// + /// Advances the by the length of the . + /// + /// The to advance. + /// The to advance by. + /// The advanced . + public static SourceLocation Advance(SourceLocation left, string text) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + var tracker = new SourceLocationTracker(left); + tracker.UpdateLocation(text); + return tracker.CurrentLocation; + } + + /// + /// Adds two s. + /// + /// The left operand. + /// The right operand. + /// A that is the sum of the left and right operands. + /// if the of the left and right operands + /// are different, and neither is null. + public static SourceLocation operator +(SourceLocation left, SourceLocation right) + { + if (!string.Equals(left.FilePath, right.FilePath, StringComparison.Ordinal) && + left.FilePath != null && + right.FilePath != null) + { + // Throw if FilePath for left and right are different, and neither is null. + throw new ArgumentException( + RazorResources.FormatSourceLocationFilePathDoesNotMatch(nameof(SourceLocation), "+"), + nameof(right)); + } + + var resultFilePath = left.FilePath ?? right.FilePath; + if (right.LineIndex > 0) + { + // Column index doesn't matter + return new SourceLocation( + resultFilePath, + left.AbsoluteIndex + right.AbsoluteIndex, + left.LineIndex + right.LineIndex, + right.CharacterIndex); + } + else + { + return new SourceLocation( + resultFilePath, + left.AbsoluteIndex + right.AbsoluteIndex, + left.LineIndex + right.LineIndex, + left.CharacterIndex + right.CharacterIndex); + } + } + + /// + /// Subtracts two s. + /// + /// The left operand. + /// The right operand. + /// A that is the difference of the left and right operands. + /// if the of the left and right operands + /// are different. + public static SourceLocation operator -(SourceLocation left, SourceLocation right) + { + if (!string.Equals(left.FilePath, right.FilePath, StringComparison.Ordinal)) + { + throw new ArgumentException( + RazorResources.FormatSourceLocationFilePathDoesNotMatch(nameof(SourceLocation), "-"), + nameof(right)); + } + + var characterIndex = left.LineIndex != right.LineIndex ? + left.CharacterIndex : left.CharacterIndex - right.CharacterIndex; + + return new SourceLocation( + filePath: null, + absoluteIndex: left.AbsoluteIndex - right.AbsoluteIndex, + lineIndex: left.LineIndex - right.LineIndex, + characterIndex: characterIndex); + } + + /// + /// Determines whether the first operand is less than the second operand. + /// + /// The left operand. + /// The right operand. + /// true if is less than . + public static bool operator <(SourceLocation left, SourceLocation right) + { + return left.CompareTo(right) < 0; + } + + /// + /// Determines whether the first operand is greater than the second operand. + /// + /// The left operand. + /// The right operand. + /// true if is greater than . + public static bool operator >(SourceLocation left, SourceLocation right) + { + return left.CompareTo(right) > 0; + } + + /// + /// Determines whether the operands are equal. + /// + /// The left operand. + /// The right operand. + /// true if and are equal. + public static bool operator ==(SourceLocation left, SourceLocation right) + { + return left.Equals(right); + } + + /// + /// Determines whether the operands are not equal. + /// + /// The left operand. + /// The right operand. + /// true if and are not equal. + public static bool operator !=(SourceLocation left, SourceLocation right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/StateMachine.cs b/src/Microsoft.AspNet.Razor.VSRC1/StateMachine.cs new file mode 100644 index 0000000000..0439744b67 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/StateMachine.cs @@ -0,0 +1,106 @@ +// 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.AspNet.Razor +{ + public abstract class StateMachine + { + protected delegate StateResult State(); + + protected abstract State StartState { get; } + + protected State CurrentState { get; set; } + + protected virtual TReturn Turn() + { + if (CurrentState != null) + { + StateResult result; + do + { + // Keep running until we get a null result or output + result = CurrentState(); + CurrentState = result.Next; + } + while (result != null && !result.HasOutput); + + if (result == null) + { + return default(TReturn); // Terminated + } + return result.Output; + } + return default(TReturn); + } + + /// + /// Returns a result indicating that the machine should stop executing and return null output. + /// + protected StateResult Stop() + { + return null; + } + + /// + /// Returns a result indicating that this state has no output and the machine should immediately invoke the specified state + /// + /// + /// By returning no output, the state machine will invoke the next state immediately, before returning + /// controller to the caller of + /// + protected StateResult Transition(State newState) + { + return new StateResult(newState); + } + + /// + /// Returns a result containing the specified output and indicating that the next call to + /// should invoke the provided state. + /// + protected StateResult Transition(TReturn output, State newState) + { + return new StateResult(output, newState); + } + + /// + /// Returns a result indicating that this state has no output and the machine should remain in this state + /// + /// + /// By returning no output, the state machine will re-invoke the current state again before returning + /// controller to the caller of + /// + protected StateResult Stay() + { + return new StateResult(CurrentState); + } + + /// + /// Returns a result containing the specified output and indicating that the next call to + /// should re-invoke the current state. + /// + protected StateResult Stay(TReturn output) + { + return new StateResult(output, CurrentState); + } + + protected class StateResult + { + public StateResult(State next) + { + HasOutput = false; + Next = next; + } + + public StateResult(TReturn output, State next) + { + HasOutput = true; + Output = output; + Next = next; + } + + public bool HasOutput { get; set; } + public TReturn Output { get; set; } + public State Next { get; set; } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/TagHelpers/TagMode.cs b/src/Microsoft.AspNet.Razor.VSRC1/TagHelpers/TagMode.cs new file mode 100644 index 0000000000..d6c33d4b4e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/TagHelpers/TagMode.cs @@ -0,0 +1,26 @@ +// 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.AspNet.Razor.TagHelpers +{ + /// + /// The mode in which an element should render. + /// + public enum TagMode + { + /// + /// Include both start and end tags. + /// + StartTagAndEndTag, + + /// + /// A self-closed tag. + /// + SelfClosing, + + /// + /// Only a start tag. + /// + StartTagOnly + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/TagHelpers/TagStructure.cs b/src/Microsoft.AspNet.Razor.VSRC1/TagHelpers/TagStructure.cs new file mode 100644 index 0000000000..5c801f3e27 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/TagHelpers/TagStructure.cs @@ -0,0 +1,28 @@ +// 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.AspNet.Razor.TagHelpers +{ + /// + /// The structure the element should be written in. + /// + public enum TagStructure + { + /// + /// If no other tag helper applies to the same element and specifies a , + /// will be used. + /// + Unspecified, + + /// + /// Element can be written as <my-tag-helper></my-tag-helper> or <my-tag-helper />. + /// + NormalOrSelfClosing, + + /// + /// Element can be written as <my-tag-helper> or <my-tag-helper />. + /// + /// Elements with a structure will never have any content. + WithoutEndTag + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/BufferingTextReader.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/BufferingTextReader.cs new file mode 100644 index 0000000000..b247b416e4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/BufferingTextReader.cs @@ -0,0 +1,201 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.AspNet.Razor.Utils; + +namespace Microsoft.AspNet.Razor.Text +{ + public class BufferingTextReader : LookaheadTextReader + { + private Stack _backtrackStack = new Stack(); + private int _currentBufferPosition; + + private int _currentCharacter; + private SourceLocationTracker _locationTracker; + + public BufferingTextReader(TextReader source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + InnerReader = source; + _locationTracker = new SourceLocationTracker(); + + UpdateCurrentCharacter(); + } + + internal StringBuilder Buffer { get; set; } + internal bool Buffering { get; set; } + internal TextReader InnerReader { get; private set; } + + public override SourceLocation CurrentLocation + { + get { return _locationTracker.CurrentLocation; } + } + + protected virtual int CurrentCharacter + { + get { return _currentCharacter; } + } + + public override int Read() + { + var ch = CurrentCharacter; + NextCharacter(); + return ch; + } + + // TODO: Optimize Read(char[],int,int) to copy direct from the buffer where possible + + public override int Peek() + { + return CurrentCharacter; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + InnerReader.Dispose(); + } + base.Dispose(disposing); + } + + public override IDisposable BeginLookahead() + { + // Is this our first lookahead? + if (Buffer == null) + { + // Yes, setup the backtrack buffer + Buffer = new StringBuilder(); + } + + if (!Buffering) + { + // We're not already buffering, so we need to expand the buffer to hold the first character + ExpandBuffer(); + Buffering = true; + } + + // Mark the position to return to when we backtrack + // Use the closures and the "using" statement rather than an explicit stack + var context = new BacktrackContext() + { + BufferIndex = _currentBufferPosition, + Location = CurrentLocation + }; + _backtrackStack.Push(context); + return new DisposableAction(() => + { + EndLookahead(context); + }); + } + + // REVIEW: This really doesn't sound like the best name for this... + public override void CancelBacktrack() + { + if (_backtrackStack.Count == 0) + { + throw new InvalidOperationException(RazorResources.CancelBacktrack_Must_Be_Called_Within_Lookahead); + } + // Just pop the current backtrack context so that when the lookahead ends, it won't be backtracked + _backtrackStack.Pop(); + } + + private void EndLookahead(BacktrackContext context) + { + // If the specified context is not the one on the stack, it was popped by a call to DoNotBacktrack + if (_backtrackStack.Count > 0 && ReferenceEquals(_backtrackStack.Peek(), context)) + { + _backtrackStack.Pop(); + _currentBufferPosition = context.BufferIndex; + _locationTracker.CurrentLocation = context.Location; + + UpdateCurrentCharacter(); + } + } + + protected virtual void NextCharacter() + { + var prevChar = CurrentCharacter; + if (prevChar == -1) + { + return; // We're at the end of the source + } + + if (Buffering) + { + if (_currentBufferPosition >= Buffer.Length - 1) + { + // If there are no more lookaheads (thus no need to continue with the buffer) we can just clean up the buffer + if (_backtrackStack.Count == 0) + { + // Reset the buffer + Buffer.Length = 0; + _currentBufferPosition = 0; + Buffering = false; + } + else if (!ExpandBuffer()) + { + // Failed to expand the buffer, because we're at the end of the source + _currentBufferPosition = Buffer.Length; // Force the position past the end of the buffer + } + } + else + { + // Not at the end yet, just advance the buffer pointer + _currentBufferPosition++; + } + } + else + { + // Just act like normal + InnerReader.Read(); // Don't care about the return value, Peek() is used to get characters from the source + } + + UpdateCurrentCharacter(); + _locationTracker.UpdateLocation((char)prevChar, (char)CurrentCharacter); + } + + protected bool ExpandBuffer() + { + // Pull another character into the buffer and update the position + var ch = InnerReader.Read(); + + // Only append the character to the buffer if there actually is one + if (ch != -1) + { + Buffer.Append((char)ch); + _currentBufferPosition = Buffer.Length - 1; + return true; + } + return false; + } + + private void UpdateCurrentCharacter() + { + if (Buffering && _currentBufferPosition < Buffer.Length) + { + // Read from the buffer + _currentCharacter = (int)Buffer[_currentBufferPosition]; + } + else + { + // No buffer? Peek from the source + _currentCharacter = InnerReader.Peek(); + } + } + + private class BacktrackContext + { + public int BufferIndex { get; set; } + public SourceLocation Location { get; set; } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/ITextBuffer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/ITextBuffer.cs new file mode 100644 index 0000000000..17bc920401 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/ITextBuffer.cs @@ -0,0 +1,19 @@ +// 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.AspNet.Razor.Text +{ + public interface ITextBuffer + { + int Length { get; } + int Position { get; set; } + int Read(); + int Peek(); + } + + // TextBuffer with Location tracking + public interface ITextDocument : ITextBuffer + { + SourceLocation Location { get; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/LineTrackingStringBuffer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/LineTrackingStringBuffer.cs new file mode 100644 index 0000000000..ef6e878067 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/LineTrackingStringBuffer.cs @@ -0,0 +1,166 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNet.Razor.Parser; + +namespace Microsoft.AspNet.Razor.Text +{ + internal class LineTrackingStringBuffer + { + private TextLine _currentLine; + private TextLine _endLine; + private IList _lines; + + public LineTrackingStringBuffer() + { + _endLine = new TextLine(0, 0); + _lines = new List() { _endLine }; + } + + public int Length + { + get { return _endLine.End; } + } + + public SourceLocation EndLocation + { + get { return new SourceLocation(Length, _lines.Count - 1, _lines[_lines.Count - 1].Length); } + } + + public void Append(string content) + { + for (int i = 0; i < content.Length; i++) + { + AppendCore(content[i]); + + // \r on it's own: Start a new line, otherwise wait for \n + // Other Newline: Start a new line + if ((content[i] == '\r' && (i + 1 == content.Length || content[i + 1] != '\n')) || (content[i] != '\r' && ParserHelpers.IsNewLine(content[i]))) + { + PushNewLine(); + } + } + } + + public CharacterReference CharAt(int absoluteIndex) + { + var line = FindLine(absoluteIndex); + if (line == null) + { + throw new ArgumentOutOfRangeException(nameof(absoluteIndex)); + } + var idx = absoluteIndex - line.Start; + return new CharacterReference(line.Content[idx], new SourceLocation(absoluteIndex, line.Index, idx)); + } + + private void PushNewLine() + { + _endLine = new TextLine(_endLine.End, _endLine.Index + 1); + _lines.Add(_endLine); + } + + private void AppendCore(char chr) + { + Debug.Assert(_lines.Count > 0); + _lines[_lines.Count - 1].Content.Append(chr); + } + + private TextLine FindLine(int absoluteIndex) + { + TextLine selected = null; + + if (_currentLine != null) + { + if (_currentLine.Contains(absoluteIndex)) + { + // This index is on the last read line + selected = _currentLine; + } + else if (absoluteIndex > _currentLine.Index && _currentLine.Index + 1 < _lines.Count) + { + // This index is ahead of the last read line + selected = ScanLines(absoluteIndex, _currentLine.Index); + } + } + + // Have we found a line yet? + if (selected == null) + { + // Scan from line 0 + selected = ScanLines(absoluteIndex, 0); + } + + Debug.Assert(selected == null || selected.Contains(absoluteIndex)); + _currentLine = selected; + return selected; + } + + private TextLine ScanLines(int absoluteIndex, int startPos) + { + for (int i = 0; i < _lines.Count; i++) + { + var idx = (i + startPos) % _lines.Count; + Debug.Assert(idx >= 0 && idx < _lines.Count); + + if (_lines[idx].Contains(absoluteIndex)) + { + return _lines[idx]; + } + } + return null; + } + + internal struct CharacterReference + { + private readonly char _character; + private readonly SourceLocation _location; + + public CharacterReference(char character, SourceLocation location) + { + _character = character; + _location = location; + } + + public char Character { get { return _character; } } + public SourceLocation Location { get { return _location; } } + } + + private class TextLine + { + private StringBuilder _content = new StringBuilder(); + + public TextLine(int start, int index) + { + Start = start; + Index = index; + } + + public StringBuilder Content + { + get { return _content; } + } + + public int Length + { + get { return Content.Length; } + } + + public int Start { get; set; } + public int Index { get; set; } + + public int End + { + get { return Start + Length; } + } + + public bool Contains(int index) + { + return index < End && index >= Start; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/LocationTagged.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/LocationTagged.cs new file mode 100644 index 0000000000..5156b7a212 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/LocationTagged.cs @@ -0,0 +1,104 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Text +{ + [DebuggerDisplay("({Location})\"{Value}\"")] + public class LocationTagged : IFormattable + { + private LocationTagged() + { + Location = SourceLocation.Undefined; + Value = default(TValue); + } + + public LocationTagged(TValue value, int offset, int line, int col) + : this(value, new SourceLocation(offset, line, col)) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + } + + public LocationTagged(TValue value, SourceLocation location) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Location = location; + Value = value; + } + + public SourceLocation Location { get; } + + public TValue Value { get; } + + public override bool Equals(object obj) + { + LocationTagged other = obj as LocationTagged; + if (ReferenceEquals(other, null)) + { + return false; + } + + return Equals(other.Location, Location) && + Equals(other.Value, Value); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Location); + hashCodeCombiner.Add(Value); + + return hashCodeCombiner.CombinedHash; + } + + public override string ToString() + { + return Value.ToString(); + } + + public string ToString(string format, IFormatProvider formatProvider) + { + if (string.IsNullOrEmpty(format)) + { + format = "P"; + } + if (formatProvider == null) + { + formatProvider = CultureInfo.CurrentCulture; + } + switch (format.ToUpperInvariant()) + { + case "F": + return string.Format(formatProvider, "{0}@{1}", Value, Location); + default: + return Value.ToString(); + } + } + + public static implicit operator TValue(LocationTagged value) + { + return value == null ? default(TValue) : value.Value; + } + + public static bool operator ==(LocationTagged left, LocationTagged right) + { + return Equals(left, right); + } + + public static bool operator !=(LocationTagged left, LocationTagged right) + { + return !Equals(left, right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/LookaheadTextReader.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/LookaheadTextReader.cs new file mode 100644 index 0000000000..7d84a548a4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/LookaheadTextReader.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.AspNet.Razor.Text +{ + public abstract class LookaheadTextReader : TextReader + { + public abstract SourceLocation CurrentLocation { get; } + public abstract IDisposable BeginLookahead(); + public abstract void CancelBacktrack(); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/LookaheadToken.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/LookaheadToken.cs new file mode 100644 index 0000000000..063c5f95fb --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/LookaheadToken.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.Text +{ + public class LookaheadToken : IDisposable + { + private Action _cancelAction; + private bool _accepted; + + public LookaheadToken(Action cancelAction) + { + _cancelAction = cancelAction; + } + + public void Accept() + { + _accepted = true; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_accepted) + { + _cancelAction(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/SeekableTextReader.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/SeekableTextReader.cs new file mode 100644 index 0000000000..d055eded9e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/SeekableTextReader.cs @@ -0,0 +1,109 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.AspNet.Razor.Text +{ + public class SeekableTextReader : TextReader, ITextDocument + { + private int _position = 0; + private LineTrackingStringBuffer _buffer = new LineTrackingStringBuffer(); + private SourceLocation _location = SourceLocation.Zero; + private char? _current; + + public SeekableTextReader(string content) + { + _buffer.Append(content); + UpdateState(); + } + + public SeekableTextReader(TextReader source) + : this(source.ReadToEnd()) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + } + + public SeekableTextReader(ITextBuffer buffer) + : this(buffer.ReadToEnd()) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + } + + public SourceLocation Location + { + get { return _location; } + } + + public int Length + { + get { return _buffer.Length; } + } + + public int Position + { + get { return _position; } + set + { + if (_position != value) + { + _position = value; + UpdateState(); + } + } + } + + internal LineTrackingStringBuffer Buffer + { + get { return _buffer; } + } + + public override int Read() + { + if (_current == null) + { + return -1; + } + var chr = _current.Value; + _position++; + UpdateState(); + return chr; + } + + public override int Peek() + { + if (_current == null) + { + return -1; + } + return _current.Value; + } + + private void UpdateState() + { + if (_position < _buffer.Length) + { + LineTrackingStringBuffer.CharacterReference chr = _buffer.CharAt(_position); + _current = chr.Character; + _location = chr.Location; + } + else if (_buffer.Length == 0) + { + _current = null; + _location = SourceLocation.Zero; + } + else + { + _current = null; + _location = _buffer.EndLocation; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/SourceLocationTracker.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/SourceLocationTracker.cs new file mode 100644 index 0000000000..280917eae0 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/SourceLocationTracker.cs @@ -0,0 +1,102 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser; + +namespace Microsoft.AspNet.Razor.Text +{ + public class SourceLocationTracker + { + private int _absoluteIndex = 0; + private int _characterIndex = 0; + private int _lineIndex = 0; + private SourceLocation _currentLocation; + + public SourceLocationTracker() + : this(SourceLocation.Zero) + { + } + + public SourceLocationTracker(SourceLocation currentLocation) + { + CurrentLocation = currentLocation; + + UpdateInternalState(); + } + + public SourceLocation CurrentLocation + { + get + { + return _currentLocation; + } + set + { + if (_currentLocation != value) + { + _currentLocation = value; + UpdateInternalState(); + } + } + } + + public void UpdateLocation(char characterRead, char nextCharacter) + { + UpdateCharacterCore(characterRead, nextCharacter); + RecalculateSourceLocation(); + } + + public SourceLocationTracker UpdateLocation(string content) + { + for (int i = 0; i < content.Length; i++) + { + var nextCharacter = '\0'; + if (i < content.Length - 1) + { + nextCharacter = content[i + 1]; + } + UpdateCharacterCore(content[i], nextCharacter); + } + RecalculateSourceLocation(); + return this; + } + + private void UpdateCharacterCore(char characterRead, char nextCharacter) + { + _absoluteIndex++; + + if (Environment.NewLine.Length == 1 && characterRead == Environment.NewLine[0] || + ParserHelpers.IsNewLine(characterRead) && (characterRead != '\r' || nextCharacter != '\n')) + { + _lineIndex++; + _characterIndex = 0; + } + else + { + _characterIndex++; + } + } + + private void UpdateInternalState() + { + _absoluteIndex = CurrentLocation.AbsoluteIndex; + _characterIndex = CurrentLocation.CharacterIndex; + _lineIndex = CurrentLocation.LineIndex; + } + + private void RecalculateSourceLocation() + { + _currentLocation = new SourceLocation( + _currentLocation.FilePath, + _absoluteIndex, + _lineIndex, + _characterIndex); + } + + public static SourceLocation CalculateNewLocation(SourceLocation lastPosition, string newContent) + { + return new SourceLocationTracker(lastPosition).UpdateLocation(newContent).CurrentLocation; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/SourceSpan.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/SourceSpan.cs new file mode 100644 index 0000000000..147af1be44 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/SourceSpan.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.AspNet.Razor.Text +{ + public class SourceSpan + { + public SourceLocation Begin { get; set; } + public SourceLocation End { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/TextBufferReader.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextBufferReader.cs new file mode 100644 index 0000000000..3aa56ffbb4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextBufferReader.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Utils; + +namespace Microsoft.AspNet.Razor.Text +{ + public class TextBufferReader : LookaheadTextReader + { + private Stack _bookmarks = new Stack(); + private SourceLocationTracker _tracker = new SourceLocationTracker(); + + public TextBufferReader(ITextBuffer buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + InnerBuffer = buffer; + } + + internal ITextBuffer InnerBuffer { get; private set; } + + public override SourceLocation CurrentLocation + { + get { return _tracker.CurrentLocation; } + } + + public override int Peek() + { + return InnerBuffer.Peek(); + } + + public override int Read() + { + var read = InnerBuffer.Read(); + if (read != -1) + { + var nextChar = '\0'; + var next = Peek(); + if (next != -1) + { + nextChar = (char)next; + } + _tracker.UpdateLocation((char)read, nextChar); + } + return read; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + var disposable = InnerBuffer as IDisposable; + if (disposable != null) + { + disposable.Dispose(); + } + } + base.Dispose(disposing); + } + + public override IDisposable BeginLookahead() + { + var context = new BacktrackContext() { Location = CurrentLocation }; + _bookmarks.Push(context); + return new DisposableAction(() => + { + EndLookahead(context); + }); + } + + public override void CancelBacktrack() + { + if (_bookmarks.Count == 0) + { + throw new InvalidOperationException(RazorResources.CancelBacktrack_Must_Be_Called_Within_Lookahead); + } + _bookmarks.Pop(); + } + + private void EndLookahead(BacktrackContext context) + { + if (_bookmarks.Count > 0 && ReferenceEquals(_bookmarks.Peek(), context)) + { + // Backtrack wasn't cancelled, so pop it + _bookmarks.Pop(); + + // Set the new current location + _tracker.CurrentLocation = context.Location; + InnerBuffer.Position = context.Location.AbsoluteIndex; + } + } + + /// + /// Need a class for reference equality to support cancelling backtrack. + /// + private class BacktrackContext + { + public SourceLocation Location { get; set; } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/TextChange.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextChange.cs new file mode 100644 index 0000000000..ec432b6a4b --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextChange.cs @@ -0,0 +1,253 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Text +{ + public struct TextChange + { + private string _newText; + private string _oldText; + + /// + /// Constructor for changes where the position hasn't moved (primarily for tests) + /// + internal TextChange(int position, int oldLength, ITextBuffer oldBuffer, int newLength, ITextBuffer newBuffer) + : this(position, oldLength, oldBuffer, position, newLength, newBuffer) + { + } + + public TextChange( + int oldPosition, + int oldLength, + ITextBuffer oldBuffer, + int newPosition, + int newLength, + ITextBuffer newBuffer) + : this() + { + if (oldBuffer == null) + { + throw new ArgumentNullException(nameof(oldBuffer)); + } + + if (newBuffer == null) + { + throw new ArgumentNullException(nameof(newBuffer)); + } + + if (oldPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(oldPosition), CommonResources.FormatArgument_Must_Be_GreaterThanOrEqualTo(0)); + } + if (newPosition < 0) + { + throw new ArgumentOutOfRangeException(nameof(newPosition), CommonResources.FormatArgument_Must_Be_GreaterThanOrEqualTo(0)); + } + if (oldLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(oldLength), CommonResources.FormatArgument_Must_Be_GreaterThanOrEqualTo(0)); + } + if (newLength < 0) + { + throw new ArgumentOutOfRangeException(nameof(newLength), CommonResources.FormatArgument_Must_Be_GreaterThanOrEqualTo(0)); + } + + OldPosition = oldPosition; + NewPosition = newPosition; + OldLength = oldLength; + NewLength = newLength; + NewBuffer = newBuffer; + OldBuffer = oldBuffer; + } + + public int OldPosition { get; } + + public int NewPosition { get; } + + public int OldLength { get; } + + public int NewLength { get; } + + public ITextBuffer NewBuffer { get; } + + public ITextBuffer OldBuffer { get; } + + /// + /// Note: This property is not thread safe, and will move position on the textbuffer while being read. + /// https://aspnetwebstack.codeplex.com/workitem/1317, tracks making this immutable and improving the access + /// to ITextBuffer to be thread safe. + /// + public string OldText + { + get + { + if (_oldText == null && OldBuffer != null) + { + _oldText = GetText(OldBuffer, OldPosition, OldLength); + } + return _oldText; + } + } + + /// + /// Note: This property is not thread safe, and will move position on the textbuffer while being read. + /// https://aspnetwebstack.codeplex.com/workitem/1317, tracks making this immutable and improving the access + /// to ITextBuffer to be thread safe. + /// + public string NewText + { + get + { + if (_newText == null) + { + _newText = GetText(NewBuffer, NewPosition, NewLength); + } + return _newText; + } + } + + public bool IsInsert + { + get { return OldLength == 0 && NewLength > 0; } + } + + public bool IsDelete + { + get { return OldLength > 0 && NewLength == 0; } + } + + public bool IsReplace + { + get { return OldLength > 0 && NewLength > 0; } + } + + public override bool Equals(object obj) + { + if (!(obj is TextChange)) + { + return false; + } + + var change = (TextChange)obj; + return change.OldPosition == OldPosition && + change.NewPosition == NewPosition && + change.OldLength == OldLength && + change.NewLength == NewLength && + OldBuffer.Equals(change.OldBuffer) && + NewBuffer.Equals(change.NewBuffer); + } + + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(OldPosition); + hashCodeCombiner.Add(NewPosition); + hashCodeCombiner.Add(OldLength); + hashCodeCombiner.Add(NewLength); + hashCodeCombiner.Add(OldBuffer); + hashCodeCombiner.Add(NewBuffer); + + return hashCodeCombiner; + } + + public string ApplyChange(string content, int changeOffset) + { + var changeRelativePosition = OldPosition - changeOffset; + + Debug.Assert(changeRelativePosition >= 0); + return content.Remove(changeRelativePosition, OldLength) + .Insert(changeRelativePosition, NewText); + } + + /// + /// Applies the text change to the content of the span and returns the new content. + /// This method doesn't update the span content. + /// + public string ApplyChange(Span span) + { + return ApplyChange(span.Content, span.Start.AbsoluteIndex); + } + + public override string ToString() + { + return string.Format(CultureInfo.CurrentCulture, "({0}:{1}) \"{3}\" -> ({0}:{2}) \"{4}\"", OldPosition, OldLength, NewLength, OldText, NewText); + } + + /// + /// Removes a common prefix from the edit to turn IntelliSense replacements into insertions where possible + /// + /// A normalized text change + public TextChange Normalize() + { + if (OldBuffer != null && IsReplace && NewLength > OldLength && NewText.StartsWith(OldText, StringComparison.Ordinal) && NewPosition == OldPosition) + { + // Normalize the change into an insertion of the uncommon suffix (i.e. strip out the common prefix) + return new TextChange(oldPosition: OldPosition + OldLength, + oldLength: 0, + oldBuffer: OldBuffer, + newPosition: OldPosition + OldLength, + newLength: NewLength - OldLength, + newBuffer: NewBuffer); + } + return this; + } + + private static string GetText(ITextBuffer buffer, int position, int length) + { + // Optimization for the common case of one char inserts, in this case we don't even need to seek the buffer. + if (length == 0) + { + return string.Empty; + } + + var oldPosition = buffer.Position; + try + { + buffer.Position = position; + + // Optimization for the common case of one char inserts, in this case we seek the buffer. + if (length == 1) + { + return ((char)buffer.Read()).ToString(); + } + else + { + var builder = new StringBuilder(); + for (int i = 0; i < length; i++) + { + var c = (char)buffer.Read(); + builder.Append(c); + + // This check is probably not necessary, will revisit when fixing https://aspnetwebstack.codeplex.com/workitem/1317 + if (Char.IsHighSurrogate(c)) + { + builder.Append((char)buffer.Read()); + } + } + return builder.ToString(); + } + } + finally + { + buffer.Position = oldPosition; + } + } + + public static bool operator ==(TextChange left, TextChange right) + { + return left.Equals(right); + } + + public static bool operator !=(TextChange left, TextChange right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/TextChangeType.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextChangeType.cs new file mode 100644 index 0000000000..84ec14124f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextChangeType.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.AspNet.Razor.Text +{ + public enum TextChangeType + { + Insert, + Remove + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/TextDocumentReader.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextDocumentReader.cs new file mode 100644 index 0000000000..1ce682ce2c --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextDocumentReader.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; + +namespace Microsoft.AspNet.Razor.Text +{ + public class TextDocumentReader : TextReader, ITextDocument + { + public TextDocumentReader(ITextDocument source) + { + Document = source; + } + + internal ITextDocument Document { get; private set; } + + public SourceLocation Location + { + get { return Document.Location; } + } + + public int Length + { + get { return Document.Length; } + } + + public int Position + { + get { return Document.Position; } + set { Document.Position = value; } + } + + public override int Read() + { + return Document.Read(); + } + + public override int Peek() + { + return Document.Peek(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Text/TextExtensions.cs b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextExtensions.cs new file mode 100644 index 0000000000..5724df4751 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Text/TextExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Microsoft.AspNet.Razor.Text +{ + internal static class TextExtensions + { + public static void Seek(this ITextBuffer self, int characters) + { + self.Position += characters; + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "The consumer is expected to dispose this object")] + public static ITextDocument ToDocument(this ITextBuffer self) + { + var ret = self as ITextDocument; + if (ret == null) + { + ret = new SeekableTextReader(self); + } + return ret; + } + + public static LookaheadToken BeginLookahead(this ITextBuffer self) + { + var start = self.Position; + return new LookaheadToken(() => + { + self.Position = start; + }); + } + + public static string ReadToEnd(this ITextBuffer self) + { + var builder = new StringBuilder(); + int read; + while ((read = self.Read()) != -1) + { + builder.Append((char)read); + } + return builder.ToString(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpHelpers.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpHelpers.cs new file mode 100644 index 0000000000..44c891ca4e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpHelpers.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + public static class CSharpHelpers + { + // CSharp Spec §2.4.2 + public static bool IsIdentifierStart(char character) + { + return Char.IsLetter(character) || + character == '_' || + CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.LetterNumber; + } + + public static bool IsIdentifierPart(char character) + { + return Char.IsDigit(character) || + IsIdentifierStart(character) || + IsIdentifierPartByUnicodeCategory(character); + } + + public static bool IsRealLiteralSuffix(char character) + { + return character == 'F' || + character == 'f' || + character == 'D' || + character == 'd' || + character == 'M' || + character == 'm'; + } + + private static bool IsIdentifierPartByUnicodeCategory(char character) + { + var category = CharUnicodeInfo.GetUnicodeCategory(character); + + return category == UnicodeCategory.NonSpacingMark || // Mn + category == UnicodeCategory.SpacingCombiningMark || // Mc + category == UnicodeCategory.ConnectorPunctuation || // Pc + category == UnicodeCategory.Format; // Cf + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpKeywordDetector.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpKeywordDetector.cs new file mode 100644 index 0000000000..02b7fd8111 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpKeywordDetector.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + internal static class CSharpKeywordDetector + { + private static readonly Dictionary _keywords = new Dictionary(StringComparer.Ordinal) + { + { "await", CSharpKeyword.Await }, + { "abstract", CSharpKeyword.Abstract }, + { "byte", CSharpKeyword.Byte }, + { "class", CSharpKeyword.Class }, + { "delegate", CSharpKeyword.Delegate }, + { "event", CSharpKeyword.Event }, + { "fixed", CSharpKeyword.Fixed }, + { "if", CSharpKeyword.If }, + { "internal", CSharpKeyword.Internal }, + { "new", CSharpKeyword.New }, + { "override", CSharpKeyword.Override }, + { "readonly", CSharpKeyword.Readonly }, + { "short", CSharpKeyword.Short }, + { "struct", CSharpKeyword.Struct }, + { "try", CSharpKeyword.Try }, + { "unsafe", CSharpKeyword.Unsafe }, + { "volatile", CSharpKeyword.Volatile }, + { "as", CSharpKeyword.As }, + { "do", CSharpKeyword.Do }, + { "is", CSharpKeyword.Is }, + { "params", CSharpKeyword.Params }, + { "ref", CSharpKeyword.Ref }, + { "switch", CSharpKeyword.Switch }, + { "ushort", CSharpKeyword.Ushort }, + { "while", CSharpKeyword.While }, + { "case", CSharpKeyword.Case }, + { "const", CSharpKeyword.Const }, + { "explicit", CSharpKeyword.Explicit }, + { "float", CSharpKeyword.Float }, + { "null", CSharpKeyword.Null }, + { "sizeof", CSharpKeyword.Sizeof }, + { "typeof", CSharpKeyword.Typeof }, + { "implicit", CSharpKeyword.Implicit }, + { "private", CSharpKeyword.Private }, + { "this", CSharpKeyword.This }, + { "using", CSharpKeyword.Using }, + { "extern", CSharpKeyword.Extern }, + { "return", CSharpKeyword.Return }, + { "stackalloc", CSharpKeyword.Stackalloc }, + { "uint", CSharpKeyword.Uint }, + { "base", CSharpKeyword.Base }, + { "catch", CSharpKeyword.Catch }, + { "continue", CSharpKeyword.Continue }, + { "double", CSharpKeyword.Double }, + { "for", CSharpKeyword.For }, + { "in", CSharpKeyword.In }, + { "lock", CSharpKeyword.Lock }, + { "object", CSharpKeyword.Object }, + { "protected", CSharpKeyword.Protected }, + { "static", CSharpKeyword.Static }, + { "false", CSharpKeyword.False }, + { "public", CSharpKeyword.Public }, + { "sbyte", CSharpKeyword.Sbyte }, + { "throw", CSharpKeyword.Throw }, + { "virtual", CSharpKeyword.Virtual }, + { "decimal", CSharpKeyword.Decimal }, + { "else", CSharpKeyword.Else }, + { "operator", CSharpKeyword.Operator }, + { "string", CSharpKeyword.String }, + { "ulong", CSharpKeyword.Ulong }, + { "bool", CSharpKeyword.Bool }, + { "char", CSharpKeyword.Char }, + { "default", CSharpKeyword.Default }, + { "foreach", CSharpKeyword.Foreach }, + { "long", CSharpKeyword.Long }, + { "void", CSharpKeyword.Void }, + { "enum", CSharpKeyword.Enum }, + { "finally", CSharpKeyword.Finally }, + { "int", CSharpKeyword.Int }, + { "out", CSharpKeyword.Out }, + { "sealed", CSharpKeyword.Sealed }, + { "true", CSharpKeyword.True }, + { "goto", CSharpKeyword.Goto }, + { "unchecked", CSharpKeyword.Unchecked }, + { "interface", CSharpKeyword.Interface }, + { "break", CSharpKeyword.Break }, + { "checked", CSharpKeyword.Checked }, + { "namespace", CSharpKeyword.Namespace }, + { "when", CSharpKeyword.When } + }; + + public static CSharpKeyword? SymbolTypeForIdentifier(string id) + { + CSharpKeyword type; + if (!_keywords.TryGetValue(id, out type)) + { + return null; + } + return type; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpTokenizer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpTokenizer.cs new file mode 100644 index 0000000000..bfc191b5de --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/CSharpTokenizer.cs @@ -0,0 +1,449 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + public class CSharpTokenizer : Tokenizer + { + private Dictionary> _operatorHandlers; + + public CSharpTokenizer(ITextDocument source) + : base(source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + CurrentState = Data; + + _operatorHandlers = new Dictionary>() + { + { '-', MinusOperator }, + { '<', LessThanOperator }, + { '>', GreaterThanOperator }, + { '&', CreateTwoCharOperatorHandler(CSharpSymbolType.And, '=', CSharpSymbolType.AndAssign, '&', CSharpSymbolType.DoubleAnd) }, + { '|', CreateTwoCharOperatorHandler(CSharpSymbolType.Or, '=', CSharpSymbolType.OrAssign, '|', CSharpSymbolType.DoubleOr) }, + { '+', CreateTwoCharOperatorHandler(CSharpSymbolType.Plus, '=', CSharpSymbolType.PlusAssign, '+', CSharpSymbolType.Increment) }, + { '=', CreateTwoCharOperatorHandler(CSharpSymbolType.Assign, '=', CSharpSymbolType.Equals, '>', CSharpSymbolType.GreaterThanEqual) }, + { '!', CreateTwoCharOperatorHandler(CSharpSymbolType.Not, '=', CSharpSymbolType.NotEqual) }, + { '%', CreateTwoCharOperatorHandler(CSharpSymbolType.Modulo, '=', CSharpSymbolType.ModuloAssign) }, + { '*', CreateTwoCharOperatorHandler(CSharpSymbolType.Star, '=', CSharpSymbolType.MultiplyAssign) }, + { ':', CreateTwoCharOperatorHandler(CSharpSymbolType.Colon, ':', CSharpSymbolType.DoubleColon) }, + { '?', CreateTwoCharOperatorHandler(CSharpSymbolType.QuestionMark, '?', CSharpSymbolType.NullCoalesce) }, + { '^', CreateTwoCharOperatorHandler(CSharpSymbolType.Xor, '=', CSharpSymbolType.XorAssign) }, + { '(', () => CSharpSymbolType.LeftParenthesis }, + { ')', () => CSharpSymbolType.RightParenthesis }, + { '{', () => CSharpSymbolType.LeftBrace }, + { '}', () => CSharpSymbolType.RightBrace }, + { '[', () => CSharpSymbolType.LeftBracket }, + { ']', () => CSharpSymbolType.RightBracket }, + { ',', () => CSharpSymbolType.Comma }, + { ';', () => CSharpSymbolType.Semicolon }, + { '~', () => CSharpSymbolType.Tilde }, + { '#', () => CSharpSymbolType.Hash } + }; + } + + protected override State StartState + { + get { return Data; } + } + + public override CSharpSymbolType RazorCommentType + { + get { return CSharpSymbolType.RazorComment; } + } + + public override CSharpSymbolType RazorCommentTransitionType + { + get { return CSharpSymbolType.RazorCommentTransition; } + } + + public override CSharpSymbolType RazorCommentStarType + { + get { return CSharpSymbolType.RazorCommentStar; } + } + + protected override CSharpSymbol CreateSymbol(SourceLocation start, string content, CSharpSymbolType type, IEnumerable errors) + { + return new CSharpSymbol(start, content, type, errors); + } + + private StateResult Data() + { + if (ParserHelpers.IsNewLine(CurrentCharacter)) + { + // CSharp Spec §2.3.1 + var checkTwoCharNewline = CurrentCharacter == '\r'; + TakeCurrent(); + if (checkTwoCharNewline && CurrentCharacter == '\n') + { + TakeCurrent(); + } + return Stay(EndSymbol(CSharpSymbolType.NewLine)); + } + else if (ParserHelpers.IsWhitespace(CurrentCharacter)) + { + // CSharp Spec §2.3.3 + TakeUntil(c => !ParserHelpers.IsWhitespace(c)); + return Stay(EndSymbol(CSharpSymbolType.WhiteSpace)); + } + else if (CSharpHelpers.IsIdentifierStart(CurrentCharacter)) + { + return Identifier(); + } + else if (Char.IsDigit(CurrentCharacter)) + { + return NumericLiteral(); + } + switch (CurrentCharacter) + { + case '@': + return AtSymbol(); + case '\'': + TakeCurrent(); + return Transition(() => QuotedLiteral('\'', CSharpSymbolType.CharacterLiteral)); + case '"': + TakeCurrent(); + return Transition(() => QuotedLiteral('"', CSharpSymbolType.StringLiteral)); + case '.': + if (Char.IsDigit(Peek())) + { + return RealLiteral(); + } + return Stay(Single(CSharpSymbolType.Dot)); + case '/': + TakeCurrent(); + if (CurrentCharacter == '/') + { + TakeCurrent(); + return SingleLineComment(); + } + else if (CurrentCharacter == '*') + { + TakeCurrent(); + return Transition(BlockComment); + } + else if (CurrentCharacter == '=') + { + TakeCurrent(); + return Stay(EndSymbol(CSharpSymbolType.DivideAssign)); + } + else + { + return Stay(EndSymbol(CSharpSymbolType.Slash)); + } + default: + return Stay(EndSymbol(Operator())); + } + } + + private StateResult AtSymbol() + { + TakeCurrent(); + if (CurrentCharacter == '"') + { + TakeCurrent(); + return Transition(VerbatimStringLiteral); + } + else if (CurrentCharacter == '*') + { + return Transition(EndSymbol(CSharpSymbolType.RazorCommentTransition), AfterRazorCommentTransition); + } + else if (CurrentCharacter == '@') + { + // Could be escaped comment transition + return Transition(EndSymbol(CSharpSymbolType.Transition), () => + { + TakeCurrent(); + return Transition(EndSymbol(CSharpSymbolType.Transition), Data); + }); + } + return Stay(EndSymbol(CSharpSymbolType.Transition)); + } + + private CSharpSymbolType Operator() + { + var first = CurrentCharacter; + TakeCurrent(); + Func handler; + if (_operatorHandlers.TryGetValue(first, out handler)) + { + return handler(); + } + return CSharpSymbolType.Unknown; + } + + private CSharpSymbolType LessThanOperator() + { + if (CurrentCharacter == '=') + { + TakeCurrent(); + return CSharpSymbolType.LessThanEqual; + } + return CSharpSymbolType.LessThan; + } + + private CSharpSymbolType GreaterThanOperator() + { + if (CurrentCharacter == '=') + { + TakeCurrent(); + return CSharpSymbolType.GreaterThanEqual; + } + return CSharpSymbolType.GreaterThan; + } + + private CSharpSymbolType MinusOperator() + { + if (CurrentCharacter == '>') + { + TakeCurrent(); + return CSharpSymbolType.Arrow; + } + else if (CurrentCharacter == '-') + { + TakeCurrent(); + return CSharpSymbolType.Decrement; + } + else if (CurrentCharacter == '=') + { + TakeCurrent(); + return CSharpSymbolType.MinusAssign; + } + return CSharpSymbolType.Minus; + } + + private Func CreateTwoCharOperatorHandler(CSharpSymbolType typeIfOnlyFirst, char second, CSharpSymbolType typeIfBoth) + { + return () => + { + if (CurrentCharacter == second) + { + TakeCurrent(); + return typeIfBoth; + } + return typeIfOnlyFirst; + }; + } + + private Func CreateTwoCharOperatorHandler(CSharpSymbolType typeIfOnlyFirst, char option1, CSharpSymbolType typeIfOption1, char option2, CSharpSymbolType typeIfOption2) + { + return () => + { + if (CurrentCharacter == option1) + { + TakeCurrent(); + return typeIfOption1; + } + else if (CurrentCharacter == option2) + { + TakeCurrent(); + return typeIfOption2; + } + return typeIfOnlyFirst; + }; + } + + private StateResult VerbatimStringLiteral() + { + TakeUntil(c => c == '"'); + if (CurrentCharacter == '"') + { + TakeCurrent(); + if (CurrentCharacter == '"') + { + TakeCurrent(); + // Stay in the literal, this is an escaped " + return Stay(); + } + } + else if (EndOfFile) + { + CurrentErrors.Add( + new RazorError( + RazorResources.ParseError_Unterminated_String_Literal, + CurrentStart, + length: 1 /* end of file */)); + } + return Transition(EndSymbol(CSharpSymbolType.StringLiteral), Data); + } + + private StateResult QuotedLiteral(char quote, CSharpSymbolType literalType) + { + TakeUntil(c => c == '\\' || c == quote || ParserHelpers.IsNewLine(c)); + if (CurrentCharacter == '\\') + { + TakeCurrent(); // Take the '\' + + // If the next char is the same quote that started this + if (CurrentCharacter == quote || CurrentCharacter == '\\') + { + TakeCurrent(); // Take it so that we don't prematurely end the literal. + } + return Stay(); + } + else if (EndOfFile || ParserHelpers.IsNewLine(CurrentCharacter)) + { + CurrentErrors.Add( + new RazorError( + RazorResources.ParseError_Unterminated_String_Literal, + CurrentStart, + length: 1 /* " */)); + } + else + { + TakeCurrent(); // No-op if at EOF + } + return Transition(EndSymbol(literalType), Data); + } + + // CSharp Spec §2.3.2 + private StateResult BlockComment() + { + TakeUntil(c => c == '*'); + if (EndOfFile) + { + CurrentErrors.Add( + new RazorError( + RazorResources.ParseError_BlockComment_Not_Terminated, + CurrentStart, + length: 1 /* end of file */)); + return Transition(EndSymbol(CSharpSymbolType.Comment), Data); + } + if (CurrentCharacter == '*') + { + TakeCurrent(); + if (CurrentCharacter == '/') + { + TakeCurrent(); + return Transition(EndSymbol(CSharpSymbolType.Comment), Data); + } + } + return Stay(); + } + + // CSharp Spec §2.3.2 + private StateResult SingleLineComment() + { + TakeUntil(c => ParserHelpers.IsNewLine(c)); + return Stay(EndSymbol(CSharpSymbolType.Comment)); + } + + // CSharp Spec §2.4.4 + private StateResult NumericLiteral() + { + if (TakeAll("0x", caseSensitive: true)) + { + return HexLiteral(); + } + else + { + return DecimalLiteral(); + } + } + + private StateResult HexLiteral() + { + TakeUntil(c => !ParserHelpers.IsHexDigit(c)); + TakeIntegerSuffix(); + return Stay(EndSymbol(CSharpSymbolType.IntegerLiteral)); + } + + private StateResult DecimalLiteral() + { + TakeUntil(c => !Char.IsDigit(c)); + if (CurrentCharacter == '.' && Char.IsDigit(Peek())) + { + return RealLiteral(); + } + else if (CSharpHelpers.IsRealLiteralSuffix(CurrentCharacter) || + CurrentCharacter == 'E' || CurrentCharacter == 'e') + { + return RealLiteralExponentPart(); + } + else + { + TakeIntegerSuffix(); + return Stay(EndSymbol(CSharpSymbolType.IntegerLiteral)); + } + } + + private StateResult RealLiteralExponentPart() + { + if (CurrentCharacter == 'E' || CurrentCharacter == 'e') + { + TakeCurrent(); + if (CurrentCharacter == '+' || CurrentCharacter == '-') + { + TakeCurrent(); + } + TakeUntil(c => !Char.IsDigit(c)); + } + if (CSharpHelpers.IsRealLiteralSuffix(CurrentCharacter)) + { + TakeCurrent(); + } + return Stay(EndSymbol(CSharpSymbolType.RealLiteral)); + } + + // CSharp Spec §2.4.4.3 + private StateResult RealLiteral() + { + AssertCurrent('.'); + TakeCurrent(); + Debug.Assert(Char.IsDigit(CurrentCharacter)); + TakeUntil(c => !Char.IsDigit(c)); + return RealLiteralExponentPart(); + } + + private void TakeIntegerSuffix() + { + if (Char.ToLowerInvariant(CurrentCharacter) == 'u') + { + TakeCurrent(); + if (Char.ToLowerInvariant(CurrentCharacter) == 'l') + { + TakeCurrent(); + } + } + else if (Char.ToLowerInvariant(CurrentCharacter) == 'l') + { + TakeCurrent(); + if (Char.ToLowerInvariant(CurrentCharacter) == 'u') + { + TakeCurrent(); + } + } + } + + // CSharp Spec §2.4.2 + private StateResult Identifier() + { + Debug.Assert(CSharpHelpers.IsIdentifierStart(CurrentCharacter)); + TakeCurrent(); + TakeUntil(c => !CSharpHelpers.IsIdentifierPart(c)); + CSharpSymbol sym = null; + if (HaveContent) + { + var kwd = CSharpKeywordDetector.SymbolTypeForIdentifier(Buffer.ToString()); + var type = CSharpSymbolType.Identifier; + if (kwd != null) + { + type = CSharpSymbolType.Keyword; + } + sym = new CSharpSymbol(CurrentStart, Buffer.ToString(), type) { Keyword = kwd }; + } + StartSymbol(); + return Stay(sym); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/HtmlTokenizer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/HtmlTokenizer.cs new file mode 100644 index 0000000000..74a3d784df --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/HtmlTokenizer.cs @@ -0,0 +1,211 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + // Tokenizer _loosely_ based on http://dev.w3.org/html5/spec/Overview.html#tokenization + public class HtmlTokenizer : Tokenizer + { + private const char TransitionChar = '@'; + + public HtmlTokenizer(ITextDocument source) + : base(source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + CurrentState = Data; + } + + protected override State StartState + { + get { return Data; } + } + + public override HtmlSymbolType RazorCommentType + { + get { return HtmlSymbolType.RazorComment; } + } + + public override HtmlSymbolType RazorCommentTransitionType + { + get { return HtmlSymbolType.RazorCommentTransition; } + } + + public override HtmlSymbolType RazorCommentStarType + { + get { return HtmlSymbolType.RazorCommentStar; } + } + + internal static IEnumerable Tokenize(string content) + { + using (SeekableTextReader reader = new SeekableTextReader(content)) + { + var tok = new HtmlTokenizer(reader); + HtmlSymbol sym; + while ((sym = tok.NextSymbol()) != null) + { + yield return sym; + } + } + } + + protected override HtmlSymbol CreateSymbol(SourceLocation start, string content, HtmlSymbolType type, IEnumerable errors) + { + return new HtmlSymbol(start, content, type, errors); + } + + // http://dev.w3.org/html5/spec/Overview.html#data-state + private StateResult Data() + { + if (ParserHelpers.IsWhitespace(CurrentCharacter)) + { + return Stay(Whitespace()); + } + else if (ParserHelpers.IsNewLine(CurrentCharacter)) + { + return Stay(Newline()); + } + else if (CurrentCharacter == '@') + { + TakeCurrent(); + if (CurrentCharacter == '*') + { + return Transition(EndSymbol(HtmlSymbolType.RazorCommentTransition), AfterRazorCommentTransition); + } + else if (CurrentCharacter == '@') + { + // Could be escaped comment transition + return Transition(EndSymbol(HtmlSymbolType.Transition), () => + { + TakeCurrent(); + return Transition(EndSymbol(HtmlSymbolType.Transition), Data); + }); + } + return Stay(EndSymbol(HtmlSymbolType.Transition)); + } + else if (AtSymbol()) + { + return Stay(Symbol()); + } + else + { + return Transition(Text); + } + } + + private StateResult Text() + { + var prev = '\0'; + while (!EndOfFile && !ParserHelpers.IsWhitespaceOrNewLine(CurrentCharacter) && !AtSymbol()) + { + prev = CurrentCharacter; + TakeCurrent(); + } + + if (CurrentCharacter == '@') + { + var next = Peek(); + if (ParserHelpers.IsLetterOrDecimalDigit(prev) && ParserHelpers.IsLetterOrDecimalDigit(next)) + { + TakeCurrent(); // Take the "@" + return Stay(); // Stay in the Text state + } + } + + // Output the Text token and return to the Data state to tokenize the next character (if there is one) + return Transition(EndSymbol(HtmlSymbolType.Text), Data); + } + + private HtmlSymbol Symbol() + { + Debug.Assert(AtSymbol()); + var sym = CurrentCharacter; + TakeCurrent(); + switch (sym) + { + case '<': + return EndSymbol(HtmlSymbolType.OpenAngle); + case '!': + return EndSymbol(HtmlSymbolType.Bang); + case '/': + return EndSymbol(HtmlSymbolType.ForwardSlash); + case '?': + return EndSymbol(HtmlSymbolType.QuestionMark); + case '[': + return EndSymbol(HtmlSymbolType.LeftBracket); + case '>': + return EndSymbol(HtmlSymbolType.CloseAngle); + case ']': + return EndSymbol(HtmlSymbolType.RightBracket); + case '=': + return EndSymbol(HtmlSymbolType.Equals); + case '"': + return EndSymbol(HtmlSymbolType.DoubleQuote); + case '\'': + return EndSymbol(HtmlSymbolType.SingleQuote); + case '-': + Debug.Assert(CurrentCharacter == '-'); + TakeCurrent(); + return EndSymbol(HtmlSymbolType.DoubleHyphen); + default: +#if NET451 + // No Debug.Fail in CoreCLR + + Debug.Fail("Unexpected symbol!"); +#else + Debug.Assert(false, "Unexpected symbol"); +#endif + return EndSymbol(HtmlSymbolType.Unknown); + } + } + + private HtmlSymbol Whitespace() + { + while (ParserHelpers.IsWhitespace(CurrentCharacter)) + { + TakeCurrent(); + } + return EndSymbol(HtmlSymbolType.WhiteSpace); + } + + private HtmlSymbol Newline() + { + Debug.Assert(ParserHelpers.IsNewLine(CurrentCharacter)); + // CSharp Spec §2.3.1 + var checkTwoCharNewline = CurrentCharacter == '\r'; + TakeCurrent(); + if (checkTwoCharNewline && CurrentCharacter == '\n') + { + TakeCurrent(); + } + return EndSymbol(HtmlSymbolType.NewLine); + } + + private bool AtSymbol() + { + return CurrentCharacter == '<' || + CurrentCharacter == '<' || + CurrentCharacter == '!' || + CurrentCharacter == '/' || + CurrentCharacter == '?' || + CurrentCharacter == '[' || + CurrentCharacter == '>' || + CurrentCharacter == ']' || + CurrentCharacter == '=' || + CurrentCharacter == '"' || + CurrentCharacter == '\'' || + CurrentCharacter == '@' || + (CurrentCharacter == '-' && Peek() == '-'); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/ITokenizer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/ITokenizer.cs new file mode 100644 index 0000000000..00c6f10805 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/ITokenizer.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + public interface ITokenizer + { + ISymbol NextSymbol(); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpKeyword.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpKeyword.cs new file mode 100644 index 0000000000..e5a397ceaf --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpKeyword.cs @@ -0,0 +1,88 @@ +// 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.AspNet.Razor.Tokenizer.Symbols +{ + public enum CSharpKeyword + { + Await, + Abstract, + Byte, + Class, + Delegate, + Event, + Fixed, + If, + Internal, + New, + Override, + Readonly, + Short, + Struct, + Try, + Unsafe, + Volatile, + As, + Do, + Is, + Params, + Ref, + Switch, + Ushort, + While, + Case, + Const, + Explicit, + Float, + Null, + Sizeof, + Typeof, + Implicit, + Private, + This, + Using, + Extern, + Return, + Stackalloc, + Uint, + Base, + Catch, + Continue, + Double, + For, + In, + Lock, + Object, + Protected, + Static, + False, + Public, + Sbyte, + Throw, + Virtual, + Decimal, + Else, + Operator, + String, + Ulong, + Bool, + Char, + Default, + Foreach, + Long, + Void, + Enum, + Finally, + Int, + Out, + Sealed, + True, + Goto, + Unchecked, + Interface, + Break, + Checked, + Namespace, + When + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpSymbol.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpSymbol.cs new file mode 100644 index 0000000000..4cc9acd2c6 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpSymbol.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Razor.Tokenizer.Symbols +{ + public class CSharpSymbol : SymbolBase + { + // Helper constructor + public CSharpSymbol(int offset, int line, int column, string content, CSharpSymbolType type) + : this(new SourceLocation(offset, line, column), content, type, Enumerable.Empty()) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpSymbol(SourceLocation start, string content, CSharpSymbolType type) + : this(start, content, type, Enumerable.Empty()) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpSymbol( + int offset, + int line, + int column, + string content, + CSharpSymbolType type, + IEnumerable errors) + : base(new SourceLocation(offset, line, column), content, type, errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public CSharpSymbol( + SourceLocation start, + string content, + CSharpSymbolType type, + IEnumerable errors) + : base(start, content, type, errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public bool? EscapedIdentifier { get; set; } + public CSharpKeyword? Keyword { get; set; } + + public override bool Equals(object obj) + { + var other = obj as CSharpSymbol; + return base.Equals(other) && other.Keyword == Keyword; + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpSymbolType.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpSymbolType.cs new file mode 100644 index 0000000000..b221ccb195 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/CSharpSymbolType.cs @@ -0,0 +1,75 @@ +// 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.AspNet.Razor.Tokenizer.Symbols +{ + public enum CSharpSymbolType + { + Unknown, + Identifier, + Keyword, + IntegerLiteral, + NewLine, + WhiteSpace, + Comment, + RealLiteral, + CharacterLiteral, + StringLiteral, + + // Operators + Arrow, + Minus, + Decrement, + MinusAssign, + NotEqual, + Not, + Modulo, + ModuloAssign, + AndAssign, + And, + DoubleAnd, + LeftParenthesis, + RightParenthesis, + Star, + MultiplyAssign, + Comma, + Dot, + Slash, + DivideAssign, + DoubleColon, + Colon, + Semicolon, + QuestionMark, + NullCoalesce, + RightBracket, + LeftBracket, + XorAssign, + Xor, + LeftBrace, + OrAssign, + DoubleOr, + Or, + RightBrace, + Tilde, + Plus, + PlusAssign, + Increment, + LessThan, + LessThanEqual, + LeftShift, + LeftShiftAssign, + Assign, + Equals, + GreaterThan, + GreaterThanEqual, + RightShift, + RightShiftAssign, + Hash, + Transition, + + // Razor specific + RazorCommentTransition, + RazorCommentStar, + RazorComment + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/HtmlSymbol.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/HtmlSymbol.cs new file mode 100644 index 0000000000..e7b4c7e881 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/HtmlSymbol.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Razor.Tokenizer.Symbols +{ + public class HtmlSymbol : SymbolBase + { + // Helper constructor + public HtmlSymbol(int offset, int line, int column, string content, HtmlSymbolType type) + : this(new SourceLocation(offset, line, column), content, type, Enumerable.Empty()) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public HtmlSymbol(SourceLocation start, string content, HtmlSymbolType type) + : base(start, content, type, Enumerable.Empty()) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public HtmlSymbol( + int offset, + int line, + int column, + string content, + HtmlSymbolType type, + IEnumerable errors) + : base(new SourceLocation(offset, line, column), content, type, errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + + public HtmlSymbol( + SourceLocation start, + string content, + HtmlSymbolType type, + IEnumerable errors) + : base(start, content, type, errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/HtmlSymbolType.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/HtmlSymbolType.cs new file mode 100644 index 0000000000..27cbd7e590 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/HtmlSymbolType.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.Tokenizer.Symbols +{ + [Flags] + public enum HtmlSymbolType + { + Unknown, + Text, // Text which isn't one of the below + WhiteSpace, // Non-newline Whitespace + NewLine, // Newline + OpenAngle, // < + Bang, // ! + ForwardSlash, // / + QuestionMark, // ? + DoubleHyphen, // -- + LeftBracket, // [ + CloseAngle, // > + RightBracket, // ] + Equals, // = + DoubleQuote, // " + SingleQuote, // ' + Transition, // @ + Colon, + RazorComment, + RazorCommentStar, + RazorCommentTransition + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/ISymbol.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/ISymbol.cs new file mode 100644 index 0000000000..a2bd22041f --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/ISymbol.cs @@ -0,0 +1,14 @@ +// 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.AspNet.Razor.Tokenizer.Symbols +{ + public interface ISymbol + { + SourceLocation Start { get; } + string Content { get; } + + void OffsetStart(SourceLocation documentStart); + void ChangeStart(SourceLocation newStart); + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/KnownSymbolType.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/KnownSymbolType.cs new file mode 100644 index 0000000000..bbcdf842fd --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/KnownSymbolType.cs @@ -0,0 +1,18 @@ +// 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.AspNet.Razor.Tokenizer.Symbols +{ + public enum KnownSymbolType + { + WhiteSpace, + NewLine, + Identifier, + Keyword, + Transition, + Unknown, + CommentStart, + CommentStar, + CommentBody + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolBase.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolBase.cs new file mode 100644 index 0000000000..fca7684a2e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolBase.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNet.Razor.Tokenizer.Symbols +{ + public abstract class SymbolBase : ISymbol + where TType : struct + { + protected SymbolBase( + SourceLocation start, + string content, + TType type, + IEnumerable errors) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + Start = start; + Content = content; + Type = type; + Errors = errors; + } + + public SourceLocation Start { get; private set; } + + public string Content { get; } + + public IEnumerable Errors { get; } + + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is the most appropriate name for this property and conflicts are unlikely")] + public TType Type { get; } + + public override bool Equals(object obj) + { + SymbolBase other = obj as SymbolBase; + return other != null && + Start.Equals(other.Start) && + string.Equals(Content, other.Content, StringComparison.Ordinal) && + Type.Equals(other.Type); + } + + public override int GetHashCode() + { + // Hash code should include only immutable properties. + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Content, StringComparer.Ordinal); + hashCodeCombiner.Add(Type); + + return hashCodeCombiner; + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0} {1} - [{2}]", Start, Type, Content); + } + + public void OffsetStart(SourceLocation documentStart) + { + Start = documentStart + Start; + } + + public void ChangeStart(SourceLocation newStart) + { + Start = newStart; + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolExtensions.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolExtensions.cs new file mode 100644 index 0000000000..f8ccaf7826 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolExtensions.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Tokenizer.Symbols +{ + public static class SymbolExtensions + { + public static LocationTagged GetContent(this SpanBuilder span) + { + return GetContent(span, e => e); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended type for generic delegates and requires this level of nesting")] + public static LocationTagged GetContent(this SpanBuilder span, Func, IEnumerable> filter) + { + return GetContent(filter(span.Symbols), span.Start); + } + + public static LocationTagged GetContent(this IEnumerable symbols, SourceLocation spanStart) + { + if (symbols.Any()) + { + return new LocationTagged(string.Concat(symbols.Select(s => s.Content)), spanStart + symbols.First().Start); + } + else + { + return new LocationTagged(string.Empty, spanStart); + } + } + + public static LocationTagged GetContent(this ISymbol symbol) + { + return new LocationTagged(symbol.Content, symbol.Start); + } + + /// + /// Converts the generic to a and + /// finds the first with type . + /// + /// The instance this method extends. + /// The to search for. + /// The first of type . + public static HtmlSymbol FirstHtmlSymbolAs(this IEnumerable symbols, HtmlSymbolType type) + { + return symbols.OfType().FirstOrDefault(sym => (type & sym.Type) == sym.Type); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolTypeSuppressions.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolTypeSuppressions.cs new file mode 100644 index 0000000000..4cfba3241e --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Symbols/SymbolTypeSuppressions.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Foreach", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Foreach", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Readonly", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Readonly", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sbyte", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Sbyte", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sizeof", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Sizeof", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Stackalloc", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Stackalloc", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Typeof", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Typeof", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Uint", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Uint", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ulong", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Ulong", Justification = Justifications.SymbolTypeNames)] +[assembly: SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ushort", Scope = "member", Target = "Microsoft.AspNet.Razor.Tokenizer.Symbols.CSharpKeyword.#Ushort", Justification = Justifications.SymbolTypeNames)] + +internal static partial class Justifications +{ + internal const string SymbolTypeNames = "Symbol Type Names are spelled according to the language keyword or token they represent"; +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Tokenizer.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Tokenizer.cs new file mode 100644 index 0000000000..6d72895aaa --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/Tokenizer.cs @@ -0,0 +1,321 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +#if DEBUG +using System.Globalization; +#endif +using System.Linq; +using System.Text; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + public abstract partial class Tokenizer : StateMachine, ITokenizer + where TSymbolType : struct + where TSymbol : SymbolBase + { + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "TextDocumentReader does not require disposal")] + protected Tokenizer(ITextDocument source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + Source = new TextDocumentReader(source); + Buffer = new StringBuilder(); + CurrentErrors = new List(); + StartSymbol(); + } + + public TextDocumentReader Source { get; private set; } + + protected StringBuilder Buffer { get; private set; } + + protected bool EndOfFile + { + get { return Source.Peek() == -1; } + } + + protected IList CurrentErrors { get; private set; } + + public abstract TSymbolType RazorCommentStarType { get; } + public abstract TSymbolType RazorCommentType { get; } + public abstract TSymbolType RazorCommentTransitionType { get; } + + protected bool HaveContent + { + get { return Buffer.Length > 0; } + } + + protected char CurrentCharacter + { + get + { + var peek = Source.Peek(); + return peek == -1 ? '\0' : (char)peek; + } + } + + protected SourceLocation CurrentLocation + { + get { return Source.Location; } + } + + protected SourceLocation CurrentStart { get; private set; } + + public virtual TSymbol NextSymbol() + { + // Post-Condition: Buffer should be empty at the start of Next() + Debug.Assert(Buffer.Length == 0); + StartSymbol(); + + if (EndOfFile) + { + return null; + } + var sym = Turn(); + + // Post-Condition: Buffer should be empty at the end of Next() + Debug.Assert(Buffer.Length == 0); + + return sym; + } + + public void Reset() + { + CurrentState = StartState; + } + + protected abstract TSymbol CreateSymbol(SourceLocation start, string content, TSymbolType type, IEnumerable errors); + + protected TSymbol Single(TSymbolType type) + { + TakeCurrent(); + return EndSymbol(type); + } + + protected void StartSymbol() + { + Buffer.Clear(); + CurrentStart = CurrentLocation; + CurrentErrors.Clear(); + } + + protected TSymbol EndSymbol(TSymbolType type) + { + return EndSymbol(CurrentStart, type); + } + + protected TSymbol EndSymbol(SourceLocation start, TSymbolType type) + { + TSymbol sym = null; + if (HaveContent) + { + sym = CreateSymbol(start, Buffer.ToString(), type, CurrentErrors.ToArray()); + } + StartSymbol(); + return sym; + } + + protected bool TakeUntil(Func predicate) + { + // Take all the characters up to the end character + while (!EndOfFile && !predicate(CurrentCharacter)) + { + TakeCurrent(); + } + + // Why did we end? + return !EndOfFile; + } + + protected void TakeCurrent() + { + if (EndOfFile) + { + return; + } // No-op + Buffer.Append(CurrentCharacter); + MoveNext(); + } + + protected void MoveNext() + { +#if DEBUG + _read.Append(CurrentCharacter); +#endif + Source.Read(); + } + + protected bool TakeAll(string expected, bool caseSensitive) + { + return Lookahead(expected, takeIfMatch: true, caseSensitive: caseSensitive); + } + + protected char Peek() + { + using (LookaheadToken lookahead = Source.BeginLookahead()) + { + MoveNext(); + return CurrentCharacter; + } + } + + protected StateResult AfterRazorCommentTransition() + { + if (CurrentCharacter != '*') + { + // We've been moved since last time we were asked for a symbol... reset the state + return Transition(StartState); + } + AssertCurrent('*'); + TakeCurrent(); + return Transition(EndSymbol(RazorCommentStarType), RazorCommentBody); + } + + protected StateResult RazorCommentBody() + { + TakeUntil(c => c == '*'); + if (CurrentCharacter == '*') + { + var star = CurrentCharacter; + var start = CurrentLocation; + MoveNext(); + if (!EndOfFile && CurrentCharacter == '@') + { + State next = () => + { + Buffer.Append(star); + return Transition(EndSymbol(start, RazorCommentStarType), () => + { + if (CurrentCharacter != '@') + { + // We've been moved since last time we were asked for a symbol... reset the state + return Transition(StartState); + } + TakeCurrent(); + return Transition(EndSymbol(RazorCommentTransitionType), StartState); + }); + }; + + if (HaveContent) + { + return Transition(EndSymbol(RazorCommentType), next); + } + else + { + return Transition(next); + } + } + else + { + Buffer.Append(star); + return Stay(); + } + } + return Transition(EndSymbol(RazorCommentType), StartState); + } + + /// + /// Internal for unit testing + /// + internal bool Lookahead(string expected, bool takeIfMatch, bool caseSensitive) + { + Func filter = c => c; + if (!caseSensitive) + { + filter = Char.ToLowerInvariant; + } + + if (expected.Length == 0 || filter(CurrentCharacter) != filter(expected[0])) + { + return false; + } + + // Capture the current buffer content in case we have to backtrack + string oldBuffer = null; + if (takeIfMatch) + { + oldBuffer = Buffer.ToString(); + } + + using (LookaheadToken lookahead = Source.BeginLookahead()) + { + for (int i = 0; i < expected.Length; i++) + { + if (filter(CurrentCharacter) != filter(expected[i])) + { + if (takeIfMatch) + { + // Clear the buffer and put the old buffer text back + Buffer.Clear(); + Buffer.Append(oldBuffer); + } + // Return without accepting lookahead (thus rejecting it) + return false; + } + if (takeIfMatch) + { + TakeCurrent(); + } + else + { + MoveNext(); + } + } + if (takeIfMatch) + { + lookahead.Accept(); + } + } + return true; + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")] + [Conditional("DEBUG")] + internal void AssertCurrent(char current) + { +#if NET451 + // No Debug.Assert with this many arguments in CoreCLR + + Debug.Assert(CurrentCharacter == current, "CurrentCharacter Assumption violated", "Assumed that the current character would be {0}, but it is actually {1}", current, CurrentCharacter); +#else + Debug.Assert(CurrentCharacter == current, string.Format("CurrentCharacter Assumption violated. Assumed that the current character would be {0}, but it is actually {1}", current, CurrentCharacter)); +#endif + } + + ISymbol ITokenizer.NextSymbol() + { + return (ISymbol)NextSymbol(); + } + } + +#if DEBUG + [DebuggerDisplay("{DebugDisplay}")] + public partial class Tokenizer + { + private StringBuilder _read = new StringBuilder(); + + public string DebugDisplay + { + get { return string.Format(CultureInfo.InvariantCulture, "[{0}] [{1}] [{2}]", _read.ToString(), CurrentCharacter, Remaining); } + } + + public string Remaining + { + get + { + var remaining = Source.ReadToEnd(); + Source.Seek(-remaining.Length); + return remaining; + } + } + } +#endif +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/TokenizerView.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/TokenizerView.cs new file mode 100644 index 0000000000..8c4df8bca8 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/TokenizerView.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Tokenizer +{ + [SuppressMessage("Microsoft.Design", "CA1005:AvoidExcessiveParametersOnGenericTypes", Justification = "All generic parameters are required")] + public class TokenizerView + where TSymbolType : struct + where TTokenizer : Tokenizer + where TSymbol : SymbolBase + { + public TokenizerView(TTokenizer tokenizer) + { + Tokenizer = tokenizer; + } + + public TTokenizer Tokenizer { get; private set; } + public bool EndOfFile { get; private set; } + public TSymbol Current { get; private set; } + + public ITextDocument Source + { + get { return Tokenizer.Source; } + } + + public bool Next() + { + Current = Tokenizer.NextSymbol(); + EndOfFile = (Current == null); + return !EndOfFile; + } + + public void PutBack(TSymbol symbol) + { + Debug.Assert(Source.Position == symbol.Start.AbsoluteIndex + symbol.Content.Length); + if (Source.Position != symbol.Start.AbsoluteIndex + symbol.Content.Length) + { + // We've already passed this symbol + throw new InvalidOperationException( + RazorResources.FormatTokenizerView_CannotPutBack( + symbol.Start.AbsoluteIndex + symbol.Content.Length, + Source.Position)); + } + Source.Position -= symbol.Content.Length; + Current = null; + EndOfFile = Source.Position >= Source.Length; + Tokenizer.Reset(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/XmlHelpers.cs b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/XmlHelpers.cs new file mode 100644 index 0000000000..ae60235628 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Tokenizer/XmlHelpers.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Tokenizer +{ + internal static class XmlHelpers + { + public static bool IsXmlNameStartChar(char chr) + { + // [4] NameStartChar ::= ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | + // [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | + // [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] + // http://www.w3.org/TR/REC-xml/#NT-Name + + return Char.IsLetter(chr) || + chr == ':' || + chr == '_' || + IsInRange(chr, 0xC0, 0xD6) || + IsInRange(chr, 0xD8, 0xF6) || + IsInRange(chr, 0xF8, 0x2FF) || + IsInRange(chr, 0x370, 0x37D) || + IsInRange(chr, 0x37F, 0x1FFF) || + IsInRange(chr, 0x200C, 0x200D) || + IsInRange(chr, 0x2070, 0x218F) || + IsInRange(chr, 0x2C00, 0x2FEF) || + IsInRange(chr, 0x3001, 0xD7FF) || + IsInRange(chr, 0xF900, 0xFDCF) || + IsInRange(chr, 0xFDF0, 0xFFFD) || + IsInRange(chr, 0x10000, 0xEFFFF); + } + + public static bool IsXmlNameChar(char chr) + { + // [4a] NameChar ::= NameStartChar | "-" | "." | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040] + // http://www.w3.org/TR/REC-xml/#NT-Name + return Char.IsDigit(chr) || + IsXmlNameStartChar(chr) || + chr == '-' || + chr == '.' || + chr == '·' || // (U+00B7 is middle dot: ·) + IsInRange(chr, 0x0300, 0x036F) || + IsInRange(chr, 0x203F, 0x2040); + } + + public static bool IsInRange(char chr, int low, int high) + { + return ((int)chr >= low) && ((int)chr <= high); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Utils/CharUtils.cs b/src/Microsoft.AspNet.Razor.VSRC1/Utils/CharUtils.cs new file mode 100644 index 0000000000..2d4094ccc0 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Utils/CharUtils.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Utils +{ + internal static class CharUtils + { + internal static bool IsNonNewLineWhitespace(char c) + { + return Char.IsWhiteSpace(c) && !IsNewLine(c); + } + + internal static bool IsNewLine(char c) + { + return c == 0x000d // Carriage return + || c == 0x000a // Linefeed + || c == 0x2028 // Line separator + || c == 0x2029; // Paragraph separator + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Utils/DisposableAction.cs b/src/Microsoft.AspNet.Razor.VSRC1/Utils/DisposableAction.cs new file mode 100644 index 0000000000..8ff2f09179 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Utils/DisposableAction.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Razor.Utils +{ + internal class DisposableAction : IDisposable + { + private Action _action; + private bool _invoked; + + public DisposableAction(Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + _action = action; + } + + public void Dispose() + { + if (!_invoked) + { + _action(); + _invoked = true; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Utils/EnumUtil.cs b/src/Microsoft.AspNet.Razor.VSRC1/Utils/EnumUtil.cs new file mode 100644 index 0000000000..7f0474a315 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Utils/EnumUtil.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNet.Razor.Utils +{ + internal static class EnumUtil + { + public static IEnumerable Single(T item) + { + yield return item; + } + + public static IEnumerable Prepend(T item, IEnumerable enumerable) + { + yield return item; + foreach (T t in enumerable) + { + yield return t; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/Utils/EnumeratorExtensions.cs b/src/Microsoft.AspNet.Razor.VSRC1/Utils/EnumeratorExtensions.cs new file mode 100644 index 0000000000..ad0911da50 --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/Utils/EnumeratorExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.AspNet.Razor.Utils +{ + internal static class EnumeratorExtensions + { + public static IEnumerable Flatten(this IEnumerable> source) + { + return source.SelectMany(e => e); + } + } +} diff --git a/src/Microsoft.AspNet.Razor.VSRC1/project.json b/src/Microsoft.AspNet.Razor.VSRC1/project.json new file mode 100644 index 0000000000..b18801b08a --- /dev/null +++ b/src/Microsoft.AspNet.Razor.VSRC1/project.json @@ -0,0 +1,16 @@ +{ + "description": "Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor parser and code generation infrastructure.", + "version": "4.0.0-rc1-final", + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "repository": { + "type": "git", + "url": "git://github.com/aspnet/razor" + }, + "dependencies": { }, + "frameworks": { + "net451": {} + } +} \ No newline at end of file