Add TagHelperPrefix directive.

- Updated TagHelperDescriptor to have a Prefix property. This enables new tooling scenarios such as refactoring prefixes or even giving them their own classification.
- Added invalid prefix cases.
- Added TagHelperPrefix chunks, codegenerator, parsing logic to flow the directive through the Razor pipeline.

#309
This commit is contained in:
N. Taylor Mullen 2015-02-23 21:21:26 -08:00
parent 677df32160
commit 4d97a544f1
21 changed files with 376 additions and 57 deletions

View File

@ -138,6 +138,38 @@ namespace Microsoft.AspNet.Razor.Runtime
return string.Format(CultureInfo.CurrentCulture, GetString("HtmlElementNameAttribute_InvalidElementName"), p0, p1); return string.Format(CultureInfo.CurrentCulture, GetString("HtmlElementNameAttribute_InvalidElementName"), p0, p1);
} }
/// <summary>
/// Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page.
/// </summary>
internal static string TagHelperDescriptorResolver_InvalidTagHelperDirective
{
get { return GetString("TagHelperDescriptorResolver_InvalidTagHelperDirective"); }
}
/// <summary>
/// Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page.
/// </summary>
internal static string FormatTagHelperDescriptorResolver_InvalidTagHelperDirective(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("TagHelperDescriptorResolver_InvalidTagHelperDirective"), p0);
}
/// <summary>
/// Invalid tag helper directive '{0}' value. '{1} is not allowed in prefix '{2}'.
/// </summary>
internal static string TagHelperDescriptorResolver_InvalidTagHelperPrefixValue
{
get { return GetString("TagHelperDescriptorResolver_InvalidTagHelperPrefixValue"); }
}
/// <summary>
/// Invalid tag helper directive '{0}' value. '{1} is not allowed in prefix '{2}'.
/// </summary>
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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -141,4 +141,10 @@
<data name="HtmlElementNameAttribute_InvalidElementName" xml:space="preserve"> <data name="HtmlElementNameAttribute_InvalidElementName" xml:space="preserve">
<value>Tag helpers cannot target element name '{0}' because it contains a '{1}' character.</value> <value>Tag helpers cannot target element name '{0}' because it contains a '{1}' character.</value>
</data> </data>
<data name="TagHelperDescriptorResolver_InvalidTagHelperDirective" xml:space="preserve">
<value>Invalid tag helper directive '{0}'. Cannot have multiple '{0}' directives on a page.</value>
</data>
<data name="TagHelperDescriptorResolver_InvalidTagHelperPrefixValue" xml:space="preserve">
<value>Invalid tag helper directive '{0}' value. '{1} is not allowed in prefix '{2}'.</value>
</data>
</root> </root>

View File

