-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add legacy color function migrator (#260)
- Loading branch information
Showing
11 changed files
with
352 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Use of this source code is governed by an MIT-style | ||
// license that can be found in the LICENSE file or at | ||
// https://opensource.org/licenses/MIT. | ||
|
||
import 'package:sass_api/sass_api.dart'; | ||
import 'package:sass_migrator/src/migrators/module/reference_source.dart'; | ||
import 'package:source_span/source_span.dart'; | ||
|
||
import 'module/references.dart'; | ||
import '../migration_visitor.dart'; | ||
import '../migrator.dart'; | ||
import '../patch.dart'; | ||
import '../utils.dart'; | ||
|
||
/// Migrates off of legacy color functions. | ||
class ColorMigrator extends Migrator { | ||
final name = "color"; | ||
final description = "Migrates off of legacy color functions."; | ||
|
||
@override | ||
Map<Uri, String> migrateFile( | ||
ImportCache importCache, Stylesheet stylesheet, Importer importer) { | ||
var references = References(importCache, stylesheet, importer); | ||
var visitor = | ||
_ColorMigrationVisitor(references, importCache, migrateDependencies); | ||
var result = visitor.run(stylesheet, importer); | ||
missingDependencies.addAll(visitor.missingDependencies); | ||
return result; | ||
} | ||
} | ||
|
||
/// URL for the sass:color module. | ||
final _colorUrl = Uri(scheme: 'sass', path: 'color'); | ||
|
||
class _ColorMigrationVisitor extends MigrationVisitor { | ||
final References references; | ||
|
||
_ColorMigrationVisitor( | ||
this.references, super.importCache, super.migrateDependencies); | ||
|
||
/// The namespace of an existing `@use "sass:color"` rule in the current | ||
/// file, if any. | ||
String? _colorModuleNamespace; | ||
|
||
/// The set of all other namespaces already used in the current file. | ||
Set<String> _usedNamespaces = {}; | ||
|
||
@override | ||
void visitStylesheet(Stylesheet node) { | ||
var oldColorModuleNamespace = _colorModuleNamespace; | ||
var oldUsedNamespaces = _usedNamespaces; | ||
_colorModuleNamespace = null; | ||
_usedNamespaces = {}; | ||
// Check all the namespaces used by this file before visiting the | ||
// stylesheet, in case deprecated functions are called before all `@use` | ||
// rules. | ||
for (var useRule in node.uses) { | ||
if (_colorModuleNamespace != null || useRule.namespace == null) continue; | ||
if (useRule.url == _colorUrl) { | ||
_colorModuleNamespace = useRule.namespace; | ||
} else { | ||
_usedNamespaces.add(useRule.namespace!); | ||
} | ||
} | ||
super.visitStylesheet(node); | ||
_colorModuleNamespace = oldColorModuleNamespace; | ||
_usedNamespaces = oldUsedNamespaces; | ||
} | ||
|
||
@override | ||
void visitFunctionExpression(FunctionExpression node) { | ||
var source = references.sources[node]; | ||
if (source is! BuiltInSource || source.url != _colorUrl) return; | ||
switch (node.name) { | ||
case 'red' || 'green' || 'blue': | ||
_patchChannel(node, 'rgb'); | ||
case 'hue' || 'saturation' || 'lightness': | ||
_patchChannel(node, 'hsl'); | ||
case 'whiteness' || 'blackness': | ||
_patchChannel(node, 'hwb'); | ||
case 'alpha': | ||
_patchChannel(node); | ||
case 'adjust-hue': | ||
_patchAdjust(node, channel: 'hue', space: 'hsl'); | ||
case 'saturate' | ||
when node.arguments.named.length + node.arguments.positional.length != | ||
1: | ||
_patchAdjust(node, channel: 'saturation', space: 'hsl'); | ||
case 'desaturate': | ||
_patchAdjust(node, channel: 'saturation', negate: true, space: 'hsl'); | ||
case 'transparentize' || 'fade-out': | ||
_patchAdjust(node, channel: 'alpha', negate: true); | ||
case 'opacify' || 'fade-in': | ||
_patchAdjust(node, channel: 'alpha'); | ||
case 'lighten': | ||
_patchAdjust(node, channel: 'lightness', space: 'hsl'); | ||
case 'darken': | ||
_patchAdjust(node, channel: 'lightness', negate: true, space: 'hsl'); | ||
default: | ||
return; | ||
} | ||
if (node.namespace == null) { | ||
addPatch( | ||
patchBefore( | ||
node, '${_getOrAddColorModuleNamespace(node.span.file)}.'), | ||
beforeExisting: true); | ||
} | ||
} | ||
|
||
/// Returns the namespace used for the color module, adding a new `@use` rule | ||
/// if necessary. | ||
String _getOrAddColorModuleNamespace(SourceFile file) { | ||
if (_colorModuleNamespace == null) { | ||
_colorModuleNamespace = _chooseColorModuleNamespace(); | ||
var asClause = | ||
_colorModuleNamespace == 'color' ? '' : ' as $_colorModuleNamespace'; | ||
addPatch( | ||
Patch.insert(file.location(0), '@use "sass:color"$asClause;\n\n')); | ||
} | ||
return _colorModuleNamespace!; | ||
} | ||
|
||
/// Find an unused namespace for the sass:color module. | ||
String _chooseColorModuleNamespace() { | ||
if (!_usedNamespaces.contains('color')) return 'color'; | ||
if (!_usedNamespaces.contains('sass-color')) return 'sass-color'; | ||
var count = 2; | ||
var namespace = 'color$count'; | ||
while (_usedNamespaces.contains(namespace)) { | ||
namespace = 'color${++count}'; | ||
} | ||
return namespace; | ||
} | ||
|
||
/// Patches a deprecated channel function to use `color.channel` instead. | ||
void _patchChannel(FunctionExpression node, [String? colorSpace]) { | ||
addPatch(Patch(node.nameSpan, 'channel')); | ||
|
||
if (node.arguments.named.isEmpty) { | ||
addPatch(patchAfter( | ||
node.arguments.positional.last, | ||
", '${node.name}'" | ||
"${colorSpace == null ? '' : ', \$space: $colorSpace'}")); | ||
} else { | ||
addPatch(patchAfter( | ||
[...node.arguments.positional, ...node.arguments.named.values].last, | ||
", \$channel: '${node.name}'" | ||
"${colorSpace == null ? '' : ', \$space: $colorSpace'}")); | ||
} | ||
} | ||
|
||
/// Patches a deprecated adjustment function to use `color.adjust` instead. | ||
void _patchAdjust(FunctionExpression node, | ||
{required String channel, bool negate = false, String? space}) { | ||
addPatch(Patch(node.nameSpan, 'adjust')); | ||
switch (node.arguments) { | ||
case ArgumentInvocation(positional: [_, var adjustment]): | ||
addPatch(patchBefore(adjustment, '\$$channel: ${negate ? '-' : ''}')); | ||
if (negate && adjustment.needsParens) { | ||
addPatch(patchBefore(adjustment, '(')); | ||
addPatch(patchAfter(adjustment, ')')); | ||
} | ||
if (space != null) { | ||
addPatch(patchAfter(adjustment, ', \$space: $space')); | ||
} | ||
|
||
case ArgumentInvocation( | ||
named: {'amount': var adjustment} || {'degrees': var adjustment} | ||
): | ||
var start = adjustment.span.start.offset - 1; | ||
while (adjustment.span.file.getText(start, start + 1) != r'$') { | ||
start--; | ||
} | ||
var argNameSpan = adjustment.span.file | ||
.location(start + 1) | ||
.pointSpan() | ||
.extendIfMatches('amount') | ||
.extendIfMatches('degrees'); | ||
addPatch(Patch(argNameSpan, channel)); | ||
if (negate) { | ||
addPatch(patchBefore(adjustment, '-')); | ||
if (adjustment.needsParens) { | ||
addPatch(patchBefore(adjustment, '(')); | ||
addPatch(patchAfter(adjustment, ')')); | ||
} | ||
} | ||
if (space != null) { | ||
addPatch(patchAfter(adjustment, ', \$space: $space')); | ||
} | ||
|
||
default: | ||
warn(node.span.message('Cannot migrate unexpected arguments.')); | ||
} | ||
} | ||
} | ||
|
||
extension _NeedsParens on Expression { | ||
/// Returns true if this expression needs parentheses when it's negated. | ||
bool get needsParens => switch (this) { | ||
BinaryOperationExpression() || | ||
UnaryOperationExpression() || | ||
FunctionExpression() => | ||
true, | ||
_ => false, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
<==> input/entrypoint.scss | ||
a { | ||
b: red(gold); | ||
c: green(gold); | ||
d: blue(gold); | ||
e: hue(gold); | ||
f: saturation(gold); | ||
g: lightness(gold); | ||
h: adjust-hue(gold, 20deg); | ||
i: saturate(gold, 10%); | ||
j: desaturate(gold, 10%); | ||
k: transparentize(gold, 0.1); | ||
l: fade-out(gold, 0.1); | ||
m: opacify(gold, 0.1); | ||
n: fade-in(gold, 0.1); | ||
o: lighten(gold, 10%); | ||
p: darken(gold, 10%); | ||
} | ||
|
||
<==> output/entrypoint.scss | ||
@use "sass:color"; | ||
|
||
a { | ||
b: color.channel(gold, 'red', $space: rgb); | ||
c: color.channel(gold, 'green', $space: rgb); | ||
d: color.channel(gold, 'blue', $space: rgb); | ||
e: color.channel(gold, 'hue', $space: hsl); | ||
f: color.channel(gold, 'saturation', $space: hsl); | ||
g: color.channel(gold, 'lightness', $space: hsl); | ||
h: color.adjust(gold, $hue: 20deg, $space: hsl); | ||
i: color.adjust(gold, $saturation: 10%, $space: hsl); | ||
j: color.adjust(gold, $saturation: -10%, $space: hsl); | ||
k: color.adjust(gold, $alpha: -0.1); | ||
l: color.adjust(gold, $alpha: -0.1); | ||
m: color.adjust(gold, $alpha: 0.1); | ||
n: color.adjust(gold, $alpha: 0.1); | ||
o: color.adjust(gold, $lightness: 10%, $space: hsl); | ||
p: color.adjust(gold, $lightness: -10%, $space: hsl); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<==> input/entrypoint.scss | ||
@use "sass:color"; | ||
|
||
a { | ||
b: color.red(gold); | ||
c: color.green(gold); | ||
d: color.blue(gold); | ||
e: color.hue(gold); | ||
f: color.saturation(gold); | ||
g: color.lightness(gold); | ||
h: color.whiteness(gold); | ||
i: color.blackness(gold); | ||
j: color.alpha(gold); | ||
} | ||
|
||
<==> output/entrypoint.scss | ||
@use "sass:color"; | ||
|
||
a { | ||
b: color.channel(gold, 'red', $space: rgb); | ||
c: color.channel(gold, 'green', $space: rgb); | ||
d: color.channel(gold, 'blue', $space: rgb); | ||
e: color.channel(gold, 'hue', $space: hsl); | ||
f: color.channel(gold, 'saturation', $space: hsl); | ||
g: color.channel(gold, 'lightness', $space: hsl); | ||
h: color.channel(gold, 'whiteness', $space: hwb); | ||
i: color.channel(gold, 'blackness', $space: hwb); | ||
j: color.channel(gold, 'alpha'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<==> input/entrypoint.scss | ||
a { | ||
b: red($color: gold); | ||
c: adjust-hue(gold, $degrees: 20deg); | ||
d: saturate(gold, $amount: 10%); | ||
e: desaturate($color: gold, $amount: 10%); | ||
f: lighten($amount: 10%, $color: gold); | ||
} | ||
|
||
<==> output/entrypoint.scss | ||
@use "sass:color"; | ||
|
||
a { | ||
b: color.channel($color: gold, $channel: 'red', $space: rgb); | ||
c: color.adjust(gold, $hue: 20deg, $space: hsl); | ||
d: color.adjust(gold, $saturation: 10%, $space: hsl); | ||
e: color.adjust($color: gold, $saturation: -10%, $space: hsl); | ||
f: color.adjust($lightness: 10%, $space: hsl, $color: gold); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<==> input/entrypoint.scss | ||
@use "sass:math" as color; | ||
|
||
a { | ||
b: red(gold); | ||
c: green(gold); | ||
d: blue(gold); | ||
} | ||
|
||
<==> output/entrypoint.scss | ||
@use "sass:color" as sass-color; | ||
|
||
@use "sass:math" as color; | ||
|
||
a { | ||
b: sass-color.channel(gold, 'red', $space: rgb); | ||
c: sass-color.channel(gold, 'green', $space: rgb); | ||
d: sass-color.channel(gold, 'blue', $space: rgb); | ||
} |
Oops, something went wrong.