In CSS scoping, support ::deep alongside other combinators (#24376)

This commit is contained in:
Steve Sanderson 2020-07-29 14:49:50 +01:00 committed by GitHub
parent 01e05359d6
commit 5e49fc336e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 78 additions and 5 deletions

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Css.Parser.Parser;
using Microsoft.Css.Parser.Tokens;
@ -18,6 +19,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
{
internal class RewriteCssCommand : CommandBase
{
private const string DeepCombinatorText = "::deep";
private readonly static TimeSpan _regexTimeout = TimeSpan.FromSeconds(1);
private readonly static Regex _deepCombinatorRegex = new Regex($@"^{DeepCombinatorText}\s*", RegexOptions.None, _regexTimeout);
private readonly static Regex _trailingCombinatorRegex = new Regex(@"\s+[\>\+\~]$", RegexOptions.None, _regexTimeout);
public RewriteCssCommand(Application parent)
: base(parent, "rewritecss")
{
@ -145,12 +151,12 @@ namespace Microsoft.AspNetCore.Razor.Tools
// If there's a deep combinator among the sequence of simple selectors, we consider that to signal
// the end of the set of simple selectors for us to look at, plus we strip it out
var allSimpleSelectors = selector.Children.OfType<SimpleSelector>();
var firstDeepCombinator = allSimpleSelectors.FirstOrDefault(s => IsDeepCombinator(s.Text));
var firstDeepCombinator = allSimpleSelectors.FirstOrDefault(s => _deepCombinatorRegex.IsMatch(s.Text));
var lastSimpleSelector = allSimpleSelectors.TakeWhile(s => s != firstDeepCombinator).LastOrDefault();
if (lastSimpleSelector != null)
{
Edits.Add(new InsertSelectorScopeEdit { Position = lastSimpleSelector.AfterEnd });
Edits.Add(new InsertSelectorScopeEdit { Position = FindPositionBeforeTrailingCombinator(lastSimpleSelector) });
}
else if (firstDeepCombinator != null)
{
@ -162,13 +168,32 @@ namespace Microsoft.AspNetCore.Razor.Tools
// Also remove the deep combinator if we matched one
if (firstDeepCombinator != null)
{
Edits.Add(new DeleteContentEdit { Position = firstDeepCombinator.Start, DeleteLength = firstDeepCombinator.Length });
Edits.Add(new DeleteContentEdit { Position = firstDeepCombinator.Start, DeleteLength = DeepCombinatorText.Length });
}
}
private static bool IsDeepCombinator(string simpleSelectorText)
private int FindPositionBeforeTrailingCombinator(SimpleSelector lastSimpleSelector)
{
return string.Equals(simpleSelectorText, "::deep", StringComparison.Ordinal);
// For a selector like "a > ::deep b", the parser splits it as "a >", "::deep", "b".
// The place we want to insert the scope is right after "a", hence we need to detect
// if the simple selector ends with " >" or similar, and if so, insert before that.
var text = lastSimpleSelector.Text;
var lastChar = text.Length > 0 ? text[^1] : default;
switch (lastChar)
{
case '>':
case '+':
case '~':
var trailingCombinatorMatch = _trailingCombinatorRegex.Match(text);
if (trailingCombinatorMatch.Success)
{
var trailingCombinatorLength = trailingCombinatorMatch.Length;
return lastSimpleSelector.AfterEnd - trailingCombinatorLength;
}
break;
}
return lastSimpleSelector.AfterEnd;
}
protected override void VisitAtDirective(AtDirective item)

View File

@ -91,6 +91,54 @@ namespace Microsoft.AspNetCore.Razor.Tools
", result);
}
[Fact]
public void RespectsDeepCombinatorWithDirectDescendant()
{
// Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@"
a > ::deep b { color: red; }
c ::deep > d { color: blue; }
", "TestScope");
// Assert
Assert.Equal(@"
a[TestScope] > b { color: red; }
c[TestScope] > d { color: blue; }
", result);
}
[Fact]
public void RespectsDeepCombinatorWithAdjacentSibling()
{
// Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@"
a + ::deep b { color: red; }
c ::deep + d { color: blue; }
", "TestScope");
// Assert
Assert.Equal(@"
a[TestScope] + b { color: red; }
c[TestScope] + d { color: blue; }
", result);
}
[Fact]
public void RespectsDeepCombinatorWithGeneralSibling()
{
// Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@"
a ~ ::deep b { color: red; }
c ::deep ~ d { color: blue; }
", "TestScope");
// Assert
Assert.Equal(@"
a[TestScope] ~ b { color: red; }
c[TestScope] ~ d { color: blue; }
", result);
}
[Fact]
public void IgnoresMultipleDeepCombinators()
{