CSS isolation rewriter tool (#23657)
This commit is contained in:
parent
b8c5193562
commit
c5ba43f011
|
|
@ -21,6 +21,7 @@ and are generated based on the last package release.
|
||||||
|
|
||||||
<ItemGroup Label=".NET team dependencies">
|
<ItemGroup Label=".NET team dependencies">
|
||||||
<LatestPackageReference Include="Microsoft.Azure.SignalR" Version="$(MicrosoftAzureSignalRPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.Azure.SignalR" Version="$(MicrosoftAzureSignalRPackageVersion)" />
|
||||||
|
<LatestPackageReference Include="Microsoft.Css.Parser" Version="$(MicrosoftCssParserPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.CodeAnalysis.Common" Version="$(MicrosoftCodeAnalysisCommonPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.CodeAnalysis.Common" Version="$(MicrosoftCodeAnalysisCommonPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion)" />
|
||||||
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpPackageVersion)" />
|
<LatestPackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpPackageVersion)" />
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@
|
||||||
<MicrosoftCodeAnalysisCommonPackageVersion>3.7.0-4.20351.7</MicrosoftCodeAnalysisCommonPackageVersion>
|
<MicrosoftCodeAnalysisCommonPackageVersion>3.7.0-4.20351.7</MicrosoftCodeAnalysisCommonPackageVersion>
|
||||||
<MicrosoftCodeAnalysisCSharpPackageVersion>3.7.0-4.20351.7</MicrosoftCodeAnalysisCSharpPackageVersion>
|
<MicrosoftCodeAnalysisCSharpPackageVersion>3.7.0-4.20351.7</MicrosoftCodeAnalysisCSharpPackageVersion>
|
||||||
<MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>3.7.0-4.20351.7</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>
|
<MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>3.7.0-4.20351.7</MicrosoftCodeAnalysisCSharpWorkspacesPackageVersion>
|
||||||
|
<MicrosoftCssParserPackageVersion>1.0.0-20200708.1</MicrosoftCssParserPackageVersion>
|
||||||
<MicrosoftIdentityModelClientsActiveDirectoryPackageVersion>3.19.8</MicrosoftIdentityModelClientsActiveDirectoryPackageVersion>
|
<MicrosoftIdentityModelClientsActiveDirectoryPackageVersion>3.19.8</MicrosoftIdentityModelClientsActiveDirectoryPackageVersion>
|
||||||
<MicrosoftIdentityModelLoggingPackageVersion>5.5.0</MicrosoftIdentityModelLoggingPackageVersion>
|
<MicrosoftIdentityModelLoggingPackageVersion>5.5.0</MicrosoftIdentityModelLoggingPackageVersion>
|
||||||
<MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion>5.5.0</MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion>
|
<MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion>5.5.0</MicrosoftIdentityModelProtocolsOpenIdConnectPackageVersion>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
Commands.Add(new DiscoverCommand(this));
|
Commands.Add(new DiscoverCommand(this));
|
||||||
Commands.Add(new GenerateCommand(this));
|
Commands.Add(new GenerateCommand(this));
|
||||||
Commands.Add(new BrotliCompressCommand(this));
|
Commands.Add(new BrotliCompressCommand(this));
|
||||||
|
Commands.Add(new RewriteCssCommand(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public CancellationToken CancellationToken { get; }
|
public CancellationToken CancellationToken { get; }
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Newtonsoft.Json" Version="$(Razor_NewtonsoftJsonPackageVersion)" />
|
<Reference Include="Newtonsoft.Json" Version="$(Razor_NewtonsoftJsonPackageVersion)" />
|
||||||
|
<Reference Include="Microsoft.Css.Parser" Version="$(MicrosoftCssParserPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,277 @@
|
||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Css.Parser.Parser;
|
||||||
|
using Microsoft.Css.Parser.Tokens;
|
||||||
|
using Microsoft.Css.Parser.TreeItems;
|
||||||
|
using Microsoft.Css.Parser.TreeItems.AtDirectives;
|
||||||
|
using Microsoft.Css.Parser.TreeItems.Selectors;
|
||||||
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal class RewriteCssCommand : CommandBase
|
||||||
|
{
|
||||||
|
public RewriteCssCommand(Application parent)
|
||||||
|
: base(parent, "rewritecss")
|
||||||
|
{
|
||||||
|
Sources = Option("-s", "Files to rewrite", CommandOptionType.MultipleValue);
|
||||||
|
Outputs = Option("-o", "Output file paths", CommandOptionType.MultipleValue);
|
||||||
|
CssScopes = Option("-c", "CSS scope identifiers", CommandOptionType.MultipleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandOption Sources { get; }
|
||||||
|
|
||||||
|
public CommandOption Outputs { get; }
|
||||||
|
|
||||||
|
public CommandOption CssScopes { get; }
|
||||||
|
|
||||||
|
protected override bool ValidateArguments()
|
||||||
|
{
|
||||||
|
if (Sources.Values.Count != Outputs.Values.Count)
|
||||||
|
{
|
||||||
|
Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {Outputs.Description} has {Outputs.Values.Count} values.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sources.Values.Count != CssScopes.Values.Count)
|
||||||
|
{
|
||||||
|
Error.WriteLine($"{Sources.Description} has {Sources.Values.Count}, but {CssScopes.Description} has {CssScopes.Values.Count} values.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Task<int> ExecuteCoreAsync()
|
||||||
|
{
|
||||||
|
Parallel.For(0, Sources.Values.Count, i =>
|
||||||
|
{
|
||||||
|
var source = Sources.Values[i];
|
||||||
|
var output = Outputs.Values[i];
|
||||||
|
var cssScope = CssScopes.Values[i];
|
||||||
|
|
||||||
|
var inputText = File.ReadAllText(source);
|
||||||
|
var rewrittenCss = AddScopeToSelectors(inputText, cssScope);
|
||||||
|
File.WriteAllText(output, rewrittenCss);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.FromResult(ExitCodeSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public for tests
|
||||||
|
public static string AddScopeToSelectors(string inputText, string cssScope)
|
||||||
|
{
|
||||||
|
var cssParser = new DefaultParserFactory().CreateParser();
|
||||||
|
var stylesheet = cssParser.Parse(inputText, insertComments: false);
|
||||||
|
|
||||||
|
var resultBuilder = new StringBuilder();
|
||||||
|
var previousInsertionPosition = 0;
|
||||||
|
|
||||||
|
var scopeInsertionPositionsVisitor = new FindScopeInsertionPositionsVisitor(stylesheet);
|
||||||
|
scopeInsertionPositionsVisitor.Visit();
|
||||||
|
foreach (var (currentInsertionPosition, insertionType) in scopeInsertionPositionsVisitor.InsertionPositions)
|
||||||
|
{
|
||||||
|
resultBuilder.Append(inputText.Substring(previousInsertionPosition, currentInsertionPosition - previousInsertionPosition));
|
||||||
|
|
||||||
|
switch (insertionType)
|
||||||
|
{
|
||||||
|
case ScopeInsertionType.Selector:
|
||||||
|
resultBuilder.AppendFormat("[{0}]", cssScope);
|
||||||
|
break;
|
||||||
|
case ScopeInsertionType.KeyframesName:
|
||||||
|
resultBuilder.AppendFormat("-{0}", cssScope);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException($"Unknown insertion type: '{insertionType}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
previousInsertionPosition = currentInsertionPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBuilder.Append(inputText.Substring(previousInsertionPosition));
|
||||||
|
|
||||||
|
return resultBuilder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindKeyframesIdentifier(AtDirective atDirective, out ParseItem identifier)
|
||||||
|
{
|
||||||
|
var keyword = atDirective.Keyword;
|
||||||
|
if (string.Equals(keyword?.Text, "keyframes", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var nextSiblingText = keyword.NextSibling?.Text;
|
||||||
|
if (!string.IsNullOrEmpty(nextSiblingText))
|
||||||
|
{
|
||||||
|
identifier = keyword.NextSibling;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
identifier = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ScopeInsertionType
|
||||||
|
{
|
||||||
|
Selector,
|
||||||
|
KeyframesName,
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FindScopeInsertionPositionsVisitor : Visitor
|
||||||
|
{
|
||||||
|
public List<(int, ScopeInsertionType)> InsertionPositions { get; } = new List<(int, ScopeInsertionType)>();
|
||||||
|
|
||||||
|
private readonly HashSet<string> _keyframeIdentifiers;
|
||||||
|
|
||||||
|
public FindScopeInsertionPositionsVisitor(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);
|
||||||
|
keyframesIdentifiersVisitor.Visit();
|
||||||
|
_keyframeIdentifiers = keyframesIdentifiersVisitor.KeyframesIdentifiers
|
||||||
|
.Select(x => x.Text)
|
||||||
|
.ToHashSet(StringComparer.Ordinal); // Keyframe names are case-sensitive
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void VisitSelector(Selector selector)
|
||||||
|
{
|
||||||
|
// For a ruleset like ".first child, .second { ... }", we'll see two selectors:
|
||||||
|
// ".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 (lastSimpleSelector != null)
|
||||||
|
{
|
||||||
|
InsertionPositions.Add((lastSimpleSelector.AfterEnd, ScopeInsertionType.Selector));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
VisitDefault(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void VisitDeclaration(Declaration item)
|
||||||
|
{
|
||||||
|
switch (item.PropertyNameText)
|
||||||
|
{
|
||||||
|
case "animation":
|
||||||
|
case "animation-name":
|
||||||
|
// The first two tokens are <propertyname> and <colon> (otherwise we wouldn't be here).
|
||||||
|
// After that, any of the subsequent tokens might be the animation name.
|
||||||
|
// Unfortunately the rules for determining which token is the animation name are very
|
||||||
|
// complex - https://developer.mozilla.org/en-US/docs/Web/CSS/animation#Syntax
|
||||||
|
// Fortunately we only want to rewrite animation names that are explicitly declared in
|
||||||
|
// the same document (we don't want to add scopes to references to global keyframes)
|
||||||
|
// so it's sufficient just to match known animation names.
|
||||||
|
var animationNameTokens = item.Children.Skip(2).OfType<TokenItem>()
|
||||||
|
.Where(x => x.TokenType == CssTokenType.Identifier && _keyframeIdentifiers.Contains(x.Text));
|
||||||
|
foreach (var token in animationNameTokens)
|
||||||
|
{
|
||||||
|
InsertionPositions.Add((token.AfterEnd, ScopeInsertionType.KeyframesName));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// We don't need to do anything else with other declaration types
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FindKeyframesIdentifiersVisitor : Visitor
|
||||||
|
{
|
||||||
|
public FindKeyframesIdentifiersVisitor(ComplexItem root) : base(root)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ParseItem> KeyframesIdentifiers { get; } = new List<ParseItem>();
|
||||||
|
|
||||||
|
protected override void VisitAtDirective(AtDirective item)
|
||||||
|
{
|
||||||
|
if (TryFindKeyframesIdentifier(item, out var identifier))
|
||||||
|
{
|
||||||
|
KeyframesIdentifiers.Add(identifier);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
VisitDefault(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Visitor
|
||||||
|
{
|
||||||
|
private readonly ComplexItem _root;
|
||||||
|
|
||||||
|
public Visitor(ComplexItem root)
|
||||||
|
{
|
||||||
|
_root = root ?? throw new ArgumentNullException(nameof(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Visit()
|
||||||
|
{
|
||||||
|
VisitDefault(_root);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void VisitSelector(Selector item)
|
||||||
|
{
|
||||||
|
VisitDefault(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void VisitAtDirective(AtDirective item)
|
||||||
|
{
|
||||||
|
VisitDefault(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void VisitDeclaration(Declaration item)
|
||||||
|
{
|
||||||
|
VisitDefault(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void VisitDefault(ParseItem item)
|
||||||
|
{
|
||||||
|
if (item is ComplexItem complexItem)
|
||||||
|
{
|
||||||
|
VisitDescendants(complexItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VisitDescendants(ComplexItem container)
|
||||||
|
{
|
||||||
|
foreach (var child in container.Children)
|
||||||
|
{
|
||||||
|
switch (child)
|
||||||
|
{
|
||||||
|
case Selector selector:
|
||||||
|
VisitSelector(selector);
|
||||||
|
break;
|
||||||
|
case AtDirective atDirective:
|
||||||
|
VisitAtDirective(atDirective);
|
||||||
|
break;
|
||||||
|
case Declaration declaration:
|
||||||
|
VisitDeclaration(declaration);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
VisitDefault(child);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
public class RewriteCssCommandTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void HandlesEmptyFile()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(string.Empty, "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(string.Empty, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddsScopeAfterSelector()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.myclass { color: red; }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.myclass[TestScope] { color: red; }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HandlesMultipleSelectors()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.first, .second { color: red; }
|
||||||
|
.third { color: blue; }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.first[TestScope], .second[TestScope] { color: red; }
|
||||||
|
.third[TestScope] { color: blue; }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HandlesComplexSelectors()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.first div > li, body .second:not(.fancy)[attr~=whatever] { color: red; }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.first div > li[TestScope], body .second:not(.fancy)[attr~=whatever][TestScope] { color: red; }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HandlesSpacesAndCommentsWithinSelectors()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.first /* space at end {} */ div , .myclass /* comment at end */ { color: red; }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.first /* space at end {} */ div[TestScope] , .myclass[TestScope] /* comment at end */ { color: red; }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HandlesAtBlocks()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.myclass { color: red; }
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.another .thing {
|
||||||
|
content: 'This should not be a selector: .fake-selector { color: red }'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.myclass[TestScope] { color: red; }
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.another .thing[TestScope] {
|
||||||
|
content: 'This should not be a selector: .fake-selector { color: red }'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AddsScopeToKeyframeNames()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
@keyframes my-animation { /* whatever */ }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
@keyframes my-animation-TestScope { /* whatever */ }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RewritesAnimationNamesWhenMatchingKnownKeyframes()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.myclass {
|
||||||
|
color: red;
|
||||||
|
animation: /* ignore comment */ my-animation 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.another-thing { animation-name: different-animation; }
|
||||||
|
|
||||||
|
h1 { animation: unknown-animation; } /* Should not be scoped */
|
||||||
|
|
||||||
|
@keyframes my-animation { /* whatever */ }
|
||||||
|
@keyframes different-animation { /* whatever */ }
|
||||||
|
@keyframes unused-animation { /* whatever */ }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.myclass[TestScope] {
|
||||||
|
color: red;
|
||||||
|
animation: /* ignore comment */ my-animation-TestScope 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.another-thing[TestScope] { animation-name: different-animation-TestScope; }
|
||||||
|
|
||||||
|
h1[TestScope] { animation: unknown-animation; } /* Should not be scoped */
|
||||||
|
|
||||||
|
@keyframes my-animation-TestScope { /* whatever */ }
|
||||||
|
@keyframes different-animation-TestScope { /* whatever */ }
|
||||||
|
@keyframes unused-animation-TestScope { /* whatever */ }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RewritesMultipleAnimationNames()
|
||||||
|
{
|
||||||
|
// Arrange/act
|
||||||
|
var result = RewriteCssCommand.AddScopeToSelectors(@"
|
||||||
|
.myclass1 { animation-name: my-animation , different-animation }
|
||||||
|
.myclass2 { animation: 4s linear 0s alternate my-animation infinite, different-animation 0s }
|
||||||
|
@keyframes my-animation { }
|
||||||
|
@keyframes different-animation { }
|
||||||
|
", "TestScope");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(@"
|
||||||
|
.myclass1[TestScope] { animation-name: my-animation-TestScope , different-animation-TestScope }
|
||||||
|
.myclass2[TestScope] { animation: 4s linear 0s alternate my-animation-TestScope infinite, different-animation-TestScope 0s }
|
||||||
|
@keyframes my-animation-TestScope { }
|
||||||
|
@keyframes different-animation-TestScope { }
|
||||||
|
", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue