Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ protected override void VisitSelector(Selector selector)
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)
{
Expand All @@ -203,30 +203,62 @@ protected override void VisitSelector(Selector selector)
}
}

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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,19 @@ public void HandlesMultipleSelectors()
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
Assert.Empty(diagnostics);
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);
}

Expand Down Expand Up @@ -81,6 +87,83 @@ public void HandlesSpacesAndCommentsWithinSelectors()
", 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()
{
Expand Down