Skip to content
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

Add support for "@use with" #728

Merged
merged 4 commits into from
Jun 27, 2019
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
38 changes: 35 additions & 3 deletions lib/src/ast/sass/statement/use_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:collection';

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

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

Expand All @@ -19,12 +24,39 @@ 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;

final FileSpan span;

UseRule(this.url, this.namespace, this.span);
UseRule(this.url, this.namespace, this.span,
{Map<String, Tuple2<Expression, FileSpan>> configuration})
: configuration = configuration == null
? const {}
: UnmodifiableMapView(normalizedMap(configuration));

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

String toString() => "@use ${StringExpression.quoteText(url.toString())} as "
"${namespace ?? "*"};";
String toString() {
var buffer =
StringBuffer("@use ${StringExpression.quoteText(url.toString())}");

var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last;
var dot = basename.indexOf(".");
if (namespace != basename.substring(0, dot == -1 ? basename.length : dot)) {
buffer.write(" as ${namespace ?? "*"}");
}

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

buffer.write(";");
return buffer.toString();
}
}
81 changes: 63 additions & 18 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import 'parser.dart';
/// `feature.use` branch. It allows us to avoid having separate development
/// tracks as much as possible without shipping `@use` support until we're
/// ready.
const _parseUse = true;
///
/// This should not be referenced outside the Sass package.
const parseUse = true;

