diff --git a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs index 33d8f46ae5..2ff49efa5c 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/Properties/Resources.Designer.cs @@ -138,6 +138,38 @@ namespace Microsoft.AspNet.Razor.Runtime return string.Format(CultureInfo.CurrentCulture, GetString("HtmlElementNameAttribute_InvalidElementName"), p0, p1); } + /// + /// 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); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx index 7469e1a225..48f5cb1fdd 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/Resources.resx +++ b/src/Microsoft.AspNet.Razor.Runtime/Resources.resx @@ -141,4 +141,10 @@ Tag helpers cannot target element name '{0}' because it contains a '{1}' 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}'. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs index 2602a5a8d5..5c956dc409 100644 --- a/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs +++ b/src/Microsoft.AspNet.Razor.Runtime/TagHelpers/TagHelperDescriptorResolver.cs @@ -23,7 +23,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { { TagHelperDirectiveType.AddTagHelper, SyntaxConstants.CSharp.AddTagHelperKeyword }, { TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword }, + { TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword }, }; + private static readonly HashSet InvalidNonWhitespacePrefixCharacters = + new HashSet(new[] { '@', '!', '<', '!', '/', '?', '[', '>', ']', '=', '"', '\'' }); private readonly TagHelperTypeResolver _typeResolver; @@ -51,7 +54,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers { var resolvedDescriptors = new HashSet(TagHelperDescriptorComparer.Default); - foreach (var directiveDescriptor in context.DirectiveDescriptors) + // 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 { @@ -69,9 +77,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers } else if (directiveDescriptor.DirectiveType == TagHelperDirectiveType.AddTagHelper) { - var descriptors = ResolveDescriptorsInAssembly(lookupInfo.AssemblyName, - directiveDescriptor.Location, - context.ErrorSink); + var descriptors = ResolveDescriptorsInAssembly( + lookupInfo.AssemblyName, + directiveDescriptor.Location, + context.ErrorSink); // Only use descriptors that match our lookup info descriptors = descriptors.Where(descriptor => MatchesLookupInfo(descriptor, lookupInfo)); @@ -89,12 +98,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers directiveDescriptor.Location, Resources.FormatTagHelperDescriptorResolver_EncounteredUnexpectedError( "@" + directiveName, - directiveDescriptor.LookupText, + directiveDescriptor.DirectiveText, ex.Message)); } } - return resolvedDescriptors; + var prefixedDescriptors = PrefixDescriptors(context, resolvedDescriptors); + + return prefixedDescriptors; } /// @@ -110,9 +121,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers /// 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, - ParserErrorSink errorSink) + protected virtual IEnumerable ResolveDescriptorsInAssembly( + string assemblyName, + SourceLocation documentLocation, + ParserErrorSink errorSink) { // Resolve valid tag helper types from the assembly. var tagHelperTypes = _typeResolver.Resolve(assemblyName, documentLocation, errorSink); @@ -124,6 +136,84 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers return descriptors; } + private static IEnumerable PrefixDescriptors( + TagHelperDescriptorResolutionContext context, + IEnumerable descriptors) + { + var tagHelperPrefix = ResolveTagHelperPrefix(context); + + if (!string.IsNullOrEmpty(tagHelperPrefix)) + { + return descriptors.Select(descriptor => + new TagHelperDescriptor( + tagHelperPrefix, + descriptor.TagName, + descriptor.TypeName, + descriptor.AssemblyName, + descriptor.Attributes)); + } + + 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)); + } + } + + 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, + ParserErrorSink errorSink) + { + foreach (var character in prefix) + { + // Prefixes are correlated with tag names, tag names cannot have whitespace. + if (char.IsWhiteSpace(character) || + InvalidNonWhitespacePrefixCharacters.Contains(character)) + { + errorSink.OnError( + directiveLocation, + Resources.FormatTagHelperDescriptorResolver_InvalidTagHelperPrefixValue( + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + character, + prefix)); + + return false; + } + } + + return true; + } + private static bool MatchesLookupInfo(TagHelperDescriptor descriptor, LookupInfo lookupInfo) { if (!string.Equals(descriptor.AssemblyName, lookupInfo.AssemblyName, StringComparison.Ordinal)) @@ -146,7 +236,7 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers private static LookupInfo GetLookupInfo(TagHelperDirectiveDescriptor directiveDescriptor, ParserErrorSink errorSink) { - var lookupText = directiveDescriptor.LookupText; + var lookupText = directiveDescriptor.DirectiveText; var lookupStrings = lookupText?.Split(new[] { ',' }); // Ensure that we have valid lookupStrings to work with. Valid formats are: diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpDesignTimeHelpersVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpDesignTimeHelpersVisitor.cs index 7081785246..f9038ff277 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpDesignTimeHelpersVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CSharp/Visitors/CSharpDesignTimeHelpersVisitor.cs @@ -58,17 +58,22 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp } } + protected override void Visit(TagHelperPrefixDirectiveChunk chunk) + { + VisitTagHelperDirectiveChunk(chunk.Prefix, chunk); + } + protected override void Visit(AddTagHelperChunk chunk) { - VisitAddOrRemoveTagHelperChunk(chunk.LookupText, chunk); + VisitTagHelperDirectiveChunk(chunk.LookupText, chunk); } protected override void Visit(RemoveTagHelperChunk chunk) { - VisitAddOrRemoveTagHelperChunk(chunk.LookupText, chunk); + VisitTagHelperDirectiveChunk(chunk.LookupText, chunk); } - private void VisitAddOrRemoveTagHelperChunk(string lookupText, Chunk 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); @@ -81,12 +86,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp Writer.WriteStartAssignment(TagHelperDirectiveSyntaxHelper); - // The parsing mechanism for the add/remove TagHelper chunk (CSharpCodeParser.TagHelperDirective()) - // removes quotes that surround the lookupText. + // The parsing mechanism for a TagHelper directive chunk (CSharpCodeParser.TagHelperDirective()) + // removes quotes that surround the text. _csharpCodeVisitor.CreateExpressionCodeMapping( - string.Format( - CultureInfo.InvariantCulture, - "\"{0}\"", lookupText), + string.Format(CultureInfo.InvariantCulture, "\"{0}\"", text), chunk); Writer.WriteLine(";"); diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs index 9a901d41e0..f8ba39b70b 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/ChunkVisitor.cs @@ -57,6 +57,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler { Visit((TagHelperChunk)chunk); } + else if (chunk is TagHelperPrefixDirectiveChunk) + { + Visit((TagHelperPrefixDirectiveChunk)chunk); + } else if (chunk is AddTagHelperChunk) { Visit((AddTagHelperChunk)chunk); @@ -119,6 +123,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler 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); diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs index 955e4069c6..1227d96a4e 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeBuilder/CodeVisitor.cs @@ -33,6 +33,9 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler protected override void Visit(TagHelperChunk chunk) { } + protected override void Visit(TagHelperPrefixDirectiveChunk chunk) + { + } protected override void Visit(AddTagHelperChunk chunk) { } diff --git a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperPrefixDirectiveChunk.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperPrefixDirectiveChunk.cs new file mode 100644 index 0000000000..bedc080d57 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/Chunks/TagHelpers/TagHelperPrefixDirectiveChunk.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Generator.Compiler +{ + /// + /// 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/Generator/Compiler/CodeTree/CodeTreeBuilder.cs b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs index 35a854a00a..148337eea3 100644 --- a/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs +++ b/src/Microsoft.AspNet.Razor/Generator/Compiler/CodeTree/CodeTreeBuilder.cs @@ -37,6 +37,17 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler } } + 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 diff --git a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs index a25b393964..b0277a129e 100644 --- a/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs +++ b/src/Microsoft.AspNet.Razor/Generator/TagHelperCodeGenerator.cs @@ -75,9 +75,11 @@ namespace Microsoft.AspNet.Razor.Generator codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder(); } + var unprefixedTagName = tagHelperBlock.TagName.Substring(_tagHelperDescriptors.First().Prefix.Length); + context.CodeTreeBuilder.StartChunkBlock( new TagHelperChunk( - tagHelperBlock.TagName, + unprefixedTagName, tagHelperBlock.SelfClosing, attributes, _tagHelperDescriptors), diff --git a/src/Microsoft.AspNet.Razor/Generator/TagHelpers/TagHelperPrefixDirectiveCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TagHelpers/TagHelperPrefixDirectiveCodeGenerator.cs new file mode 100644 index 0000000000..7acd267395 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/TagHelpers/TagHelperPrefixDirectiveCodeGenerator.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Generator +{ + /// + /// A responsible for generating + /// s. + /// + public class TagHelperPrefixDirectiveCodeGenerator : SpanCodeGenerator + { + /// + /// Instantiates a new . + /// + /// + /// Text used as a required prefix when matching HTML. + /// + public TagHelperPrefixDirectiveCodeGenerator(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 code generation process. + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + context.CodeTreeBuilder.AddTagHelperPrefixDirectiveChunk(Prefix, target); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs index 8ba70aee02..21206c0496 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.Parser { private void SetupDirectives() { + MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword); MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword); MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword); MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword); @@ -27,14 +28,27 @@ namespace Microsoft.AspNet.Razor.Parser MapDirectives(LayoutDirective, SyntaxConstants.CSharp.LayoutKeyword); } + protected virtual void TagHelperPrefixDirective() + { + TagHelperDirective( + SyntaxConstants.CSharp.TagHelperPrefixKeyword, + prefix => new TagHelperPrefixDirectiveCodeGenerator(prefix)); + } + protected virtual void AddTagHelperDirective() { - TagHelperDirective(SyntaxConstants.CSharp.AddTagHelperKeyword, removeTagHelperDescriptors: false); + TagHelperDirective( + SyntaxConstants.CSharp.AddTagHelperKeyword, + lookupText => + new AddOrRemoveTagHelperCodeGenerator(removeTagHelperDescriptors: false, lookupText: lookupText)); } protected virtual void RemoveTagHelperDirective() { - TagHelperDirective(SyntaxConstants.CSharp.RemoveTagHelperKeyword, removeTagHelperDescriptors: true); + TagHelperDirective( + SyntaxConstants.CSharp.RemoveTagHelperKeyword, + lookupText => + new AddOrRemoveTagHelperCodeGenerator(removeTagHelperDescriptors: true, lookupText: lookupText)); } protected virtual void LayoutDirective() @@ -450,7 +464,7 @@ namespace Microsoft.AspNet.Razor.Parser Output(SpanKind.Code); } - private void TagHelperDirective(string keyword, bool removeTagHelperDescriptors) + private void TagHelperDirective(string keyword, Func buildCodeGenerator) { AssertDirective(keyword); @@ -492,8 +506,7 @@ namespace Microsoft.AspNet.Razor.Parser // 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.CodeGenerator = - new AddOrRemoveTagHelperCodeGenerator(removeTagHelperDescriptors, rawValue.Trim('"')); + Span.CodeGenerator = buildCodeGenerator(rawValue.Trim('"')); } // We expect the directive to be surrounded in quotes. diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs index 6906293549..bba1a82dd0 100644 --- a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs @@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Razor.Parser internal static ISet DefaultKeywords = new HashSet() { + SyntaxConstants.CSharp.TagHelperPrefixKeyword, SyntaxConstants.CSharp.AddTagHelperKeyword, SyntaxConstants.CSharp.RemoveTagHelperKeyword, "if", diff --git a/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs b/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs index 2d538e347c..24b4401214 100644 --- a/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs +++ b/src/Microsoft.AspNet.Razor/Parser/RazorParser.cs @@ -213,7 +213,7 @@ namespace Microsoft.AspNet.Razor.Parser [NotNull] ParserErrorSink errorSink) { var addOrRemoveTagHelperSpanVisitor = - new AddOrRemoveTagHelperSpanVisitor(TagHelperDescriptorResolver, errorSink); + new TagHelperDirectiveSpanVisitor(TagHelperDescriptorResolver, errorSink); return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot); } diff --git a/src/Microsoft.AspNet.Razor/Parser/SyntaxConstants.cs b/src/Microsoft.AspNet.Razor/Parser/SyntaxConstants.cs index 0485ca3008..7f5ea353a6 100644 --- a/src/Microsoft.AspNet.Razor/Parser/SyntaxConstants.cs +++ b/src/Microsoft.AspNet.Razor/Parser/SyntaxConstants.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.Parser public static class CSharp { public static readonly int UsingKeywordLength = 5; + public static readonly string TagHelperPrefixKeyword = "tagHelperPrefix"; public static readonly string AddTagHelperKeyword = "addTagHelper"; public static readonly string RemoveTagHelperKeyword = "removeTagHelper"; public static readonly string InheritsKeyword = "inherits"; diff --git a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/AddOrRemoveTagHelperSpanVisitor.cs b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperDirectiveSpanVisitor.cs similarity index 69% rename from src/Microsoft.AspNet.Razor/Parser/TagHelpers/AddOrRemoveTagHelperSpanVisitor.cs rename to src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperDirectiveSpanVisitor.cs index aabbc3d895..0f1c9b598e 100644 --- a/src/Microsoft.AspNet.Razor/Parser/TagHelpers/AddOrRemoveTagHelperSpanVisitor.cs +++ b/src/Microsoft.AspNet.Razor/Parser/TagHelpers/TagHelperDirectiveSpanVisitor.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers /// A that generates s from /// tag helper code generators in a Razor document. /// - public class AddOrRemoveTagHelperSpanVisitor : ParserVisitor + public class TagHelperDirectiveSpanVisitor : ParserVisitor { private readonly ITagHelperDescriptorResolver _descriptorResolver; private readonly ParserErrorSink _errorSink; @@ -20,12 +20,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers private List _directiveDescriptors; // Internal for testing use - internal AddOrRemoveTagHelperSpanVisitor(ITagHelperDescriptorResolver descriptorResolver) + internal TagHelperDirectiveSpanVisitor(ITagHelperDescriptorResolver descriptorResolver) : this(descriptorResolver, new ParserErrorSink()) { } - public AddOrRemoveTagHelperSpanVisitor([NotNull] ITagHelperDescriptorResolver descriptorResolver, + public TagHelperDirectiveSpanVisitor([NotNull] ITagHelperDescriptorResolver descriptorResolver, [NotNull] ParserErrorSink errorSink) { _descriptorResolver = descriptorResolver; @@ -61,13 +61,26 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers { var codeGenerator = (AddOrRemoveTagHelperCodeGenerator)span.CodeGenerator; - var directive = codeGenerator.RemoveTagHelperDescriptors ? - TagHelperDirectiveType.RemoveTagHelper : - TagHelperDirectiveType.AddTagHelper; + var directive = + codeGenerator.RemoveTagHelperDescriptors ? + TagHelperDirectiveType.RemoveTagHelper : + TagHelperDirectiveType.AddTagHelper; - var directiveDescriptor = new TagHelperDirectiveDescriptor(codeGenerator.LookupText, - span.Start, - directive); + var directiveDescriptor = new TagHelperDirectiveDescriptor( + codeGenerator.LookupText, + span.Start, + directive); + + _directiveDescriptors.Add(directiveDescriptor); + } + else if (span.CodeGenerator is TagHelperPrefixDirectiveCodeGenerator) + { + var codeGenerator = (TagHelperPrefixDirectiveCodeGenerator)span.CodeGenerator; + + var directiveDescriptor = new TagHelperDirectiveDescriptor( + codeGenerator.Prefix, + span.Start, + TagHelperDirectiveType.TagHelperPrefix); _directiveDescriptors.Add(directiveDescriptor); } diff --git a/src/Microsoft.AspNet.Razor/ParserResults.cs b/src/Microsoft.AspNet.Razor/ParserResults.cs index 9d4b9709b0..1eddefcd0b 100644 --- a/src/Microsoft.AspNet.Razor/ParserResults.cs +++ b/src/Microsoft.AspNet.Razor/ParserResults.cs @@ -54,6 +54,7 @@ namespace Microsoft.AspNet.Razor TagHelperDescriptors = tagHelperDescriptors; ErrorSink = errorSink; ParserErrors = errorSink.Errors; + Prefix = tagHelperDescriptors.FirstOrDefault()?.Prefix; } /// @@ -81,5 +82,10 @@ namespace Microsoft.AspNet.Razor /// The s found for the current Razor document. /// public IEnumerable TagHelperDescriptors { get; } + + /// + /// Text used as a required prefix when matching HTML. + /// + public string Prefix { get; } } } diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs index 77c080054f..ce9133081c 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptor.cs @@ -17,10 +17,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers internal TagHelperDescriptor([NotNull] string tagName, [NotNull] string typeName, [NotNull] string assemblyName) - : this(tagName, - typeName, - assemblyName, - Enumerable.Empty()) + : this( + tagName, + typeName, + assemblyName, + attributes: Enumerable.Empty()) { } @@ -35,22 +36,67 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// The s to request from the HTML tag. /// - public TagHelperDescriptor([NotNull] string tagName, - [NotNull] string typeName, - [NotNull] string assemblyName, - [NotNull] IEnumerable attributes) + public TagHelperDescriptor( + [NotNull] string tagName, + [NotNull] string typeName, + [NotNull] string assemblyName, + [NotNull] IEnumerable attributes) + : this( + prefix: string.Empty, + tagName: tagName, + typeName: typeName, + assemblyName: assemblyName, + attributes: attributes) { + } + + /// + /// Instantiates a new instance of the class with the given + /// . + /// + /// + /// Text used as a required prefix when matching HTML start and end tags in the Razor source to available + /// tag helpers. + /// + /// The tag name that the tag helper targets. '*' indicates a catch-all + /// which applies to every HTML tag. + /// The full name of the tag helper class. + /// The name of the assembly containing the tag helper class. + /// + /// The s to request from the HTML tag. + /// + public TagHelperDescriptor( + string prefix, + [NotNull] string tagName, + [NotNull] string typeName, + [NotNull] string assemblyName, + [NotNull] IEnumerable attributes) + { + Prefix = prefix ?? string.Empty; TagName = tagName; + FullTagName = Prefix + TagName; TypeName = typeName; AssemblyName = assemblyName; Attributes = new List(attributes); } + /// + /// 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; private set; } + /// /// The tag name that the tag helper should target. /// public string TagName { get; private set; } + /// + /// 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; private set; } + /// /// The full name of the tag helper class. /// diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs index 0f79c05154..92eb3742f0 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorComparer.cs @@ -29,12 +29,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// false otherwise. /// /// Determines equality based on , - /// and . + /// , and + /// . /// public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) { return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) && string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) && + string.Equals(descriptorX.Prefix, descriptorY.Prefix, StringComparison.OrdinalIgnoreCase) && string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal); } diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs index dc4a6123bb..988998212b 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDescriptorProvider.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers private const string CatchAllDescriptorTarget = "*"; private IDictionary> _registrations; + private string _tagHelperPrefix; /// /// Instantiates a new instance of the . @@ -43,10 +44,18 @@ namespace Microsoft.AspNet.Razor.TagHelpers { HashSet descriptors; - // Ensure there's an ISet to use. + 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(); + } + + // Ensure there's a HashSet to use. if (!_registrations.TryGetValue(CatchAllDescriptorTarget, out descriptors)) { - descriptors = new HashSet(); + descriptors = new HashSet(TagHelperDescriptorComparer.Default); } // If the requested tag name is the catch-all target, we should short circuit. @@ -72,11 +81,21 @@ namespace Microsoft.AspNet.Razor.TagHelpers { HashSet descriptorSet; - // Ensure there's a List to add the descriptor to. - if (!_registrations.TryGetValue(descriptor.TagName, out descriptorSet)) + if (_tagHelperPrefix == null) + { + _tagHelperPrefix = descriptor.Prefix; + } + + var registrationKey = + string.Equals(descriptor.TagName, CatchAllDescriptorTarget, StringComparison.Ordinal) ? + CatchAllDescriptorTarget : + descriptor.FullTagName; + + // Ensure there's a HashSet to add the descriptor to. + if (!_registrations.TryGetValue(registrationKey, out descriptorSet)) { descriptorSet = new HashSet(TagHelperDescriptorComparer.Default); - _registrations[descriptor.TagName] = descriptorSet; + _registrations[registrationKey] = descriptorSet; } descriptorSet.Add(descriptor); diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveDescriptor.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveDescriptor.cs index 6a662f1dd6..59f85bf42a 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveDescriptor.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveDescriptor.cs @@ -11,23 +11,24 @@ namespace Microsoft.AspNet.Razor.TagHelpers public class TagHelperDirectiveDescriptor { // Internal for testing purposes. - internal TagHelperDirectiveDescriptor(string lookupText, + internal TagHelperDirectiveDescriptor(string directiveText, TagHelperDirectiveType directiveType) - : this(lookupText, SourceLocation.Zero, directiveType) + : this(directiveText, SourceLocation.Zero, directiveType) { } /// /// Instantiates a new instance of . /// - /// A used to find tag helper s. + /// A used to understand tag helper + /// s. /// The of the directive. /// The of this directive. - public TagHelperDirectiveDescriptor([NotNull] string lookupText, + public TagHelperDirectiveDescriptor([NotNull] string directiveText, SourceLocation location, TagHelperDirectiveType directiveType) { - LookupText = lookupText; + DirectiveText = directiveText; Location = location; DirectiveType = directiveType; } @@ -35,7 +36,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers /// /// A used to find tag helper s. /// - public string LookupText { get; private set; } + public string DirectiveText { get; private set; } /// /// The of this directive. diff --git a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveType.cs b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveType.cs index 46193df6db..ff45e8cd0a 100644 --- a/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveType.cs +++ b/src/Microsoft.AspNet.Razor/TagHelpers/TagHelperDirectiveType.cs @@ -9,13 +9,18 @@ namespace Microsoft.AspNet.Razor.TagHelpers public enum TagHelperDirectiveType { /// - /// An @addTagHelper directive. + /// An @addTagHelper directive. /// AddTagHelper, /// - /// A @removeTagHelper directive. + /// A @removeTagHelper directive. /// - RemoveTagHelper + RemoveTagHelper, + + /// + /// A @tagHelperPrefix directive. + /// + TagHelperPrefix } } \ No newline at end of file