Skip to content

Commit 5e49fc3

Browse files
In CSS scoping, support ::deep alongside other combinators (#24376)
1 parent 01e0535 commit 5e49fc3

File tree

2 files changed

+78
-5
lines changed

2 files changed

+78
-5
lines changed

src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Linq;
88
using System.Text;
9+
using System.Text.RegularExpressions;
910
using System.Threading.Tasks;
1011
using Microsoft.Css.Parser.Parser;
1112
using Microsoft.Css.Parser.Tokens;
@@ -18,6 +19,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
1819
{
1920
internal class RewriteCssCommand : CommandBase
2021
{
22+
private const string DeepCombinatorText = "::deep";
23+
private readonly static TimeSpan _regexTimeout = TimeSpan.FromSeconds(1);
24+
private readonly static Regex _deepCombinatorRegex = new Regex($@"^{DeepCombinatorText}\s*", RegexOptions.None, _regexTimeout);
25+
private readonly static Regex _trailingCombinatorRegex = new Regex(@"\s+[\>\+\~]$", RegexOptions.None, _regexTimeout);
26+
2127
public RewriteCssCommand(Application parent)
2228
: base(parent, "rewritecss")
2329
{
@@ -145,12 +151,12 @@ protected override void VisitSelector(Selector selector)
145151
// If there's a deep combinator among the sequence of simple selectors, we consider that to signal
146152
// the end of the set of simple selectors for us to look at, plus we strip it out
147153
var allSimpleSelectors = selector.Children.OfType<SimpleSelector>();
148-
var firstDeepCombinator = allSimpleSelectors.FirstOrDefault(s => IsDeepCombinator(s.Text));
154+
var firstDeepCombinator = allSimpleSelectors.FirstOrDefault(s => _deepCombinatorRegex.IsMatch(s.Text));
149155

150156
var lastSimpleSelector = allSimpleSelectors.TakeWhile(s => s != firstDeepCombinator).LastOrDefault();
151157
if (lastSimpleSelector != null)
152158
{
153-
Edits.Add(new InsertSelectorScopeEdit { Position = lastSimpleSelector.AfterEnd });
159+
Edits.Add(new InsertSelectorScopeEdit { Position = FindPositionBeforeTrailingCombinator(lastSimpleSelector) });
154160
}
155161
else if (firstDeepCombinator != null)
156162
{
@@ -162,13 +168,32 @@ protected override void VisitSelector(Selector selector)
162168
// Also remove the deep combinator if we matched one
163169
if (firstDeepCombinator != null)
164170
{
165-
Edits.Add(new DeleteContentEdit { Position = firstDeepCombinator.Start, DeleteLength = firstDeepCombinator.Length });
171+
Edits.Add(new DeleteContentEdit { Position = firstDeepCombinator.Start, DeleteLength = DeepCombinatorText.Length });
166172
}
167173
}
168174

169-
private static bool IsDeepCombinator(string simpleSelectorText)
175+
private int FindPositionBeforeTrailingCombinator(SimpleSelector lastSimpleSelector)
170176
{
171-
return string.Equals(simpleSelectorText, "::deep", StringComparison.Ordinal);
177+
// For a selector like "a > ::deep b", the parser splits it as "a >", "::deep", "b".
178+
// The place we want to insert the scope is right after "a", hence we need to detect
179+
// if the simple selector ends with " >" or similar, and if so, insert before that.
180+
var text = lastSimpleSelector.Text;
181+
var lastChar = text.Length > 0 ? text[^1] : default;
182+
switch (lastChar)
183+
{
184+
case '>':
185+
case '+':
186+
case '~':
187+
var trailingCombinatorMatch = _trailingCombinatorRegex.Match(text);
188+
if (trailingCombinatorMatch.Success)
189+
{
190+
var trailingCombinatorLength = trailingCombinatorMatch.Length;
191+
return lastSimpleSelector.AfterEnd - trailingCombinatorLength;
192+
}
193+
break;
194+
}
195+
196+
return lastSimpleSelector.AfterEnd;
172197
}
173198

174199
protected override void VisitAtDirective(AtDirective item)

src/Razor/Microsoft.AspNetCore.Razor.Tools/test/RewriteCssCommandTest.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,54 @@ public void RespectsDeepCombinator()
9191
", result);
9292
}
9393

94+
[Fact]
95+
public void RespectsDeepCombinatorWithDirectDescendant()
96+
{
97+
// Arrange/act
98+
var result = RewriteCssCommand.AddScopeToSelectors(@"
99+
a > ::deep b { color: red; }
100+
c ::deep > d { color: blue; }
101+
", "TestScope");
102+
103+
// Assert
104+
Assert.Equal(@"
105+
a[TestScope] > b { color: red; }
106+
c[TestScope] > d { color: blue; }
107+
", result);
108+
}
109+
110+
[Fact]
111+
public void RespectsDeepCombinatorWithAdjacentSibling()
112+
{
113+
// Arrange/act
114+
var result = RewriteCssCommand.AddScopeToSelectors(@"
115+
a + ::deep b { color: red; }
116+
c ::deep + d { color: blue; }
117+
", "TestScope");
118+
119+
// Assert
120+
Assert.Equal(@"
121+
a[TestScope] + b { color: red; }
122+
c[TestScope] + d { color: blue; }
123+
", result);
124+
}
125+
126+
[Fact]
127+
public void RespectsDeepCombinatorWithGeneralSibling()
128+
{
129+
// Arrange/act
130+
var result = RewriteCssCommand.AddScopeToSelectors(@"
131+
a ~ ::deep b { color: red; }
132+
c ::deep ~ d { color: blue; }
133+
", "TestScope");
134+
135+
// Assert
136+
Assert.Equal(@"
137+
a[TestScope] ~ b { color: red; }
138+
c[TestScope] ~ d { color: blue; }
139+
", result);
140+
}
141+
94142
[Fact]
95143
public void IgnoresMultipleDeepCombinators()
96144
{

0 commit comments

Comments
 (0)