/// The base class for both the SCSS and indented syntax parsers.
///
Expand Down Expand Up @@ -885,7 +887,7 @@ abstract class StylesheetParser extends Parser {

expectStatementSeparator("@forward rule");
var span = scanner.spanFrom(start);
if (!_parseUse) {
if (!parseUse) {
error(
"@forward is coming soon, but it's not supported in this version of "
"Dart Sass.",
Expand Down Expand Up @@ -1271,25 +1273,14 @@ relase. For details, see http://bit.ly/moz-document.
var url = _urlString();
whitespace();

String namespace;
if (scanIdentifier("as")) {
whitespace();
namespace = scanner.scanChar($asterisk) ? null : identifier();
} else {
var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last;
var dot = basename.indexOf(".");
namespace = basename.substring(0, dot == -1 ? basename.length : dot);
var namespace = _useNamespace(url, start);
whitespace();
var configuration = _useConfiguration();

try {
namespace = Parser.parseIdentifier(namespace, logger: logger);
} on SassFormatException {
error('Invalid Sass identifier "$namespace"', scanner.spanFrom(start));
}
}
expectStatementSeparator("@use rule");

var span = scanner.spanFrom(start);
if (!_parseUse) {
if (!parseUse) {
error(
"@use is coming soon, but it's not supported in this version of "
"Dart Sass.",
Expand All @@ -1299,7 +1290,61 @@ relase. For details, see http://bit.ly/moz-document.
}
expectStatementSeparator("@use rule");

return UseRule(url, namespace, span);
return UseRule(url, namespace, span, configuration: configuration);
}

/// Parses the namespace of a `@use` rule from an `as` clause, or returns the
/// default namespace from its URL.
String _useNamespace(Uri url, LineScannerState start) {
if (scanIdentifier("as")) {
whitespace();
return scanner.scanChar($asterisk) ? null : identifier();
}

var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last;
var dot = basename.indexOf(".");
var namespace = basename.substring(0, dot == -1 ? basename.length : dot);
try {
return Parser.parseIdentifier(namespace, logger: logger);
} on SassFormatException {
error('Invalid Sass identifier "$namespace"', scanner.spanFrom(start));
}
}

/// Returns the map from variable names to expressions from a `@use` rule's
/// `with` clause.
///
/// Returns `null` if there is no `with` clause.
Map<String, Tuple2<Expression, FileSpan>> _useConfiguration() {
if (!scanIdentifier("with")) return null;

var configuration = normalizedMap<Tuple2<Expression, FileSpan>>();
whitespace();
scanner.expectChar($lparen);

while (true) {
whitespace();

var variableStart = scanner.state;
var name = variableName();
whitespace();
scanner.expectChar($colon);
whitespace();
var expression = _expressionUntilComma();
var span = scanner.spanFrom(variableStart);

if (configuration.containsKey(name)) {
error("The same variable may only be configured once.", span);
}
configuration[name] = Tuple2(expression, span);

if (!scanner.scanChar($comma)) break;
whitespace();
if (!_lookingAtExpression()) break;
}

scanner.expectChar($rparen);
return configuration;
}

/// Consumes a `@warn` rule.
Expand Down
8 changes: 7 additions & 1 deletion lib/src/util/limited_map_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import 'package:collection/collection.dart';

import '../utils.dart';

/// An unmodifiable view of a map that only allows certain keys to be accessed.
/// A mostly-unmodifiable view of a map that only allows certain keys to be
/// accessed.
///
/// Whether or not the underlying map contains keys that aren't allowed, this
/// view will behave as though it doesn't contain them.
///
/// The underlying map's values may change independently of this view, but its
/// set of keys may not.
///
/// This is unmodifiable *except for the [remove] method*, which is used for
/// `@used with` to mark configured variables as used.
class LimitedMapView<K, V> extends UnmodifiableMapBase<K, V> {
/// The wrapped map.
final Map<K, V> _map;
Expand Down Expand Up @@ -42,4 +46,6 @@ class LimitedMapView<K, V> extends UnmodifiableMapBase<K, V> {

V operator [](Object key) => _keys.contains(key) ? _map[key] : null;
bool containsKey(Object key) => _keys.contains(key);

V remove(Object key) => _keys.contains(key) ? _map.remove(key) : null;
}
12 changes: 6 additions & 6 deletions lib/src/util/prefixed_map_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ class PrefixedMapView<V> extends UnmodifiableMapBase<String, V> {
{bool equals(String string1, String string2)})
: _equals = equals ?? ((string1, string2) => string1 == string2);

V operator [](Object key) => key is String && _startsWith(key, _prefix)
V operator [](Object key) => key is String && _startsWithPrefix(key)
? _map[key.substring(_prefix.length)]
: null;

bool containsKey(Object key) => key is String && _startsWith(key, _prefix)
bool containsKey(Object key) => key is String && _startsWithPrefix(key)
? _map.containsKey(key.substring(_prefix.length))
: false;

/// Returns whether [string] begins with [prefix] according to [_equals].
bool _startsWith(String string, String prefix) =>
string.length >= prefix.length &&
_equals(string.substring(0, prefix.length), prefix);
/// Returns whether [string] begins with [_prefix] according to [_equals].
bool _startsWithPrefix(String string) =>
string.length >= _prefix.length &&
_equals(string.substring(0, _prefix.length), _prefix);
}

/// The implementation of [PrefixedMapViews.keys].
Expand Down
59 changes: 59 additions & 0 deletions lib/src/util/unprefixed_map_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 'dart:collection';

/// A mostly-unmodifiable view of a map with string keys that only allows keys
/// with a given prefix to be accessed, and presents them as though they didn't
/// have that prefix.
///
/// Whether or not the underlying map contains keys without the given prefix,
/// this view will behave as though it doesn't contain them.
///
/// This is unmodifiable *except for the [remove] method*, which is used for
/// `@used with` to mark configured variables as used.
class UnprefixedMapView<V> extends UnmodifiableMapBase<String, V> {
/// The wrapped map.
final Map<String, V> _map;

/// The prefix to remove from the map keys.
final String _prefix;

/// The equality operation to use for comparing map keys.
final bool Function(String string1, String string2) _equals;

Iterable<String> get keys => _UnprefixedKeys(this);

/// Creates a new unprefixed map view.
///
/// The map's notion of equality must match [equals], and must be stable over
/// substrings (that is, if `T == S`, then for all ranges `i..j`,
/// `T[i..j] == S[i..j]`).
UnprefixedMapView(this._map, this._prefix,
{bool equals(String string1, String string2)})
: _equals = equals ?? ((string1, string2) => string1 == string2);

V operator [](Object key) => key is String ? _map[_prefix + key] : null;

bool containsKey(Object key) =>
key is String ? _map.containsKey(_prefix + key) : false;

V remove(Object key) => key is String ? _map.remove(_prefix + key) : null;
}

/// The implementation of [UnprefixedMapViews.keys].
class _UnprefixedKeys extends IterableBase<String> {
/// The view whose keys are being iterated over.
final UnprefixedMapView<Object> _view;

Iterator<String> get iterator => _view._map.keys
.where((key) =>
key.length >= _view._prefix.length &&
_view._equals(key.substring(0, _view._prefix.length), _view._prefix))
.iterator;

_UnprefixedKeys(this._view);

bool contains(Object key) => _view.containsKey(key);
}
Loading