Skip to content

CSS scoping deep combinators #24289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 24, 2020
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
86 changes: 61 additions & 25 deletions src/Razor/Microsoft.AspNetCore.Razor.Tools/src/RewriteCssCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand Down Expand Up @@ -74,26 +74,27 @@ public static string AddScopeToSelectors(string inputText, string cssScope)
var resultBuilder = new StringBuilder();
var previousInsertionPosition = 0;

var scopeInsertionPositionsVisitor = new FindScopeInsertionPositionsVisitor(stylesheet);
var scopeInsertionPositionsVisitor = new FindScopeInsertionEdits(stylesheet);
scopeInsertionPositionsVisitor.Visit();
foreach (var (currentInsertionPosition, insertionType) in scopeInsertionPositionsVisitor.InsertionPositions)
foreach (var edit in scopeInsertionPositionsVisitor.Edits)
{
resultBuilder.Append(inputText.Substring(previousInsertionPosition, currentInsertionPosition - previousInsertionPosition));

switch (insertionType)
resultBuilder.Append(inputText.Substring(previousInsertionPosition, edit.Position - previousInsertionPosition));
previousInsertionPosition = edit.Position;

switch (edit)
{
case ScopeInsertionType.Selector:
case InsertSelectorScopeEdit _:
resultBuilder.AppendFormat("[{0}]", cssScope);
break;
case ScopeInsertionType.KeyframesName:
case InsertKeyframesNameScopeEdit _:
resultBuilder.AppendFormat("-{0}", cssScope);
break;
case DeleteContentEdit deleteContentEdit:
previousInsertionPosition += deleteContentEdit.DeleteLength;
break;
default:
throw new NotImplementedException($"Unknown insertion type: '{insertionType}'");
throw new NotImplementedException($"Unknown edit type: '{edit}'");
}


previousInsertionPosition = currentInsertionPosition;
}

resultBuilder.Append(inputText.Substring(previousInsertionPosition));
Expand All @@ -118,19 +119,13 @@ private static bool TryFindKeyframesIdentifier(AtDirective atDirective, out Pars
return false;
}

private enum ScopeInsertionType
private class FindScopeInsertionEdits : Visitor
{
Selector,
KeyframesName,
}

private class FindScopeInsertionPositionsVisitor : Visitor
{
public List<(int, ScopeInsertionType)> InsertionPositions { get; } = new List<(int, ScopeInsertionType)>();
public List<CssEdit> Edits { get; } = new List<CssEdit>();

private readonly HashSet<string> _keyframeIdentifiers;

public FindScopeInsertionPositionsVisitor(ComplexItem root) : base(root)
public FindScopeInsertionEdits(ComplexItem root) : base(root)
{
// Before we start, we need to know the full set of keyframe names declared in this document
var keyframesIdentifiersVisitor = new FindKeyframesIdentifiersVisitor(root);
Expand All @@ -146,19 +141,42 @@ protected override void VisitSelector(Selector selector)
// ".first child," containing two simple selectors: ".first" and "child"
// ".second", containing one simple selector: ".second"
// Our goal is to insert immediately after the final simple selector within each selector
var lastSimpleSelector = selector.Children.OfType<SimpleSelector>().LastOrDefault();

// 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 lastSimpleSelector = allSimpleSelectors.TakeWhile(s => s != firstDeepCombinator).LastOrDefault();
if (lastSimpleSelector != null)
{
InsertionPositions.Add((lastSimpleSelector.AfterEnd, ScopeInsertionType.Selector));
Edits.Add(new InsertSelectorScopeEdit { Position = lastSimpleSelector.AfterEnd });
}
else if (firstDeepCombinator != null)
{
// For a leading deep combinator, we want to insert the scope attribute at the start
// Otherwise the result would be a CSS rule that isn't scoped at all
Edits.Add(new InsertSelectorScopeEdit { Position = firstDeepCombinator.Start });
}

// Also remove the deep combinator if we matched one
if (firstDeepCombinator != null)
{
Edits.Add(new DeleteContentEdit { Position = firstDeepCombinator.Start, DeleteLength = firstDeepCombinator.Length });
}
}

private static bool IsDeepCombinator(string simpleSelectorText)
{
return string.Equals(simpleSelectorText, "::deep", StringComparison.Ordinal);
}

protected override void VisitAtDirective(AtDirective item)
{
// Whenever we see "@keyframes something { ... }", we want to insert right after "something"
if (TryFindKeyframesIdentifier(item, out var identifier))
{
InsertionPositions.Add((identifier.AfterEnd, ScopeInsertionType.KeyframesName));
Edits.Add(new InsertKeyframesNameScopeEdit { Position = identifier.AfterEnd });
}
else
{
Expand All @@ -183,7 +201,7 @@ protected override void VisitDeclaration(Declaration item)
.Where(x => x.TokenType == CssTokenType.Identifier && _keyframeIdentifiers.Contains(x.Text));
foreach (var token in animationNameTokens)
{
InsertionPositions.Add((token.AfterEnd, ScopeInsertionType.KeyframesName));
Edits.Add(new InsertKeyframesNameScopeEdit { Position = token.AfterEnd });
}
break;
default:
Expand Down Expand Up @@ -273,5 +291,23 @@ private void VisitDescendants(ComplexItem container)
}
}
}