@ -23,7 +23,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
{ TagHelperDirectiveType.AddTagHelper, SyntaxConstants.CSharp.AddTagHelperKeyword }, { TagHelperDirectiveType.AddTagHelper, SyntaxConstants.CSharp.AddTagHelperKeyword },
{ TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword }, { TagHelperDirectiveType.RemoveTagHelper, SyntaxConstants.CSharp.RemoveTagHelperKeyword },
{ TagHelperDirectiveType.TagHelperPrefix, SyntaxConstants.CSharp.TagHelperPrefixKeyword },
}; };
private static readonly HashSet<char> InvalidNonWhitespacePrefixCharacters =
new HashSet<char>(new[] { '@', '!', '<', '!', '/', '?', '[', '>', ']', '=', '"', '\'' });
private readonly TagHelperTypeResolver _typeResolver; private readonly TagHelperTypeResolver _typeResolver;
@ -51,7 +54,12 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
{ {
var resolvedDescriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default); var resolvedDescriptors = new HashSet<TagHelperDescriptor>(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 try
{ {
@ -69,9 +77,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
} }
else if (directiveDescriptor.DirectiveType == TagHelperDirectiveType.AddTagHelper) else if (directiveDescriptor.DirectiveType == TagHelperDirectiveType.AddTagHelper)
{ {
var descriptors = ResolveDescriptorsInAssembly(lookupInfo.AssemblyName, var descriptors = ResolveDescriptorsInAssembly(
directiveDescriptor.Location, lookupInfo.AssemblyName,
context.ErrorSink); directiveDescriptor.Location,
context.ErrorSink);
// Only use descriptors that match our lookup info // Only use descriptors that match our lookup info
descriptors = descriptors.Where(descriptor => MatchesLookupInfo(descriptor, lookupInfo)); descriptors = descriptors.Where(descriptor => MatchesLookupInfo(descriptor, lookupInfo));
@ -89,12 +98,14 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
directiveDescriptor.Location, directiveDescriptor.Location,
Resources.FormatTagHelperDescriptorResolver_EncounteredUnexpectedError( Resources.FormatTagHelperDescriptorResolver_EncounteredUnexpectedError(
"@" + directiveName, "@" + directiveName,
directiveDescriptor.LookupText, directiveDescriptor.DirectiveText,
ex.Message)); ex.Message));
} }
} }
return resolvedDescriptors; var prefixedDescriptors = PrefixDescriptors(context, resolvedDescriptors);
return prefixedDescriptors;
} }
/// <summary> /// <summary>
@ -110,9 +121,10 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
/// <returns><see cref="TagHelperDescriptor"/>s for <see cref="ITagHelper"/>s from the given /// <returns><see cref="TagHelperDescriptor"/>s for <see cref="ITagHelper"/>s from the given
/// <paramref name="assemblyName"/>.</returns> /// <paramref name="assemblyName"/>.</returns>
// This is meant to be overridden by tooling to enable assembly level caching. // This is meant to be overridden by tooling to enable assembly level caching.
protected virtual IEnumerable<TagHelperDescriptor> ResolveDescriptorsInAssembly(string assemblyName, protected virtual IEnumerable<TagHelperDescriptor> ResolveDescriptorsInAssembly(
SourceLocation documentLocation, string assemblyName,
ParserErrorSink errorSink) SourceLocation documentLocation,
ParserErrorSink errorSink)
{ {
// Resolve valid tag helper types from the assembly. // Resolve valid tag helper types from the assembly.
var tagHelperTypes = _typeResolver.Resolve(assemblyName, documentLocation, errorSink); var tagHelperTypes = _typeResolver.Resolve(assemblyName, documentLocation, errorSink);
@ -124,6 +136,84 @@ namespace Microsoft.AspNet.Razor.Runtime.TagHelpers
return descriptors; return descriptors;
} }
private static IEnumerable<TagHelperDescriptor> PrefixDescriptors(
TagHelperDescriptorResolutionContext context,
IEnumerable<TagHelperDescriptor> 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) private static bool MatchesLookupInfo(TagHelperDescriptor descriptor, LookupInfo lookupInfo)
{ {
if (!string.Equals(descriptor.AssemblyName, lookupInfo.AssemblyName, StringComparison.Ordinal)) 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, private static LookupInfo GetLookupInfo(TagHelperDirectiveDescriptor directiveDescriptor,
ParserErrorSink errorSink) ParserErrorSink errorSink)
{ {
var lookupText = directiveDescriptor.LookupText; var lookupText = directiveDescriptor.DirectiveText;
var lookupStrings = lookupText?.Split(new[] { ',' }); var lookupStrings = lookupText?.Split(new[] { ',' });
// Ensure that we have valid lookupStrings to work with. Valid formats are: // Ensure that we have valid lookupStrings to work with. Valid formats are:

View File

@ -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) protected override void Visit(AddTagHelperChunk chunk)
{ {
VisitAddOrRemoveTagHelperChunk(chunk.LookupText, chunk); VisitTagHelperDirectiveChunk(chunk.LookupText, chunk);
} }
protected override void Visit(RemoveTagHelperChunk 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. // We should always be in design time mode because of the calling AcceptTree method verification.
Debug.Assert(Context.Host.DesignTimeMode); Debug.Assert(Context.Host.DesignTimeMode);
@ -81,12 +86,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler.CSharp
Writer.WriteStartAssignment(TagHelperDirectiveSyntaxHelper); Writer.WriteStartAssignment(TagHelperDirectiveSyntaxHelper);
// The parsing mechanism for the add/remove TagHelper chunk (CSharpCodeParser.TagHelperDirective()) // The parsing mechanism for a TagHelper directive chunk (CSharpCodeParser.TagHelperDirective())
// removes quotes that surround the lookupText. // removes quotes that surround the text.
_csharpCodeVisitor.CreateExpressionCodeMapping( _csharpCodeVisitor.CreateExpressionCodeMapping(
string.Format( string.Format(CultureInfo.InvariantCulture, "\"{0}\"", text),
CultureInfo.InvariantCulture,
"\"{0}\"", lookupText),
chunk); chunk);
Writer.WriteLine(";"); Writer.WriteLine(";");

View File

@ -57,6 +57,10 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
{ {
Visit((TagHelperChunk)chunk); Visit((TagHelperChunk)chunk);
} }
else if (chunk is TagHelperPrefixDirectiveChunk)
{
Visit((TagHelperPrefixDirectiveChunk)chunk);
}
else if (chunk is AddTagHelperChunk) else if (chunk is AddTagHelperChunk)
{ {
Visit((AddTagHelperChunk)chunk); Visit((AddTagHelperChunk)chunk);
@ -119,6 +123,7 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
protected abstract void Visit(ExpressionChunk chunk); protected abstract void Visit(ExpressionChunk chunk);
protected abstract void Visit(StatementChunk chunk); protected abstract void Visit(StatementChunk chunk);
protected abstract void Visit(TagHelperChunk chunk); protected abstract void Visit(TagHelperChunk chunk);
protected abstract void Visit(TagHelperPrefixDirectiveChunk chunk);
protected abstract void Visit(AddTagHelperChunk chunk); protected abstract void Visit(AddTagHelperChunk chunk);
protected abstract void Visit(RemoveTagHelperChunk chunk); protected abstract void Visit(RemoveTagHelperChunk chunk);
protected abstract void Visit(UsingChunk chunk); protected abstract void Visit(UsingChunk chunk);

View File

@ -33,6 +33,9 @@ namespace Microsoft.AspNet.Razor.Generator.Compiler
protected override void Visit(TagHelperChunk chunk) protected override void Visit(TagHelperChunk chunk)
{ {
} }
protected override void Visit(TagHelperPrefixDirectiveChunk chunk)
{
}
protected override void Visit(AddTagHelperChunk chunk) protected override void Visit(AddTagHelperChunk chunk)
{ {
} }

View File

@ -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
{
/// <summary>
/// A <see cref="Chunk"/> for the <c>@tagHelperPrefix</c> directive.
/// </summary>
public class TagHelperPrefixDirectiveChunk : Chunk
{
/// <summary>
/// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
/// tag helpers.
/// </summary>
public string Prefix { get; set; }
}
}

View File

@ -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) public void AddAddTagHelperChunk(string lookupText, SyntaxTreeNode association)
{ {
AddChunk(new AddTagHelperChunk AddChunk(new AddTagHelperChunk

View File

@ -75,9 +75,11 @@ namespace Microsoft.AspNet.Razor.Generator
codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder(); codeGenerator.Context.CodeTreeBuilder = new CodeTreeBuilder();
} }
var unprefixedTagName = tagHelperBlock.TagName.Substring(_tagHelperDescriptors.First().Prefix.Length);
context.CodeTreeBuilder.StartChunkBlock( context.CodeTreeBuilder.StartChunkBlock(
new TagHelperChunk( new TagHelperChunk(
tagHelperBlock.TagName, unprefixedTagName,
tagHelperBlock.SelfClosing, tagHelperBlock.SelfClosing,
attributes, attributes,
_tagHelperDescriptors), _tagHelperDescriptors),

View File

@ -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
{
/// <summary>
/// A <see cref="SpanCodeGenerator"/> responsible for generating
/// <see cref="Compiler.TagHelperPrefixDirectiveChunk"/>s.
/// </summary>
public class TagHelperPrefixDirectiveCodeGenerator : SpanCodeGenerator
{
/// <summary>
/// Instantiates a new <see cref="TagHelperPrefixDirectiveCodeGenerator"/>.
/// </summary>
/// <param name="prefix">
/// Text used as a required prefix when matching HTML.
/// </param>
public TagHelperPrefixDirectiveCodeGenerator(string prefix)
{
Prefix = prefix;
}
/// <summary>
/// Text used as a required prefix when matching HTML.
/// </summary>
public string Prefix { get; }
/// <summary>
/// Generates <see cref="Compiler.TagHelperPrefixDirectiveChunk"/>s.
/// </summary>
/// <param name="target">
/// The <see cref="Span"/> responsible for this <see cref="TagHelperPrefixDirectiveCodeGenerator"/>.
/// </param>
/// <param name="context">A <see cref="CodeGeneratorContext"/> instance that contains information about
/// the current code generation process.</param>
public override void GenerateCode(Span target, CodeGeneratorContext context)
{
context.CodeTreeBuilder.AddTagHelperPrefixDirectiveChunk(Prefix, target);
}
}
}

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.Parser
{ {
private void SetupDirectives() private void SetupDirectives()
{ {
MapDirectives(TagHelperPrefixDirective, SyntaxConstants.CSharp.TagHelperPrefixKeyword);
MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword); MapDirectives(AddTagHelperDirective, SyntaxConstants.CSharp.AddTagHelperKeyword);
MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword); MapDirectives(RemoveTagHelperDirective, SyntaxConstants.CSharp.RemoveTagHelperKeyword);
MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword); MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword);
@ -27,14 +28,27 @@ namespace Microsoft.AspNet.Razor.Parser
MapDirectives(LayoutDirective, SyntaxConstants.CSharp.LayoutKeyword); MapDirectives(LayoutDirective, SyntaxConstants.CSharp.LayoutKeyword);
} }
protected virtual void TagHelperPrefixDirective()
{
TagHelperDirective(
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
prefix => new TagHelperPrefixDirectiveCodeGenerator(prefix));
}
protected virtual void AddTagHelperDirective() 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() 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() protected virtual void LayoutDirective()
@ -450,7 +464,7 @@ namespace Microsoft.AspNet.Razor.Parser
Output(SpanKind.Code); Output(SpanKind.Code);
} }
private void TagHelperDirective(string keyword, bool removeTagHelperDescriptors) private void TagHelperDirective(string keyword, Func<string, ISpanCodeGenerator> buildCodeGenerator)
{ {
AssertDirective(keyword); 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 // 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# // so when we render the code we can project the users value into double quotes to not invoke C#
// IntelliSense. // IntelliSense.
Span.CodeGenerator = Span.CodeGenerator = buildCodeGenerator(rawValue.Trim('"'));
new AddOrRemoveTagHelperCodeGenerator(removeTagHelperDescriptors, rawValue.Trim('"'));
} }
// We expect the directive to be surrounded in quotes. // We expect the directive to be surrounded in quotes.

View File

@ -19,6 +19,7 @@ namespace Microsoft.AspNet.Razor.Parser
internal static ISet<string> DefaultKeywords = new HashSet<string>() internal static ISet<string> DefaultKeywords = new HashSet<string>()
{ {
SyntaxConstants.CSharp.TagHelperPrefixKeyword,
SyntaxConstants.CSharp.AddTagHelperKeyword, SyntaxConstants.CSharp.AddTagHelperKeyword,
SyntaxConstants.CSharp.RemoveTagHelperKeyword, SyntaxConstants.CSharp.RemoveTagHelperKeyword,
"if", "if",

View File

@ -213,7 +213,7 @@ namespace Microsoft.AspNet.Razor.Parser
[NotNull] ParserErrorSink errorSink) [NotNull] ParserErrorSink errorSink)
{ {
var addOrRemoveTagHelperSpanVisitor = var addOrRemoveTagHelperSpanVisitor =
new AddOrRemoveTagHelperSpanVisitor(TagHelperDescriptorResolver, errorSink); new TagHelperDirectiveSpanVisitor(TagHelperDescriptorResolver, errorSink);
return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot); return addOrRemoveTagHelperSpanVisitor.GetDescriptors(documentRoot);
} }

View File

@ -18,6 +18,7 @@ namespace Microsoft.AspNet.Razor.Parser
public static class CSharp public static class CSharp
{ {
public static readonly int UsingKeywordLength = 5; public static readonly int UsingKeywordLength = 5;
public static readonly string TagHelperPrefixKeyword = "tagHelperPrefix";
public static readonly string AddTagHelperKeyword = "addTagHelper"; public static readonly string AddTagHelperKeyword = "addTagHelper";
public static readonly string RemoveTagHelperKeyword = "removeTagHelper"; public static readonly string RemoveTagHelperKeyword = "removeTagHelper";
public static readonly string InheritsKeyword = "inherits"; public static readonly string InheritsKeyword = "inherits";

View File

@ -12,7 +12,7 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
/// A <see cref="ParserVisitor"/> that generates <see cref="TagHelperDescriptor"/>s from /// A <see cref="ParserVisitor"/> that generates <see cref="TagHelperDescriptor"/>s from
/// tag helper code generators in a Razor document. /// tag helper code generators in a Razor document.
/// </summary> /// </summary>
public class AddOrRemoveTagHelperSpanVisitor : ParserVisitor public class TagHelperDirectiveSpanVisitor : ParserVisitor
{ {
private readonly ITagHelperDescriptorResolver _descriptorResolver; private readonly ITagHelperDescriptorResolver _descriptorResolver;
private readonly ParserErrorSink _errorSink; private readonly ParserErrorSink _errorSink;
@ -20,12 +20,12 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
private List<TagHelperDirectiveDescriptor> _directiveDescriptors; private List<TagHelperDirectiveDescriptor> _directiveDescriptors;
// Internal for testing use // Internal for testing use
internal AddOrRemoveTagHelperSpanVisitor(ITagHelperDescriptorResolver descriptorResolver) internal TagHelperDirectiveSpanVisitor(ITagHelperDescriptorResolver descriptorResolver)
: this(descriptorResolver, new ParserErrorSink()) : this(descriptorResolver, new ParserErrorSink())
{ {
} }
public AddOrRemoveTagHelperSpanVisitor([NotNull] ITagHelperDescriptorResolver descriptorResolver, public TagHelperDirectiveSpanVisitor([NotNull] ITagHelperDescriptorResolver descriptorResolver,
[NotNull] ParserErrorSink errorSink) [NotNull] ParserErrorSink errorSink)
{ {
_descriptorResolver = descriptorResolver; _descriptorResolver = descriptorResolver;
@ -61,13 +61,26 @@ namespace Microsoft.AspNet.Razor.Parser.TagHelpers
{ {
var codeGenerator = (AddOrRemoveTagHelperCodeGenerator)span.CodeGenerator; var codeGenerator = (AddOrRemoveTagHelperCodeGenerator)span.CodeGenerator;
var directive = codeGenerator.RemoveTagHelperDescriptors ? var directive =
TagHelperDirectiveType.RemoveTagHelper : codeGenerator.RemoveTagHelperDescriptors ?
TagHelperDirectiveType.AddTagHelper; TagHelperDirectiveType.RemoveTagHelper :
TagHelperDirectiveType.AddTagHelper;
var directiveDescriptor = new TagHelperDirectiveDescriptor(codeGenerator.LookupText, var directiveDescriptor = new TagHelperDirectiveDescriptor(
span.Start, codeGenerator.LookupText,
directive); 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); _directiveDescriptors.Add(directiveDescriptor);
} }

View File

@ -54,6 +54,7 @@ namespace Microsoft.AspNet.Razor
TagHelperDescriptors = tagHelperDescriptors; TagHelperDescriptors = tagHelperDescriptors;
ErrorSink = errorSink; ErrorSink = errorSink;
ParserErrors = errorSink.Errors; ParserErrors = errorSink.Errors;
Prefix = tagHelperDescriptors.FirstOrDefault()?.Prefix;
} }
/// <summary> /// <summary>
@ -81,5 +82,10 @@ namespace Microsoft.AspNet.Razor
/// The <see cref="TagHelperDescriptor"/>s found for the current Razor document. /// The <see cref="TagHelperDescriptor"/>s found for the current Razor document.
/// </summary> /// </summary>
public IEnumerable<TagHelperDescriptor> TagHelperDescriptors { get; } public IEnumerable<TagHelperDescriptor> TagHelperDescriptors { get; }
/// <summary>
/// Text used as a required prefix when matching HTML.
/// </summary>
public string Prefix { get; }
} }
} }

View File

@ -17,10 +17,11 @@ namespace Microsoft.AspNet.Razor.TagHelpers
internal TagHelperDescriptor([NotNull] string tagName, internal TagHelperDescriptor([NotNull] string tagName,
[NotNull] string typeName, [NotNull] string typeName,
[NotNull] string assemblyName) [NotNull] string assemblyName)
: this(tagName, : this(
typeName, tagName,
assemblyName, typeName,
Enumerable.Empty<TagHelperAttributeDescriptor>()) assemblyName,
attributes: Enumerable.Empty<TagHelperAttributeDescriptor>())
{ {
} }
@ -35,22 +36,67 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <param name="attributes"> /// <param name="attributes">
/// The <see cref="TagHelperAttributeDescriptor"/>s to request from the HTML tag. /// The <see cref="TagHelperAttributeDescriptor"/>s to request from the HTML tag.
/// </param> /// </param>
public TagHelperDescriptor([NotNull] string tagName, public TagHelperDescriptor(
[NotNull] string typeName, [NotNull] string tagName,
[NotNull] string assemblyName, [NotNull] string typeName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes) [NotNull] string assemblyName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes)
: this(
prefix: string.Empty,
tagName: tagName,
typeName: typeName,
assemblyName: assemblyName,
attributes: attributes)
{ {
}
/// <summary>
/// Instantiates a new instance of the <see cref="TagHelperDescriptor"/> class with the given
/// <paramref name="attributes"/>.
/// </summary>
/// <param name="prefix">
/// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
/// tag helpers.
/// </param>
/// <param name="tagName">The tag name that the tag helper targets. '*' indicates a catch-all
/// <see cref="TagHelperDescriptor"/> which applies to every HTML tag.</param>
/// <param name="typeName">The full name of the tag helper class.</param>
/// <param name="assemblyName">The name of the assembly containing the tag helper class.</param>
/// <param name="attributes">
/// The <see cref="TagHelperAttributeDescriptor"/>s to request from the HTML tag.
/// </param>
public TagHelperDescriptor(
string prefix,
[NotNull] string tagName,
[NotNull] string typeName,
[NotNull] string assemblyName,
[NotNull] IEnumerable<TagHelperAttributeDescriptor> attributes)
{
Prefix = prefix ?? string.Empty;
TagName = tagName; TagName = tagName;
FullTagName = Prefix + TagName;
TypeName = typeName; TypeName = typeName;
AssemblyName = assemblyName; AssemblyName = assemblyName;
Attributes = new List<TagHelperAttributeDescriptor>(attributes); Attributes = new List<TagHelperAttributeDescriptor>(attributes);
} }
/// <summary>
/// Text used as a required prefix when matching HTML start and end tags in the Razor source to available
/// tag helpers.
/// </summary>
public string Prefix { get; private set; }
/// <summary> /// <summary>
/// The tag name that the tag helper should target. /// The tag name that the tag helper should target.
/// </summary> /// </summary>
public string TagName { get; private set; } public string TagName { get; private set; }
/// <summary>
/// The full tag name that is required for the tag helper to target an HTML element.
/// </summary>
/// <remarks>This is equivalent to <see cref="Prefix"/> and <see cref="TagName"/> concatenated.</remarks>
public string FullTagName { get; private set; }
/// <summary> /// <summary>
/// The full name of the tag helper class. /// The full name of the tag helper class.
/// </summary> /// </summary>

View File

@ -29,12 +29,14 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <c>false</c> otherwise.</returns> /// <c>false</c> otherwise.</returns>
/// <remarks> /// <remarks>
/// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>, /// Determines equality based on <see cref="TagHelperDescriptor.TypeName"/>,
/// <see cref="TagHelperDescriptor.AssemblyName"/> and <see cref="TagHelperDescriptor.TagName"/>. /// <see cref="TagHelperDescriptor.AssemblyName"/>, <see cref="TagHelperDescriptor.TagName"/> and
/// <see cref="TagHelperDescriptor.Prefix"/>.
/// </remarks> /// </remarks>
public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY) public bool Equals(TagHelperDescriptor descriptorX, TagHelperDescriptor descriptorY)
{ {
return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) && return string.Equals(descriptorX.TypeName, descriptorY.TypeName, StringComparison.Ordinal) &&
string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) && string.Equals(descriptorX.TagName, descriptorY.TagName, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.Prefix, descriptorY.Prefix, StringComparison.OrdinalIgnoreCase) &&
string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal); string.Equals(descriptorX.AssemblyName, descriptorY.AssemblyName, StringComparison.Ordinal);
} }

View File

@ -15,6 +15,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
private const string CatchAllDescriptorTarget = "*"; private const string CatchAllDescriptorTarget = "*";
private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations; private IDictionary<string, HashSet<TagHelperDescriptor>> _registrations;
private string _tagHelperPrefix;
/// <summary> /// <summary>
/// Instantiates a new instance of the <see cref="TagHelperDescriptorProvider"/>. /// Instantiates a new instance of the <see cref="TagHelperDescriptorProvider"/>.
@ -43,10 +44,18 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{ {
HashSet<TagHelperDescriptor> descriptors; HashSet<TagHelperDescriptor> 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<TagHelperDescriptor>();
}
// Ensure there's a HashSet to use.
if (!_registrations.TryGetValue(CatchAllDescriptorTarget, out descriptors)) if (!_registrations.TryGetValue(CatchAllDescriptorTarget, out descriptors))
{ {
descriptors = new HashSet<TagHelperDescriptor>(); descriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
} }
// If the requested tag name is the catch-all target, we should short circuit. // If the requested tag name is the catch-all target, we should short circuit.
@ -72,11 +81,21 @@ namespace Microsoft.AspNet.Razor.TagHelpers
{ {
HashSet<TagHelperDescriptor> descriptorSet; HashSet<TagHelperDescriptor> descriptorSet;
// Ensure there's a List to add the descriptor to. if (_tagHelperPrefix == null)
if (!_registrations.TryGetValue(descriptor.TagName, out descriptorSet)) {
_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<TagHelperDescriptor>(TagHelperDescriptorComparer.Default); descriptorSet = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
_registrations[descriptor.TagName] = descriptorSet; _registrations[registrationKey] = descriptorSet;
} }
descriptorSet.Add(descriptor); descriptorSet.Add(descriptor);

View File

@ -11,23 +11,24 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public class TagHelperDirectiveDescriptor public class TagHelperDirectiveDescriptor
{ {
// Internal for testing purposes. // Internal for testing purposes.
internal TagHelperDirectiveDescriptor(string lookupText, internal TagHelperDirectiveDescriptor(string directiveText,
TagHelperDirectiveType directiveType) TagHelperDirectiveType directiveType)
: this(lookupText, SourceLocation.Zero, directiveType) : this(directiveText, SourceLocation.Zero, directiveType)
{ {
} }
/// <summary> /// <summary>
/// Instantiates a new instance of <see cref="TagHelperDirectiveDescriptor"/>. /// Instantiates a new instance of <see cref="TagHelperDirectiveDescriptor"/>.
/// </summary> /// </summary>
/// <param name="lookupText">A <see cref="string"/> used to find tag helper <see cref="System.Type"/>s.</param> /// <param name="directiveText">A <see cref="string"/> used to understand tag helper
/// <see cref="System.Type"/>s.</param>
/// <param name="location">The <see cref="SourceLocation"/> of the directive.</param> /// <param name="location">The <see cref="SourceLocation"/> of the directive.</param>
/// <param name="directiveType">The <see cref="TagHelperDirectiveType"/> of this directive.</param> /// <param name="directiveType">The <see cref="TagHelperDirectiveType"/> of this directive.</param>
public TagHelperDirectiveDescriptor([NotNull] string lookupText, public TagHelperDirectiveDescriptor([NotNull] string directiveText,
SourceLocation location, SourceLocation location,
TagHelperDirectiveType directiveType) TagHelperDirectiveType directiveType)
{ {
LookupText = lookupText; DirectiveText = directiveText;
Location = location; Location = location;
DirectiveType = directiveType; DirectiveType = directiveType;
} }
@ -35,7 +36,7 @@ namespace Microsoft.AspNet.Razor.TagHelpers
/// <summary> /// <summary>
/// A <see cref="string"/> used to find tag helper <see cref="System.Type"/>s. /// A <see cref="string"/> used to find tag helper <see cref="System.Type"/>s.
/// </summary> /// </summary>
public string LookupText { get; private set; } public string DirectiveText { get; private set; }
/// <summary> /// <summary>
/// The <see cref="TagHelperDirectiveType"/> of this directive. /// The <see cref="TagHelperDirectiveType"/> of this directive.

View File

@ -9,13 +9,18 @@ namespace Microsoft.AspNet.Razor.TagHelpers
public enum TagHelperDirectiveType public enum TagHelperDirectiveType
{ {
/// <summary> /// <summary>
/// An @addTagHelper directive. /// An <c>@addTagHelper</c> directive.
/// </summary> /// </summary>
AddTagHelper, AddTagHelper,
/// <summary> /// <summary>
/// A @removeTagHelper directive. /// A <c>@removeTagHelper</c> directive.
/// </summary> /// </summary>
RemoveTagHelper RemoveTagHelper,
/// <summary>
/// A <c>@tagHelperPrefix</c> directive.
/// </summary>
TagHelperPrefix
} }
} }