aspnetcore/src/Microsoft.CodeAnalysis.Razor/RequiredAttributeParser.cs

305 lines
11 KiB
C#

// 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.AspNetCore.Razor.Language;
namespace Microsoft.CodeAnalysis.Razor
{
// Internal for testing
internal static class RequiredAttributeParser
{
public static void AddRequiredAttributes(string requiredAttributes, TagMatchingRuleDescriptorBuilder ruleBuilder)
{
var requiredAttributeParser = new DefaultRequiredAttributeParser(requiredAttributes);
requiredAttributeParser.AddRequiredAttributes(ruleBuilder);
}
private class DefaultRequiredAttributeParser
{
private const char RequiredAttributeWildcardSuffix = '*';
private static readonly IReadOnlyDictionary<char, RequiredAttributeDescriptor.ValueComparisonMode> CssValueComparisons =
new Dictionary<char, RequiredAttributeDescriptor.ValueComparisonMode>
{
{ '=', RequiredAttributeDescriptor.ValueComparisonMode.FullMatch },
{ '^', RequiredAttributeDescriptor.ValueComparisonMode.PrefixMatch },
{ '$', RequiredAttributeDescriptor.ValueComparisonMode.SuffixMatch }
};
private static readonly char[] InvalidPlainAttributeNameCharacters = { ' ', '\t', ',', RequiredAttributeWildcardSuffix };
private static readonly char[] InvalidCssAttributeNameCharacters = (new[] { ' ', '\t', ',', ']' })
.Concat(CssValueComparisons.Keys)
.ToArray();
private static readonly char[] InvalidCssQuotelessValueCharacters = { ' ', '\t', ']' };
private int _index;
private string _requiredAttributes;
public DefaultRequiredAttributeParser(string requiredAttributes)
{
_requiredAttributes = requiredAttributes;
}
private char Current => _requiredAttributes[_index];
private bool AtEnd => _index >= _requiredAttributes.Length;
public void AddRequiredAttributes(TagMatchingRuleDescriptorBuilder ruleBuilder)
{
if (string.IsNullOrEmpty(_requiredAttributes))
{
return;
}
var descriptors = new List<RequiredAttributeDescriptor>();
PassOptionalWhitespace();
do
{
var successfulParse = true;
ruleBuilder.Attribute(attributeBuilder =>
{
if (At('['))
{
if (!TryParseCssSelector(attributeBuilder))
{
successfulParse = false;
return;
}
}
else
{
ParsePlainSelector(attributeBuilder);
}
PassOptionalWhitespace();
if (At(','))
{
_index++;
if (!EnsureNotAtEnd(attributeBuilder))
{
successfulParse = false;
return;
}
}
else if (!AtEnd)
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeCharacter(Current, _requiredAttributes);
attributeBuilder.Diagnostics.Add(diagnostic);
successfulParse = false;
return;
}
PassOptionalWhitespace();
});
if (!successfulParse)
{
break;
}
}
while (!AtEnd);
}
private void ParsePlainSelector(RequiredAttributeDescriptorBuilder attributeBuilder)
{
var nameEndIndex = _requiredAttributes.IndexOfAny(InvalidPlainAttributeNameCharacters, _index);
string attributeName;
var nameComparison = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
if (nameEndIndex == -1)
{
attributeName = _requiredAttributes.Substring(_index);
_index = _requiredAttributes.Length;
}
else
{
attributeName = _requiredAttributes.Substring(_index, nameEndIndex - _index);
_index = nameEndIndex;
if (_requiredAttributes[nameEndIndex] == RequiredAttributeWildcardSuffix)
{
nameComparison = RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch;
// Move past wild card
_index++;
}
}
attributeBuilder.Name = attributeName;
attributeBuilder.NameComparisonMode = nameComparison;
}
private void ParseCssAttributeName(RequiredAttributeDescriptorBuilder builder)
{
var nameStartIndex = _index;
var nameEndIndex = _requiredAttributes.IndexOfAny(InvalidCssAttributeNameCharacters, _index);
nameEndIndex = nameEndIndex == -1 ? _requiredAttributes.Length : nameEndIndex;
_index = nameEndIndex;
var attributeName = _requiredAttributes.Substring(nameStartIndex, nameEndIndex - nameStartIndex);
builder.Name = attributeName;
}
private bool TryParseCssValueComparison(RequiredAttributeDescriptorBuilder builder, out RequiredAttributeDescriptor.ValueComparisonMode valueComparison)
{
Debug.Assert(!AtEnd);
if (CssValueComparisons.TryGetValue(Current, out valueComparison))
{
var op = Current;
_index++;
if (op != '=' && At('='))
{
// Two length operator (ex: ^=). Move past the second piece
_index++;
}
else if (op != '=') // We're at an incomplete operator (ex: [foo^]
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_PartialRequiredAttributeOperator(op, _requiredAttributes);
builder.Diagnostics.Add(diagnostic);
return false;
}
}
else if (!At(']'))
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeOperator(Current, _requiredAttributes);
builder.Diagnostics.Add(diagnostic);
return false;
}
builder.ValueComparisonMode = valueComparison;
return true;
}
private bool TryParseCssValue(RequiredAttributeDescriptorBuilder builder)
{
int valueStart;
int valueEnd;
if (At('\'') || At('"'))
{
var quote = Current;
// Move past the quote
_index++;
valueStart = _index;
valueEnd = _requiredAttributes.IndexOf(quote, _index);
if (valueEnd == -1)
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeMismatchedQuotes(quote, _requiredAttributes);
builder.Diagnostics.Add(diagnostic);
return false;
}
_index = valueEnd + 1;
}
else
{
valueStart = _index;
var valueEndIndex = _requiredAttributes.IndexOfAny(InvalidCssQuotelessValueCharacters, _index);
valueEnd = valueEndIndex == -1 ? _requiredAttributes.Length : valueEndIndex;
_index = valueEnd;
}
var value = _requiredAttributes.Substring(valueStart, valueEnd - valueStart);
builder.Value = value;
return true;
}
private bool TryParseCssSelector(RequiredAttributeDescriptorBuilder attributeBuilder)
{
Debug.Assert(At('['));
// Move past '['.
_index++;
PassOptionalWhitespace();
ParseCssAttributeName(attributeBuilder);
PassOptionalWhitespace();
if (!EnsureNotAtEnd(attributeBuilder))
{
return false;
}
if (!TryParseCssValueComparison(attributeBuilder, out RequiredAttributeDescriptor.ValueComparisonMode valueComparison))
{
return false;
}
PassOptionalWhitespace();
if (!EnsureNotAtEnd(attributeBuilder))
{
return false;
}
if (valueComparison != RequiredAttributeDescriptor.ValueComparisonMode.None && !TryParseCssValue(attributeBuilder))
{
return false;
}
PassOptionalWhitespace();
if (At(']'))
{
// Move past the ending bracket.
_index++;
return true;
}
else if (AtEnd)
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace(_requiredAttributes);
attributeBuilder.Diagnostics.Add(diagnostic);
}
else
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidRequiredAttributeCharacter(Current, _requiredAttributes);
attributeBuilder.Diagnostics.Add(diagnostic);
}
return false;
}
private bool EnsureNotAtEnd(RequiredAttributeDescriptorBuilder builder)
{
if (AtEnd)
{
var diagnostic = RazorDiagnosticFactory.CreateTagHelper_CouldNotFindMatchingEndBrace(_requiredAttributes);
builder.Diagnostics.Add(diagnostic);
return false;
}
return true;
}
private bool At(char c)
{
return !AtEnd && Current == c;
}
private void PassOptionalWhitespace()
{
while (!AtEnd && (Current == ' ' || Current == '\t'))
{
_index++;
}
}
}
}
}