Support pseudoelements in CSS scoping (#25270)
* Support pseudoelements in CSS scoping. Fixes #25268 * CR: More test cases * Another pseudoelement case * Case insensitivity for single-colon pseudoelements Not that anybody should ever want to do this * Avoid an allocation
This commit is contained in:
parent
ebaaa0f01d
commit
d793f473c4
|
|
@ -187,7 +187,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
var lastSimpleSelector = allSimpleSelectors.TakeWhile(s => s != firstDeepCombinator).LastOrDefault();
|
||||
if (lastSimpleSelector != null)
|
||||
{
|
||||
Edits.Add(new InsertSelectorScopeEdit { Position = FindPositionBeforeTrailingCombinator(lastSimpleSelector) });
|
||||
Edits.Add(new InsertSelectorScopeEdit { Position = FindPositionToInsertInSelector(lastSimpleSelector) });
|
||||
}
|
||||
else if (firstDeepCombinator != null)
|
||||
{
|
||||
|
|
@ -203,30 +203,62 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
}
|
||||
}
|
||||
|
||||
private int FindPositionBeforeTrailingCombinator(SimpleSelector lastSimpleSelector)
|
||||
private int FindPositionToInsertInSelector(SimpleSelector lastSimpleSelector)
|
||||
{
|
||||
// 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)
|
||||
var children = lastSimpleSelector.Children;
|
||||
for (var i = 0; i < children.Count; i++)
|
||||
{
|
||||
case '>':
|
||||
case '+':
|
||||
case '~':
|
||||
var trailingCombinatorMatch = _trailingCombinatorRegex.Match(text);
|
||||
if (trailingCombinatorMatch.Success)
|
||||
{
|
||||
var trailingCombinatorLength = trailingCombinatorMatch.Length;
|
||||
return lastSimpleSelector.AfterEnd - trailingCombinatorLength;
|
||||
}
|
||||
break;
|
||||
switch (children[i])
|
||||
{
|
||||
// Selectors like "a > ::deep b" get parsed as [[a][>]][::deep][b], and we want to
|
||||
// insert right after the "a". So if we're processing a SimpleSelector like [[a][>]],
|
||||
// consider the ">" to signal the "insert before" position.
|
||||
case TokenItem t when IsTrailingCombinator(t.TokenType):
|
||||
|
||||
// Similarly selectors like "a::before" get parsed as [[a][::before]], and we want to
|
||||
// insert right after the "a". So if we're processing a SimpleSelector like [[a][::before]],
|
||||
// consider the pseudoelement to signal the "insert before" position.
|
||||
case PseudoElementSelector:
|
||||
case PseudoElementFunctionSelector:
|
||||
case PseudoClassSelector s when IsSingleColonPseudoElement(s):
|
||||
// Insert after the previous token if there is one, otherwise before the whole thing
|
||||
return i > 0 ? children[i - 1].AfterEnd : lastSimpleSelector.Start;
|
||||
}
|
||||
}
|
||||
|
||||
// Since we didn't find any children that signal the insert-before position,
|
||||
// insert after the whole thing
|
||||
return lastSimpleSelector.AfterEnd;
|
||||
}
|
||||
|
||||
private static bool IsSingleColonPseudoElement(PseudoClassSelector selector)
|
||||
{
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
|
||||
// Normally, pseudoelements require a double-colon prefix. However the following "original set"
|
||||
// of pseudoelements also support single-colon prefixes for back-compatibility with older versions
|
||||
// of the W3C spec. Our CSS parser sees them as pseudoselectors rather than pseudoelements, so
|
||||
// we have to special-case them. The single-colon option doesn't exist for other more modern
|
||||
// pseudoelements.
|
||||
var selectorText = selector.Text;
|
||||
return string.Equals(selectorText, ":after", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(selectorText, ":before", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(selectorText, ":first-letter", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(selectorText, ":first-line", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsTrailingCombinator(CssTokenType tokenType)
|
||||
{
|
||||
switch (tokenType)
|
||||
{
|
||||
case CssTokenType.Plus:
|
||||
case CssTokenType.Tilde:
|
||||
case CssTokenType.Greater:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void VisitAtDirective(AtDirective item)
|
||||
{
|
||||
// Whenever we see "@keyframes something { ... }", we want to insert right after "something"
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
|
||||
.first, .second { color: red; }
|
||||
.third { color: blue; }
|
||||
:root { color: green; }
|
||||
* { color: white; }
|
||||
#some-id { color: yellow; }
|
||||
", "TestScope", out var diagnostics);
|
||||
|
||||
// Assert
|
||||
|
|
@ -48,6 +51,9 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
Assert.Equal(@"
|
||||
.first[TestScope], .second[TestScope] { color: red; }
|
||||
.third[TestScope] { color: blue; }
|
||||
:root[TestScope] { color: green; }
|
||||
*[TestScope] { color: white; }
|
||||
#some-id[TestScope] { color: yellow; }
|
||||
", result);
|
||||
}
|
||||
|
||||
|
|
@ -81,6 +87,83 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
|||
", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesPseudoClasses()
|
||||
{
|
||||
// Arrange/act
|
||||
var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
|
||||
a:fake-pseudo-class { color: red; }
|
||||
a:focus b:hover { color: green; }
|
||||
tr:nth-child(4n + 1) { color: blue; }
|
||||
a:has(b > c) { color: yellow; }
|
||||
a:last-child > ::deep b { color: pink; }
|
||||
a:not(#something) { color: purple; }
|
||||
", "TestScope", out var diagnostics);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(diagnostics);
|
||||
Assert.Equal(@"
|
||||
a:fake-pseudo-class[TestScope] { color: red; }
|
||||
a:focus b:hover[TestScope] { color: green; }
|
||||
tr:nth-child(4n + 1)[TestScope] { color: blue; }
|
||||
a:has(b > c)[TestScope] { color: yellow; }
|
||||
a:last-child[TestScope] > b { color: pink; }
|
||||
a:not(#something)[TestScope] { color: purple; }
|
||||
", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesPseudoElements()
|
||||
{
|
||||
// Arrange/act
|
||||
var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
|
||||
a::before { content: ""✋""; }
|
||||
a::after::placeholder { content: ""🐯""; }
|
||||
custom-element::part(foo) { content: ""🤷""; }
|
||||
a::before > ::deep another { content: ""👞""; }
|
||||
a::fake-PsEuDo-element { content: ""🐔""; }
|
||||
::selection { content: ""😾""; }
|
||||
other, ::selection { content: ""👂""; }
|
||||
", "TestScope", out var diagnostics);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(diagnostics);
|
||||
Assert.Equal(@"
|
||||
a[TestScope]::before { content: ""✋""; }
|
||||
a[TestScope]::after::placeholder { content: ""🐯""; }
|
||||
custom-element[TestScope]::part(foo) { content: ""🤷""; }
|
||||
a[TestScope]::before > another { content: ""👞""; }
|
||||
a[TestScope]::fake-PsEuDo-element { content: ""🐔""; }
|
||||
[TestScope]::selection { content: ""😾""; }
|
||||
other[TestScope], [TestScope]::selection { content: ""👂""; }
|
||||
", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HandlesSingleColonPseudoElements()
|
||||
{
|
||||
// Arrange/act
|
||||
var result = RewriteCssCommand.AddScopeToSelectors("file.css", @"
|
||||
a:after { content: ""x""; }
|
||||
a:before { content: ""x""; }
|
||||
a:first-letter { content: ""x""; }
|
||||
a:first-line { content: ""x""; }
|
||||
a:AFTER { content: ""x""; }
|
||||
a:not(something):before { content: ""x""; }
|
||||
", "TestScope", out var diagnostics);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(diagnostics);
|
||||
Assert.Equal(@"
|
||||
a[TestScope]:after { content: ""x""; }
|
||||
a[TestScope]:before { content: ""x""; }
|
||||
a[TestScope]:first-letter { content: ""x""; }
|
||||
a[TestScope]:first-line { content: ""x""; }
|
||||
a[TestScope]:AFTER { content: ""x""; }
|
||||
a:not(something)[TestScope]:before { content: ""x""; }
|
||||
", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RespectsDeepCombinator()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue