CSS scoping deep combinators (#24289)

* Support deep combinators with CSS scoping

* Update CSS scope computation
This commit is contained in:
Steve Sanderson 2020-07-24 22:51:30 +01:00 committed by GitHub
parent 088a6fa1ec
commit eb8764e80f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 119 additions and 33 deletions

View File

@ -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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -74,26 +74,27 @@ namespace Microsoft.AspNetCore.Razor.Tools
var resultBuilder = new StringBuilder(); var resultBuilder = new StringBuilder();
var previousInsertionPosition = 0; var previousInsertionPosition = 0;
var scopeInsertionPositionsVisitor = new FindScopeInsertionPositionsVisitor(stylesheet); var scopeInsertionPositionsVisitor = new FindScopeInsertionEdits(stylesheet);
scopeInsertionPositionsVisitor.Visit(); scopeInsertionPositionsVisitor.Visit();
foreach (var (currentInsertionPosition, insertionType) in scopeInsertionPositionsVisitor.InsertionPositions) foreach (var edit in scopeInsertionPositionsVisitor.Edits)
{ {
resultBuilder.Append(inputText.Substring(previousInsertionPosition, currentInsertionPosition - previousInsertionPosition)); resultBuilder.Append(inputText.Substring(previousInsertionPosition, edit.Position - previousInsertionPosition));
previousInsertionPosition = edit.Position;
switch (insertionType)
switch (edit)
{ {
case ScopeInsertionType.Selector: case InsertSelectorScopeEdit _:
resultBuilder.AppendFormat("[{0}]", cssScope); resultBuilder.AppendFormat("[{0}]", cssScope);
break; break;
case ScopeInsertionType.KeyframesName: case InsertKeyframesNameScopeEdit _:
resultBuilder.AppendFormat("-{0}", cssScope); resultBuilder.AppendFormat("-{0}", cssScope);
break; break;
case DeleteContentEdit deleteContentEdit:
previousInsertionPosition += deleteContentEdit.DeleteLength;
break;
default: default:
throw new NotImplementedException($"Unknown insertion type: '{insertionType}'"); throw new NotImplementedException($"Unknown edit type: '{edit}'");
} }
previousInsertionPosition = currentInsertionPosition;
} }
resultBuilder.Append(inputText.Substring(previousInsertionPosition)); resultBuilder.Append(inputText.Substring(previousInsertionPosition));
@ -118,19 +119,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
return false; return false;
} }
private enum ScopeInsertionType private class FindScopeInsertionEdits : Visitor
{ {
Selector, public List<CssEdit> Edits { get; } = new List<CssEdit>();
KeyframesName,
}
private class FindScopeInsertionPositionsVisitor : Visitor
{
public List<(int, ScopeInsertionType)> InsertionPositions { get; } = new List<(int, ScopeInsertionType)>();
private readonly HashSet<string> _keyframeIdentifiers; 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 // Before we start, we need to know the full set of keyframe names declared in this document
var keyframesIdentifiersVisitor = new FindKeyframesIdentifiersVisitor(root); var keyframesIdentifiersVisitor = new FindKeyframesIdentifiersVisitor(root);
@ -146,11 +141,34 @@ namespace Microsoft.AspNetCore.Razor.Tools
// ".first child," containing two simple selectors: ".first" and "child" // ".first child," containing two simple selectors: ".first" and "child"
// ".second", containing one simple selector: ".second" // ".second", containing one simple selector: ".second"
// Our goal is to insert immediately after the final simple selector within each selector // 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) 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) protected override void VisitAtDirective(AtDirective item)
@ -158,7 +176,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
// Whenever we see "@keyframes something { ... }", we want to insert right after "something" // Whenever we see "@keyframes something { ... }", we want to insert right after "something"
if (TryFindKeyframesIdentifier(item, out var identifier)) if (TryFindKeyframesIdentifier(item, out var identifier))
{ {
InsertionPositions.Add((identifier.AfterEnd, ScopeInsertionType.KeyframesName)); Edits.Add(new InsertKeyframesNameScopeEdit { Position = identifier.AfterEnd });
} }
else else
{ {
@ -183,7 +201,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
.Where(x => x.TokenType == CssTokenType.Identifier && _keyframeIdentifiers.Contains(x.Text)); .Where(x => x.TokenType == CssTokenType.Identifier && _keyframeIdentifiers.Contains(x.Text));
foreach (var token in animationNameTokens) foreach (var token in animationNameTokens)
{ {
InsertionPositions.Add((token.AfterEnd, ScopeInsertionType.KeyframesName)); Edits.Add(new InsertKeyframesNameScopeEdit { Position = token.AfterEnd });
} }
break; break;
default: default:
@ -273,5 +291,23 @@ namespace Microsoft.AspNetCore.Razor.Tools
} }
} }
} }
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; }
}
} }
} }

View File

@ -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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Xunit; using Xunit;
@ -75,6 +75,54 @@ namespace Microsoft.AspNetCore.Razor.Tools
", result); ", result);
} }
[Fact]
public void RespectsDeepCombinator()
{
// 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] [Fact]
public void HandlesAtBlocks() public void HandlesAtBlocks()
{ {

View File

@ -55,17 +55,19 @@ namespace Microsoft.AspNetCore.Razor.Tasks
return builder.ToString(); return builder.ToString();
} }
private string ToBase36(byte[] hash) private static string ToBase36(byte[] hash)
{ {
var builder = new StringBuilder(); const string chars = "0123456789abcdefghijklmnopqrstuvwxyz";
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
var dividend = new BigInteger(hash.AsSpan().Slice(0,8).ToArray()); var result = new char[10];
while (dividend > 36) 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); 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);
} }
} }
} }