Skip to content

Commit

Permalink
Merge pull request #895 from sass/forward-with
Browse files Browse the repository at this point in the history
Support @forward ... with
  • Loading branch information
nex3 authored Dec 20, 2019
2 parents 37534aa + 46be33e commit b54643d
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 108 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## 1.24.0

* Add an optional `with` clause to the `@forward` rule. This works like the
`@use` rule's `with` clause, except that `@forward ... with` can declare
variables as `!default` to allow downstream modules to reconfigure their
values.

* Support configuring modules through `@import` rules.

## 1.23.8
Expand Down
1 change: 1 addition & 0 deletions lib/src/ast/sass.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export 'sass/argument_declaration.dart';
export 'sass/argument_invocation.dart';
export 'sass/at_root_query.dart';
export 'sass/callable_invocation.dart';
export 'sass/configured_variable.dart';
export 'sass/expression.dart';
export 'sass/expression/binary_operation.dart';
export 'sass/expression/boolean.dart';
Expand Down
30 changes: 30 additions & 0 deletions lib/src/ast/sass/configured_variable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2019 Google Inc. 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:source_span/source_span.dart';

import 'expression.dart';
import 'node.dart';

/// A variable configured by a `with` clause in a `@use` or `@forward` rule.
class ConfiguredVariable implements SassNode {
/// The name of the variable being configured.
final String name;

/// The variable's value.
final Expression expression;

/// Whether the variable can be further configured by outer modules.
///
/// This is always `false` for `@use` rules.
final bool isGuarded;

final FileSpan span;

ConfiguredVariable(this.name, this.expression, this.span,
{bool guarded = false})
: isGuarded = guarded;

String toString() => "\$$name: $expression${isGuarded ? ' !default' : ''}";
}
28 changes: 22 additions & 6 deletions lib/src/ast/sass/statement/forward_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:collection/collection.dart';
import 'package:source_span/source_span.dart';

import '../../../visitor/interface/statement.dart';
import '../configured_variable.dart';
import '../expression/string.dart';
import '../statement.dart';

Expand Down Expand Up @@ -64,36 +65,46 @@ class ForwardRule implements Statement {
/// module, or `null` if member names are used as-is.
final String prefix;

/// A list of variable assignments used to configure the loaded modules.
final List<ConfiguredVariable> configuration;

final FileSpan span;

/// Creates a `@forward` rule that allows all members to be accessed.
ForwardRule(this.url, this.span, {this.prefix})
ForwardRule(this.url, this.span,
{this.prefix, Iterable<ConfiguredVariable> configuration})
: shownMixinsAndFunctions = null,
shownVariables = null,
hiddenMixinsAndFunctions = null,
hiddenVariables = null;
hiddenVariables = null,
configuration =
configuration == null ? const [] : List.unmodifiable(configuration);

/// Creates a `@forward` rule that allows only members included in
/// [shownMixinsAndFunctions] and [shownVariables] to be accessed.
ForwardRule.show(this.url, Iterable<String> shownMixinsAndFunctions,
Iterable<String> shownVariables, this.span,
{this.prefix})
{this.prefix, Iterable<ConfiguredVariable> configuration})
: shownMixinsAndFunctions =
UnmodifiableSetView(Set.of(shownMixinsAndFunctions)),
shownVariables = UnmodifiableSetView(Set.of(shownVariables)),
hiddenMixinsAndFunctions = null,
hiddenVariables = null;
hiddenVariables = null,
configuration =
configuration == null ? const [] : List.unmodifiable(configuration);

/// Creates a `@forward` rule that allows only members not included in
/// [hiddenMixinsAndFunctions] and [hiddenVariables] to be accessed.
ForwardRule.hide(this.url, Iterable<String> hiddenMixinsAndFunctions,
Iterable<String> hiddenVariables, this.span,
{this.prefix})
{this.prefix, Iterable<ConfiguredVariable> configuration})
: shownMixinsAndFunctions = null,
shownVariables = null,
hiddenMixinsAndFunctions =
UnmodifiableSetView(Set.of(hiddenMixinsAndFunctions)),
hiddenVariables = UnmodifiableSetView(Set.of(hiddenVariables));
hiddenVariables = UnmodifiableSetView(Set.of(hiddenVariables)),
configuration =
configuration == null ? const [] : List.unmodifiable(configuration);

T accept<T>(StatementVisitor<T> visitor) => visitor.visitForwardRule(this);

Expand All @@ -112,6 +123,11 @@ class ForwardRule implements Statement {
}

if (prefix != null) buffer.write(" as $prefix*");

if (configuration.isNotEmpty) {
buffer.write(" with (${configuration.join(", ")})");
}

buffer.write(";");
return buffer.toString();
}
Expand Down
27 changes: 15 additions & 12 deletions lib/src/ast/sass/statement/use_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
// https://opensource.org/licenses/MIT.

import 'package:source_span/source_span.dart';
import 'package:tuple/tuple.dart';

import '../../../logger.dart';
import '../../../parse/scss.dart';
import '../../../visitor/interface/statement.dart';
import '../expression.dart';
import '../configured_variable.dart';
import '../expression/string.dart';
import '../statement.dart';

Expand All @@ -23,15 +22,23 @@ class UseRule implements Statement {
/// can be accessed without a namespace.
final String namespace;

/// A map from variable names to their values and the spans for those
/// variables, used to configure the loaded modules.
final Map<String, Tuple2<Expression, FileSpan>> configuration;
/// A list of variable assignments used to configure the loaded modules.
final List<ConfiguredVariable> configuration;

final FileSpan span;

UseRule(this.url, this.namespace, this.span,
{Map<String, Tuple2<Expression, FileSpan>> configuration})
: configuration = Map.unmodifiable(configuration ?? const {});
{Iterable<ConfiguredVariable> configuration})
: configuration = configuration == null
? const []
: List.unmodifiable(configuration) {
for (var variable in this.configuration) {
if (variable.isGuarded) {
throw ArgumentError.value(variable, "configured variable",
"can't be guarded in a @use rule.");
}
}
}

/// Parses a `@use` rule from [contents].
///
Expand All @@ -54,11 +61,7 @@ class UseRule implements Statement {
}

if (configuration.isNotEmpty) {
buffer.write(" with (");
buffer.write(configuration.entries
.map((entry) => "\$${entry.key}: ${entry.value.item1}")
.join(", "));
buffer.write(")");
buffer.write(" with (${configuration.join(", ")})");
}

buffer.write(";");
Expand Down
5 changes: 0 additions & 5 deletions lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,4 @@ class Configuration {
}
return Configuration(newValues, isImplicit: isImplicit);
}

/// Creates a copy of this configuration.
Configuration clone() => isEmpty
? const Configuration.empty()
: Configuration({...values}, isImplicit: isImplicit);
}
42 changes: 31 additions & 11 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,8 @@ abstract class StylesheetParser extends Parser {
hiddenVariables = members.item2;
}

var configuration = _configuration(allowGuarded: true);

expectStatementSeparator("@forward rule");
var span = scanner.spanFrom(start);
if (!_isUseAllowed) {
Expand All @@ -971,13 +973,14 @@ abstract class StylesheetParser extends Parser {
if (shownMixinsAndFunctions != null) {
return ForwardRule.show(
url, shownMixinsAndFunctions, shownVariables, span,
prefix: prefix);
prefix: prefix, configuration: configuration);
} else if (hiddenMixinsAndFunctions != null) {
return ForwardRule.hide(
url, hiddenMixinsAndFunctions, hiddenVariables, span,
prefix: prefix);
prefix: prefix, configuration: configuration);
} else {
return ForwardRule(url, span, prefix: prefix);
return ForwardRule(url, span,
prefix: prefix, configuration: configuration);
}
}

Expand Down Expand Up @@ -1350,7 +1353,7 @@ relase. For details, see http://bit.ly/moz-document.

var namespace = _useNamespace(url, start);
whitespace();
var configuration = _useConfiguration();
var configuration = _configuration();

expectStatementSeparator("@use rule");

Expand Down Expand Up @@ -1381,14 +1384,18 @@ relase. For details, see http://bit.ly/moz-document.
}
}

/// Returns the map from variable names to expressions from a `@use` rule's
/// `with` clause.
/// Returns the list of configured variables from a `@use` or `@forward`
/// rule's `with` clause.
///
/// If `allowGuarded` is `true`, this will allow configured variable with the
/// `!default` flag.
///
/// Returns `null` if there is no `with` clause.
Map<String, Tuple2<Expression, FileSpan>> _useConfiguration() {
List<ConfiguredVariable> _configuration({bool allowGuarded = false}) {
if (!scanIdentifier("with")) return null;

var configuration = <String, Tuple2<Expression, FileSpan>>{};
var variableNames = <String>{};
var configuration = <ConfiguredVariable>[];
whitespace();
scanner.expectChar($lparen);

Expand All @@ -1401,12 +1408,25 @@ relase. For details, see http://bit.ly/moz-document.
scanner.expectChar($colon);
whitespace();
var expression = _expressionUntilComma();
var span = scanner.spanFrom(variableStart);

if (configuration.containsKey(name)) {
var guarded = false;
var flagStart = scanner.state;
if (allowGuarded && scanner.scanChar($exclamation)) {
var flag = identifier();
if (flag == 'default') {
guarded = true;
} else {
error("Invalid flag name.", scanner.spanFrom(flagStart));
}
}

var span = scanner.spanFrom(variableStart);
if (variableNames.contains(name)) {
error("The same variable may only be configured once.", span);
}
configuration[name] = Tuple2(expression, span);
variableNames.add(name);
configuration
.add(ConfiguredVariable(name, expression, span, guarded: guarded));

if (!scanner.scanChar($comma)) break;
whitespace();
Expand Down
5 changes: 3 additions & 2 deletions lib/src/util/merged_map_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import '../utils.dart';
/// key.
///
/// Unlike `CombinedMapView` from the `collection` package, this provides `O(1)`
/// index and `length` operations. It does so by imposing the additional
/// constraint that the underlying maps' sets of keys remain unchanged.
/// index and `length` operations and provides some degree of mutability. It
/// does so by imposing the additional constraint that the underlying maps' sets
/// of keys remain unchanged.
class MergedMapView<K, V> extends MapBase<K, V> {
// A map from keys to the maps in which those keys first appear.
final _mapsByKey = <K, Map<K, V>>{};
Expand Down
Loading

0 comments on commit b54643d

Please sign in to comment.