private abstract class CssEdit
{
public int Position { get; set; }
}

private class InsertSelectorScopeEdit : CssEdit
{
}

private class InsertKeyframesNameScopeEdit : CssEdit
{
}

private class DeleteContentEdit : CssEdit
{
public int DeleteLength { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Xunit;
Expand Down Expand Up @@ -75,6 +75,54 @@ public void HandlesSpacesAndCommentsWithinSelectors()
", result);
}

[Fact]
public void RespectsDeepCombinator()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will be interesting to have a case with multiple instances of ::deep, something like .a ::deep .b ::deep .c

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test

{
// Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@"
.first ::deep .second { color: red; }
a ::deep b, c ::deep d { color: blue; }
", "TestScope");

// Assert
Assert.Equal(@"
.first[TestScope] .second { color: red; }
a[TestScope] b, c[TestScope] d { color: blue; }
", result);
}

[Fact]
public void IgnoresMultipleDeepCombinators()
{
// Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@"
.first ::deep .second ::deep .third { color:red; }
", "TestScope");

// Assert
Assert.Equal(@"
.first[TestScope] .second ::deep .third { color:red; }
", result);
}

[Fact]
public void RespectsDeepCombinatorWithSpacesAndComments()
{
// Arrange/act
var result = RewriteCssCommand.AddScopeToSelectors(@"
.a .b /* comment ::deep 1 */ ::deep /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; }
::deep * { color: blue; } /* Leading deep combinator */
another ::deep { color: green } /* Trailing deep combinator */
", "TestScope");

// Assert
Assert.Equal(@"
.a .b[TestScope] /* comment ::deep 1 */ /* comment ::deep 2 */ .c /* ::deep */ .d { color: red; }
[TestScope] * { color: blue; } /* Leading deep combinator */
another[TestScope] { color: green } /* Trailing deep combinator */
", result);
}

[Fact]
public void HandlesAtBlocks()
{
Expand Down
16 changes: 9 additions & 7 deletions src/Razor/Microsoft.NET.Sdk.Razor/src/ComputeCssScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,19 @@ private string GenerateScope(string targetName, string relativePath)
return builder.ToString();
}

private string ToBase36(byte[] hash)
private static string ToBase36(byte[] hash)
{
var builder = new StringBuilder();
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
var dividend = new BigInteger(hash.AsSpan().Slice(0,8).ToArray());
while (dividend > 36)
const string chars = "0123456789abcdefghijklmnopqrstuvwxyz";

var result = new char[10];
var dividend = BigInteger.Abs(new BigInteger(hash.AsSpan().Slice(0, 9).ToArray()));
for (var i = 0; i < 10; i++)
{
dividend = BigInteger.DivRem(dividend, 36, out var remainder);
builder.Insert(0, chars[Math.Abs(((int)remainder))]);
result[i] = chars[(int)remainder];
}
return builder.ToString();

return new string(result);
}
}
}