File: Language\TagHelperBinder.cs
Web Access
Project: src\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics;
using Microsoft.AspNetCore.Razor.PooledObjects;
 
namespace Microsoft.AspNetCore.Razor.Language;
 
/// <summary>
/// Enables retrieval of <see cref="TagHelperBinding"/>'s.
/// </summary>
internal sealed partial class TagHelperBinder
{
    private readonly TagHelperSet _catchAllTagHelpers;
    private readonly ReadOnlyDictionary<string, TagHelperSet> _tagNameToTagHelpersMap;
 
    public string? TagNamePrefix { get; }
    public TagHelperCollection TagHelpers { get; }
 
    /// <summary>
    /// Instantiates a new instance of the <see cref="TagHelperBinder"/>.
    /// </summary>
    /// <param name="tagNamePrefix">The tag helper prefix being used by the document.</param>
    /// <param name="tagHelpers">The <see cref="TagHelperDescriptor"/>s that the <see cref="TagHelperBinder"/>
    /// will pull from.</param>
    public TagHelperBinder(string? tagNamePrefix, TagHelperCollection tagHelpers)
    {
        TagNamePrefix = tagNamePrefix;
        TagHelpers = tagHelpers;
 
        ProcessDescriptors(tagHelpers, tagNamePrefix, out _tagNameToTagHelpersMap, out _catchAllTagHelpers);
    }
 
    private static void ProcessDescriptors(
        TagHelperCollection descriptors,
        string? tagNamePrefix,
        out ReadOnlyDictionary<string, TagHelperSet> tagNameToDescriptorsMap,
        out TagHelperSet catchAllDescriptors)
    {
        // Initialize a MemoryBuilder of TagHelperSet.Builders. We need a builder for each unique tag name.
        using var builders = new MemoryBuilder<TagHelperSet.Builder>(initialCapacity: 32, clearArray: true);
 
        // Keep track of what needs to be added in the second pass.
        // There will be an entry for every tag matching rule.
        // Each entry consists of an index to identify a builder and the TagHelperDescriptor to add to it.
        using var toAdd = new MemoryBuilder<(int, TagHelperDescriptor)>(initialCapacity: descriptors.Count * 4, clearArray: true);
 
        // Use a special TagHelperSet.Builder to track catch-all tag helpers.
        var catchAllBuilder = new TagHelperSet.Builder();
 
        // At most, there should only be one catch-all tag helper per descriptor.
        using var catchAllToAdd = new MemoryBuilder<TagHelperDescriptor>(initialCapacity: descriptors.Count, clearArray: true);
 
        // The builders are indexed using a map of "tag name" to the index of the builder in the array.
        using var _1 = SpecializedPools.GetPooledStringDictionary<int>(ignoreCase: true, out var tagNameToBuilderIndexMap);
 
        foreach (var tagHelper in descriptors)
        {
 
            foreach (var rule in tagHelper.TagMatchingRules)
            {
                var tagName = rule.TagName;
 
                if (tagName == TagHelperMatchingConventions.ElementCatchAllName)
                {
                    catchAllBuilder.IncreaseSize();
                    catchAllToAdd.Append(tagHelper);
                    continue;
                }
 
                if (!tagNameToBuilderIndexMap.TryGetValue(tagName, out var builderIndex))
                {
                    builderIndex = builders.Length;
                    builders.Append(default(TagHelperSet.Builder));
 
                    tagNameToBuilderIndexMap.Add(tagName, builderIndex);
                }
 
                builders[builderIndex].IncreaseSize();
                toAdd.Append((builderIndex, tagHelper));
            }
        }
 
        // Next, we walk through toAdd and add each descriptor to the appropriate builder.
        // Because we counted first, we know that each builder will allocate exactly the
        // space needed for the final result.
        foreach (var (builderIndex, tagHelper) in toAdd.AsMemory().Span)
        {
            builders[builderIndex].Add(tagHelper);
        }
 
        foreach (var tagHelper in catchAllToAdd.AsMemory().Span)
        {
            catchAllBuilder.Add(tagHelper);
        }
 
        // Build the final dictionary.
        var map = new Dictionary<string, TagHelperSet>(capacity: tagNameToBuilderIndexMap.Count, StringComparer.OrdinalIgnoreCase);
 
        foreach (var (tagName, builderIndex) in tagNameToBuilderIndexMap)
        {
            map.Add(tagNamePrefix + tagName, builders[builderIndex].ToSet());
        }
 
        tagNameToDescriptorsMap = new ReadOnlyDictionary<string, TagHelperSet>(map);
 
        // Build the "catch all" tag helpers set.
        catchAllDescriptors = catchAllBuilder.ToSet();
    }
 
    /// <summary>
    /// Gets all tag helpers that match the given HTML tag criteria.
    /// </summary>
    /// <param name="tagName">The name of the HTML tag to match. Providing a '*' tag name
    /// retrieves catch-all <see cref="TagHelperDescriptor"/>s (descriptors that target every tag).</param>
    /// <param name="attributes">Attributes on the HTML tag.</param>
    /// <param name="parentTagName">The parent tag name of the given <paramref name="tagName"/> tag.</param>
    /// <param name="parentIsTagHelper">Is the parent tag of the given <paramref name="tagName"/> tag a tag helper.</param>
    /// <returns><see cref="TagHelperDescriptor"/>s that apply to the given HTML tag criteria.
    /// Will return <see langword="null"/> if no <see cref="TagHelperDescriptor"/>s are a match.</returns>
    public TagHelperBinding? GetBinding(
        string tagName,
        ImmutableArray<KeyValuePair<string, string>> attributes,
        string? parentTagName,
        bool parentIsTagHelper)
    {
        var tagNameSpan = tagName.AsSpan();
        var parentTagNameSpan = parentTagName.AsSpan();
        var tagNamePrefixSpan = TagNamePrefix.AsSpan();
 
        if (!tagNamePrefixSpan.IsEmpty)
        {
            if (!tagNameSpan.StartsWith(tagNamePrefixSpan, StringComparison.OrdinalIgnoreCase))
            {
                // The tag name doesn't start with the prefix. So, we're done.
                return null;
            }
 
            tagNameSpan = tagNameSpan[tagNamePrefixSpan.Length..];
 
            if (parentIsTagHelper)
            {
                Debug.Assert(
                    parentTagNameSpan.StartsWith(tagNamePrefixSpan, StringComparison.OrdinalIgnoreCase),
                    "If the parent is a tag helper, it must start with the tag name prefix.");
 
                parentTagNameSpan = parentTagNameSpan[tagNamePrefixSpan.Length..];
            }
        }
 
        using var resultsBuilder = new PooledArrayBuilder<TagHelperBoundRulesInfo>();
        using var tempRulesBuilder = new PooledArrayBuilder<TagMatchingRuleDescriptor>();
        using var pooledSet = HashSetPool<TagHelperDescriptor>.GetPooledObject(out var distinctSet);
 
        // First, try any tag helpers with this tag name.
        if (_tagNameToTagHelpersMap.TryGetValue(tagName, out var matchingDescriptors))
        {
            CollectBoundRulesInfo(
                matchingDescriptors,
                tagNameSpan, parentTagNameSpan, attributes,
                ref resultsBuilder.AsRef(), ref tempRulesBuilder.AsRef(), distinctSet);
        }
 
        // Next, try any "catch all" descriptors.
        CollectBoundRulesInfo(
            _catchAllTagHelpers,
            tagNameSpan, parentTagNameSpan, attributes,
            ref resultsBuilder.AsRef(), ref tempRulesBuilder.AsRef(), distinctSet);
 
        return resultsBuilder.Count > 0
            ? new(resultsBuilder.ToImmutableAndClear(), tagName, parentTagName, attributes, TagNamePrefix)
            : null;
 
        static void CollectBoundRulesInfo(
            TagHelperSet descriptors,
            ReadOnlySpan<char> tagName,
            ReadOnlySpan<char> parentTagName,
            ImmutableArray<KeyValuePair<string, string>> attributes,
            ref PooledArrayBuilder<TagHelperBoundRulesInfo> resultsBuilder,
            ref PooledArrayBuilder<TagMatchingRuleDescriptor> tempRulesBuilder,
            HashSet<TagHelperDescriptor> distinctSet)
        {
            foreach (var descriptor in descriptors)
            {
                if (!distinctSet.Add(descriptor))
                {
                    // We're already seen this descriptor, skip it.
                    continue;
                }
 
                Debug.Assert(tempRulesBuilder.Count == 0);
 
                foreach (var rule in descriptor.TagMatchingRules)
                {
                    if (TagHelperMatchingConventions.SatisfiesRule(rule, tagName, parentTagName, attributes))
                    {
                        tempRulesBuilder.Add(rule);
                    }
                }
 
                if (tempRulesBuilder.Count > 0)
                {
                    resultsBuilder.Add(new(descriptor, tempRulesBuilder.ToImmutable()));
                }
 
                tempRulesBuilder.Clear();
            }
        }
    }
}