diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs index 6369d3bc93..f9e33cae14 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs @@ -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(); - 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) diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs b/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs index 57d8830a52..2c2d199f80 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs @@ -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() {