Merge pull request #696 from sass/forward

Add support for @forward
This commit is contained in:
Natalie Weizenbaum 2019-06-05 13:30:20 +01:00 committed by GitHub
commit c5c7d4c906
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 967 additions and 63 deletions

View File

@ -40,6 +40,7 @@ export 'sass/statement/each_rule.dart';
export 'sass/statement/error_rule.dart';
export 'sass/statement/extend_rule.dart';
export 'sass/statement/for_rule.dart';
export 'sass/statement/forward_rule.dart';
export 'sass/statement/function_rule.dart';
export 'sass/statement/if_rule.dart';
export 'sass/statement/import_rule.dart';

View File

@ -0,0 +1,126 @@
// 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:collection/collection.dart';
import 'package:source_span/source_span.dart';
import '../../../utils.dart';
import '../../../visitor/interface/statement.dart';
import '../expression/string.dart';
import '../statement.dart';
/// A `@forward` rule.
class ForwardRule implements Statement {
/// The URI of the module to forward.
///
/// If this is relative, it's relative to the containing file.
final Uri url;
/// The set of mixin and function names that may be accessed from the
/// forwarded module.
///
/// If this is empty, no mixins or functions may be accessed. If it's `null`,
/// it imposes no restrictions on which mixins and function may be accessed.
///
/// If this is non-`null`, [hiddenMixinsAndFunctions] and [hiddenVariables]
/// are guaranteed to both be `null` and [shownVariables] is guaranteed to be
/// non-`null`.
final Set<String> shownMixinsAndFunctions;
/// The set of variable names (without `$`) that may be accessed from the
/// forwarded module.
///
/// If this is empty, no variables may be accessed. If it's `null`, it imposes
/// no restrictions on which variables may be accessed.
///
/// If this is non-`null`, [hiddenMixinsAndFunctions] and [hiddenVariables]
/// are guaranteed to both be `null` and [shownMixinsAndFunctions] is
/// guaranteed to be non-`null`.
final Set<String> shownVariables;
/// The set of mixin and function names that may not be accessed from the
/// forwarded module.
///
/// If this is empty, any mixins or functions may be accessed. If it's `null`,
/// it imposes no restrictions on which mixins or functions may be accessed.
///
/// If this is non-`null`, [shownMixinsAndFunctions] and [shownVariables] are
/// guaranteed to both be `null` and [hiddenVariables] is guaranteed to be
/// non-`null`.
final Set<String> hiddenMixinsAndFunctions;
/// The set of variable names (without `$`) that may be accessed from the
/// forwarded module.
///
/// If this is empty, any variables may be accessed. If it's `null`, it
/// imposes no restrictions on which variables may be accessed.
///
/// If this is non-`null`, [shownMixinsAndFunctions] and [shownVariables] are
/// guaranteed to both be `null` and [hiddenMixinsAndFunctions] is guaranteed
/// to be non-`null`.
final Set<String> hiddenVariables;
/// The prefix to add to the beginning of the names of members of the used
/// module, or `null` if member names are used as-is.
final String prefix;
final FileSpan span;
/// Creates a `@forward` rule that allows all members to be accessed.
ForwardRule(this.url, this.span, {this.prefix})
: shownMixinsAndFunctions = null,
shownVariables = null,
hiddenMixinsAndFunctions = null,
hiddenVariables = null;
/// 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})
: shownMixinsAndFunctions =
UnmodifiableSetView(normalizedSet(shownMixinsAndFunctions)),
shownVariables = UnmodifiableSetView(normalizedSet(shownVariables)),
hiddenMixinsAndFunctions = null,
hiddenVariables = null;
/// 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})
: shownMixinsAndFunctions = null,
shownVariables = null,
hiddenMixinsAndFunctions =
UnmodifiableSetView(normalizedSet(hiddenMixinsAndFunctions)),
hiddenVariables = UnmodifiableSetView(normalizedSet(hiddenVariables));
T accept<T>(StatementVisitor<T> visitor) => visitor.visitForwardRule(this);
String toString() {
var buffer =
StringBuffer("@forward ${StringExpression.quoteText(url.toString())}");
if (shownMixinsAndFunctions != null) {
buffer
..write(" show ")
..write(_memberList(shownMixinsAndFunctions, shownVariables));
} else if (hiddenMixinsAndFunctions != null) {
buffer
..write(" hide ")
..write(_memberList(hiddenMixinsAndFunctions, hiddenVariables));
}
if (prefix != null) buffer.write(" as $prefix*");
buffer.write(";");
return buffer.toString();
}
/// Returns a combined list of names of the given members.
String _memberList(
Iterable<String> mixinsAndFunctions, Iterable<String> variables) =>
shownMixinsAndFunctions
.followedBy(shownVariables.map((name) => "\$$name"))
.join(", ");
}

View File

@ -4,16 +4,20 @@
import 'dart:async';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'ast/css.dart';
import 'ast/node.dart';
import 'ast/sass.dart';
import 'callable.dart';
import 'exception.dart';
import 'extend/extender.dart';
import 'module.dart';
import 'util/public_member_map.dart';
import 'module/forwarded_view.dart';
import 'util/merged_map_view.dart';
import 'util/public_member_map_view.dart';
import 'utils.dart';
import 'value.dart';
import 'visitor/clone_css.dart';
@ -31,8 +35,13 @@ class AsyncEnvironment {
/// This is `null` if there are no namespaceless modules.
Set<Module> _globalModules;
/// Modules from both [_modules] and [_global], in the order in which they
/// were `@use`d.
/// The modules forwarded by this module.
///
/// This is `null` if there are no forwarded modules.
List<Module> _forwardedModules;
/// Modules from [_modules], [_globalModules], and [_forwardedModules], in the
/// order in which they were `@use`d.
final List<Module> _allModules;
/// A list of variables defined at each lexical scope level.
@ -128,6 +137,7 @@ class AsyncEnvironment {
AsyncEnvironment({bool sourceMap = false})
: _modules = {},
_globalModules = null,
_forwardedModules = null,
_allModules = [],
_variables = [normalizedMap()],
_variableNodes = sourceMap ? [normalizedMap()] : null,
@ -140,6 +150,7 @@ class AsyncEnvironment {
AsyncEnvironment._(
this._modules,
this._globalModules,
this._forwardedModules,
this._allModules,
this._variables,
this._variableNodes,
@ -162,6 +173,7 @@ class AsyncEnvironment {
AsyncEnvironment closure() => AsyncEnvironment._(
_modules,
_globalModules,
_forwardedModules,
_allModules,
_variables.toList(),
_variableNodes?.toList(),
@ -176,6 +188,7 @@ class AsyncEnvironment {
AsyncEnvironment global() => AsyncEnvironment._(
{},
null,
null,
[],
_variables.toList(),
_variableNodes?.toList(),
@ -215,6 +228,82 @@ class AsyncEnvironment {
}
}
/// Exposes the members in [module] to downstream modules as though they were
/// defined in this module, according to the modifications defined by [rule].
void forwardModule(Module module, ForwardRule rule) {
_forwardedModules ??= [];
var view = ForwardedModuleView(module, rule);
for (var other in _forwardedModules) {
_assertNoConflicts(view.variables, other.variables, "variable", other);
_assertNoConflicts(view.functions, other.functions, "function", other);
_assertNoConflicts(view.mixins, other.mixins, "mixin", other);
}
// Add the original module to [_allModules] (rather than the
// [ForwardedModuleView]) so that we can de-duplicate upstream modules using
// `==`. This is safe because upstream modules are only used for collating
// CSS, not for the members they expose.
_allModules.add(module);
_forwardedModules.add(view);
}
/// Throws a [SassScriptException] if [newMembers] has any keys that overlap
/// with [oldMembers].
///
/// The [type] and [oldModule] is used for error reporting.
void _assertNoConflicts(Map<String, Object> newMembers,
Map<String, Object> oldMembers, String type, Module oldModule) {
Map<String, Object> smaller;
Map<String, Object> larger;
if (newMembers.length < oldMembers.length) {
smaller = newMembers;
larger = oldMembers;
} else {
smaller = oldMembers;
larger = newMembers;
}
for (var name in smaller.keys) {
if (larger.containsKey(name)) {
if (type == "variable") name = "\$$name";
throw SassScriptException(
'Module ${p.prettyUri(oldModule.url)} and the new module both '
'forward a $type named $name.');
}
}
}
/// Makes the members forwarded by [module] available in the current
/// environment.
///
/// This is called when [module] is `@import`ed.
void importForwards(Module module) {
if (module is _EnvironmentModule) {
for (var forwarded
in module._environment._forwardedModules ?? const <Module>[]) {
_globalModules ??= {};
_globalModules.add(forwarded);
// Remove existing definitions that the forwarded members are now
// shadowing.
for (var variable in forwarded.variables.keys) {
_variableIndices.remove(variable);
_variables[0].remove(variable);
if (_variableNodes != null) _variableNodes[0].remove(variable);
}
for (var function in forwarded.functions.keys) {
_functionIndices.remove(function);
_functions[0].remove(function);
}
for (var mixin in forwarded.mixins.keys) {
_mixinIndices.remove(mixin);
_mixins[0].remove(mixin);
}
}
}
}
/// Returns the value of the variable named [name], optionally with the given
/// [namespace], or `null` if no such variable is declared.
///
@ -577,7 +666,7 @@ class AsyncEnvironment {
/// that contains [css] as its CSS tree, which can be extended using
/// [extender].
Module toModule(CssStylesheet css, Extender extender) =>
_EnvironmentModule(this, css, extender);
_EnvironmentModule(this, css, extender, forwarded: _forwardedModules);
/// Returns the module with the given [namespace], or throws a
/// [SassScriptException] if none exists.
@ -633,24 +722,98 @@ class _EnvironmentModule implements Module {
/// The environment that defines this module's members.
final AsyncEnvironment _environment;
// TODO(nweiz): Use custom [UnmodifiableMapView]s that forbid access to
// private members.
_EnvironmentModule(this._environment, this.css, this.extender)
: upstream = _environment._allModules,
variables = PublicMemberMap(_environment._variables.first),
variableNodes = _environment._variableNodes == null
/// A map from variable names to the modules in which those variables appear,
/// used to determine where variables should be set.
///
/// Variables that don't appear in this map are either defined directly in
/// this module (if they appear in `_environment._variables.first`) or not
/// defined at all.
final Map<String, Module> _modulesByVariable;
factory _EnvironmentModule(
AsyncEnvironment environment, CssStylesheet css, Extender extender,
{List<Module> forwarded}) {
forwarded ??= const [];
return _EnvironmentModule._(
environment,
css,
extender,
_makeModulesByVariable(forwarded),
_memberMap(environment._variables.first,
forwarded.map((module) => module.variables)),
environment._variableNodes == null
? null
: PublicMemberMap(_environment._variableNodes.first),
functions = PublicMemberMap(_environment._functions.first),
mixins = PublicMemberMap(_environment._mixins.first),
transitivelyContainsCss = css.children.isNotEmpty ||
_environment._allModules
: _memberMap(environment._variableNodes.first,
forwarded.map((module) => module.variableNodes)),
_memberMap(environment._functions.first,
forwarded.map((module) => module.functions)),
_memberMap(environment._mixins.first,
forwarded.map((module) => module.mixins)),
transitivelyContainsCss: css.children.isNotEmpty ||
environment._allModules
.any((module) => module.transitivelyContainsCss),
transitivelyContainsExtensions = !extender.isEmpty ||
_environment._allModules
.any((module) => module.transitivelyContainsExtensions);
transitivelyContainsExtensions: !extender.isEmpty ||
environment._allModules
.any((module) => module.transitivelyContainsExtensions));
}
/// Create [_modulesByVariable] for a set of forwarded modules.
static Map<String, Module> _makeModulesByVariable(List<Module> forwarded) {
if (forwarded.isEmpty) return const {};
var modulesByVariable = normalizedMap<Module>();
for (var module in forwarded) {
if (module is _EnvironmentModule) {
// Flatten nested forwarded modules to avoid O(depth) overhead.
for (var child in module._modulesByVariable.values) {
setAll(modulesByVariable, child.variables.keys, child);
}
setAll(modulesByVariable, module._environment._variables.first.keys,
module);
} else {
setAll(modulesByVariable, module.variables.keys, module);
}
}
return modulesByVariable;
}
/// Returns a map that exposes the public members of [localMap] as well as all
/// the members of [otherMaps].
static Map<String, V> _memberMap<V>(
Map<String, V> localMap, Iterable<Map<String, V>> otherMaps) {
localMap = PublicMemberMapView(localMap);
if (otherMaps.isEmpty) return localMap;
var allMaps = [
for (var map in otherMaps) if (map.isNotEmpty) map,
localMap
];
if (allMaps.length == 1) return localMap;
return MergedMapView(allMaps,
equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator);
}
_EnvironmentModule._(
this._environment,
this.css,
this.extender,
this._modulesByVariable,
this.variables,
this.variableNodes,
this.functions,
this.mixins,
{@required this.transitivelyContainsCss,
@required this.transitivelyContainsExtensions})
: upstream = _environment._allModules;
void setVariable(String name, Value value, AstNode nodeWithSpan) {
var module = _modulesByVariable[name];
if (module != null) {
module.setVariable(name, value, nodeWithSpan);
return;
}
if (!_environment._variables.first.containsKey(name)) {
throw SassScriptException("Undefined variable.");
}
@ -666,8 +829,17 @@ class _EnvironmentModule implements Module {
if (css.children.isEmpty) return this;
var newCssAndExtender = cloneCssStylesheet(css, extender);
return _EnvironmentModule(
_environment, newCssAndExtender.item1, newCssAndExtender.item2);
return _EnvironmentModule._(
_environment,
newCssAndExtender.item1,
newCssAndExtender.item2,
_modulesByVariable,
variables,
variableNodes,
functions,
mixins,
transitivelyContainsCss: transitivelyContainsCss,
transitivelyContainsExtensions: transitivelyContainsExtensions);
}
String toString() => p.prettyUri(css.span.sourceUrl);

View File

@ -5,20 +5,24 @@
// DO NOT EDIT. This file was generated from async_environment.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: b3626ab4de7508e5a54ea404f2fdf0ff14cd4418
// Checksum: e3e1c304d7ca4a58ee7847229da9ec34be0fb902
//
// ignore_for_file: unused_import
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:source_span/source_span.dart';
import 'ast/css.dart';
import 'ast/node.dart';
import 'ast/sass.dart';
import 'callable.dart';
import 'exception.dart';
import 'extend/extender.dart';
import 'module.dart';
import 'util/public_member_map.dart';
import 'module/forwarded_view.dart';
import 'util/merged_map_view.dart';
import 'util/public_member_map_view.dart';
import 'utils.dart';
import 'value.dart';
import 'visitor/clone_css.dart';
@ -36,8 +40,13 @@ class Environment {
/// This is `null` if there are no namespaceless modules.
Set<Module<Callable>> _globalModules;
/// Modules from both [_modules] and [_global], in the order in which they
/// were `@use`d.
/// The modules forwarded by this module.
///
/// This is `null` if there are no forwarded modules.
List<Module<Callable>> _forwardedModules;
/// Modules from [_modules], [_globalModules], and [_forwardedModules], in the
/// order in which they were `@use`d.
final List<Module<Callable>> _allModules;
/// A list of variables defined at each lexical scope level.
@ -133,6 +142,7 @@ class Environment {
Environment({bool sourceMap = false})
: _modules = {},
_globalModules = null,
_forwardedModules = null,
_allModules = [],
_variables = [normalizedMap()],
_variableNodes = sourceMap ? [normalizedMap()] : null,
@ -145,6 +155,7 @@ class Environment {
Environment._(
this._modules,
this._globalModules,
this._forwardedModules,
this._allModules,
this._variables,
this._variableNodes,
@ -167,6 +178,7 @@ class Environment {
Environment closure() => Environment._(
_modules,
_globalModules,
_forwardedModules,
_allModules,
_variables.toList(),
_variableNodes?.toList(),
@ -181,6 +193,7 @@ class Environment {
Environment global() => Environment._(
{},
null,
null,
[],
_variables.toList(),
_variableNodes?.toList(),
@ -220,6 +233,82 @@ class Environment {
}
}
/// Exposes the members in [module] to downstream modules as though they were
/// defined in this module, according to the modifications defined by [rule].
void forwardModule(Module<Callable> module, ForwardRule rule) {
_forwardedModules ??= [];
var view = ForwardedModuleView(module, rule);
for (var other in _forwardedModules) {
_assertNoConflicts(view.variables, other.variables, "variable", other);
_assertNoConflicts(view.functions, other.functions, "function", other);
_assertNoConflicts(view.mixins, other.mixins, "mixin", other);
}
// Add the original module to [_allModules] (rather than the
// [ForwardedModuleView]) so that we can de-duplicate upstream modules using
// `==`. This is safe because upstream modules are only used for collating
// CSS, not for the members they expose.
_allModules.add(module);
_forwardedModules.add(view);
}
/// Throws a [SassScriptException] if [newMembers] has any keys that overlap
/// with [oldMembers].
///
/// The [type] and [oldModule] is used for error reporting.
void _assertNoConflicts(Map<String, Object> newMembers,
Map<String, Object> oldMembers, String type, Module<Callable> oldModule) {
Map<String, Object> smaller;
Map<String, Object> larger;
if (newMembers.length < oldMembers.length) {
smaller = newMembers;
larger = oldMembers;
} else {
smaller = oldMembers;
larger = newMembers;
}
for (var name in smaller.keys) {
if (larger.containsKey(name)) {
if (type == "variable") name = "\$$name";
throw SassScriptException(
'Module ${p.prettyUri(oldModule.url)} and the new module both '
'forward a $type named $name.');
}
}
}
/// Makes the members forwarded by [module] available in the current
/// environment.
///
/// This is called when [module] is `@import`ed.
void importForwards(Module<Callable> module) {
if (module is _EnvironmentModule) {
for (var forwarded in module._environment._forwardedModules ??
const <Module<Callable>>[]) {
_globalModules ??= {};
_globalModules.add(forwarded);
// Remove existing definitions that the forwarded members are now
// shadowing.
for (var variable in forwarded.variables.keys) {
_variableIndices.remove(variable);
_variables[0].remove(variable);
if (_variableNodes != null) _variableNodes[0].remove(variable);
}
for (var function in forwarded.functions.keys) {
_functionIndices.remove(function);
_functions[0].remove(function);
}
for (var mixin in forwarded.mixins.keys) {
_mixinIndices.remove(mixin);
_mixins[0].remove(mixin);
}
}
}
}
/// Returns the value of the variable named [name], optionally with the given
/// [namespace], or `null` if no such variable is declared.
///
@ -580,7 +669,7 @@ class Environment {
/// that contains [css] as its CSS tree, which can be extended using
/// [extender].
Module<Callable> toModule(CssStylesheet css, Extender extender) =>
_EnvironmentModule(this, css, extender);
_EnvironmentModule(this, css, extender, forwarded: _forwardedModules);
/// Returns the module with the given [namespace], or throws a
/// [SassScriptException] if none exists.
@ -637,24 +726,99 @@ class _EnvironmentModule implements Module<Callable> {
/// The environment that defines this module's members.
final Environment _environment;
// TODO(nweiz): Use custom [UnmodifiableMapView]s that forbid access to
// private members.
_EnvironmentModule(this._environment, this.css, this.extender)
: upstream = _environment._allModules,
variables = PublicMemberMap(_environment._variables.first),
variableNodes = _environment._variableNodes == null
/// A map from variable names to the modules in which those variables appear,
/// used to determine where variables should be set.
///
/// Variables that don't appear in this map are either defined directly in
/// this module (if they appear in `_environment._variables.first`) or not
/// defined at all.
final Map<String, Module<Callable>> _modulesByVariable;
factory _EnvironmentModule(
Environment environment, CssStylesheet css, Extender extender,
{List<Module<Callable>> forwarded}) {
forwarded ??= const [];
return _EnvironmentModule._(
environment,
css,
extender,
_makeModulesByVariable(forwarded),
_memberMap(environment._variables.first,
forwarded.map((module) => module.variables)),
environment._variableNodes == null
? null
: PublicMemberMap(_environment._variableNodes.first),
functions = PublicMemberMap(_environment._functions.first),
mixins = PublicMemberMap(_environment._mixins.first),
transitivelyContainsCss = css.children.isNotEmpty ||
_environment._allModules
: _memberMap(environment._variableNodes.first,
forwarded.map((module) => module.variableNodes)),
_memberMap(environment._functions.first,
forwarded.map((module) => module.functions)),
_memberMap(environment._mixins.first,
forwarded.map((module) => module.mixins)),
transitivelyContainsCss: css.children.isNotEmpty ||
environment._allModules
.any((module) => module.transitivelyContainsCss),
transitivelyContainsExtensions = !extender.isEmpty ||
_environment._allModules
.any((module) => module.transitivelyContainsExtensions);
transitivelyContainsExtensions: !extender.isEmpty ||
environment._allModules
.any((module) => module.transitivelyContainsExtensions));
}
/// Create [_modulesByVariable] for a set of forwarded modules.
static Map<String, Module<Callable>> _makeModulesByVariable(
List<Module<Callable>> forwarded) {
if (forwarded.isEmpty) return const {};
var modulesByVariable = normalizedMap<Module<Callable>>();
for (var module in forwarded) {
if (module is _EnvironmentModule) {
// Flatten nested forwarded modules to avoid O(depth) overhead.
for (var child in module._modulesByVariable.values) {
setAll(modulesByVariable, child.variables.keys, child);
}
setAll(modulesByVariable, module._environment._variables.first.keys,
module);
} else {
setAll(modulesByVariable, module.variables.keys, module);
}
}
return modulesByVariable;
}
/// Returns a map that exposes the public members of [localMap] as well as all
/// the members of [otherMaps].
static Map<String, V> _memberMap<V>(
Map<String, V> localMap, Iterable<Map<String, V>> otherMaps) {
localMap = PublicMemberMapView(localMap);
if (otherMaps.isEmpty) return localMap;
var allMaps = [
for (var map in otherMaps) if (map.isNotEmpty) map,
localMap
];
if (allMaps.length == 1) return localMap;
return MergedMapView(allMaps,
equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator);
}
_EnvironmentModule._(
this._environment,
this.css,
this.extender,
this._modulesByVariable,
this.variables,
this.variableNodes,
this.functions,
this.mixins,
{@required this.transitivelyContainsCss,
@required this.transitivelyContainsExtensions})
: upstream = _environment._allModules;
void setVariable(String name, Value value, AstNode nodeWithSpan) {
var module = _modulesByVariable[name];
if (module != null) {
module.setVariable(name, value, nodeWithSpan);
return;
}
if (!_environment._variables.first.containsKey(name)) {
throw SassScriptException("Undefined variable.");
}
@ -670,8 +834,17 @@ class _EnvironmentModule implements Module<Callable> {
if (css.children.isEmpty) return this;
var newCssAndExtender = cloneCssStylesheet(css, extender);
return _EnvironmentModule(
_environment, newCssAndExtender.item1, newCssAndExtender.item2);
return _EnvironmentModule._(
_environment,
newCssAndExtender.item1,
newCssAndExtender.item2,
_modulesByVariable,
variables,
variableNodes,
functions,
mixins,
transitivelyContainsCss: transitivelyContainsCss,
transitivelyContainsExtensions: transitivelyContainsExtensions);
}
String toString() => p.prettyUri(css.span.sourceUrl);

View File

@ -0,0 +1,96 @@
// 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 '../ast/css.dart';
import '../ast/node.dart';
import '../ast/sass.dart';
import '../callable.dart';
import '../exception.dart';
import '../extend/extender.dart';
import '../module.dart';
import '../util/limited_map_view.dart';
import '../util/prefixed_map_view.dart';
import '../utils.dart';
import '../value.dart';
/// A [Module] that exposes members according to a [ForwardRule].
class ForwardedModuleView<T extends AsyncCallable> implements Module<T> {
/// The wrapped module.
final Module<T> _inner;
/// The rule that determines how this module's members should be exposed.
final ForwardRule _rule;
Uri get url => _inner.url;
List<Module<T>> get upstream => _inner.upstream;
Extender get extender => _inner.extender;
CssStylesheet get css => _inner.css;
bool get transitivelyContainsCss => _inner.transitivelyContainsCss;
bool get transitivelyContainsExtensions =>
_inner.transitivelyContainsExtensions;
final Map<String, Value> variables;
final Map<String, AstNode> variableNodes;
final Map<String, T> functions;
final Map<String, T> mixins;
ForwardedModuleView(this._inner, this._rule)
: variables = _forwardedMap(_inner.variables, _rule.prefix,
_rule.shownVariables, _rule.hiddenVariables),
variableNodes = _inner.variableNodes == null
? null
: _forwardedMap(_inner.variableNodes, _rule.prefix,
_rule.shownVariables, _rule.hiddenVariables),
functions = _forwardedMap(_inner.functions, _rule.prefix,
_rule.shownMixinsAndFunctions, _rule.hiddenMixinsAndFunctions),
mixins = _forwardedMap(_inner.mixins, _rule.prefix,
_rule.shownMixinsAndFunctions, _rule.hiddenMixinsAndFunctions);
/// Wraps [map] so that it only shows members allowed by [blacklist] or
/// [whitelist], with the given [prefix], if given.
///
/// Only one of [blacklist] or [whitelist] may be non-`null`.
static Map<String, V> _forwardedMap<V>(Map<String, V> map, String prefix,
Set<String> whitelist, Set<String> blacklist) {
assert(whitelist == null || blacklist == null);
if (prefix == null &&
whitelist == null &&
(blacklist == null || blacklist.isEmpty)) {
return map;
}
if (prefix != null) {
map = PrefixedMapView(map, prefix, equals: equalsIgnoreSeparator);
}
if (whitelist != null) {
map = LimitedMapView.whitelist(map, whitelist);
} else if (blacklist != null && blacklist.isNotEmpty) {
map = LimitedMapView.blacklist(map, blacklist);
}
return map;
}
void setVariable(String name, Value value, AstNode nodeWithSpan) {
if (_rule.shownVariables != null && !_rule.shownVariables.contains(name)) {
throw SassScriptException("Undefined variable.");
} else if (_rule.hiddenVariables != null &&
_rule.hiddenVariables.contains(name)) {
throw SassScriptException("Undefined variable.");
}
if (_rule.prefix != null) {
if (!startsWithIgnoreSeparator(name, _rule.prefix)) {
throw SassScriptException("Undefined variable.");
}
name = name.substring(_rule.prefix.length);
}
return _inner.setVariable(name, value, nodeWithSpan);
}
Module<T> cloneCss() => ForwardedModuleView(_inner.cloneCss(), _rule);
}

View File

@ -632,6 +632,17 @@ class Parser {
void error(String message, FileSpan span) =>
throw StringScannerException(message, span, scanner.string);
/// Runs callback and, if it throws a [SourceSpanFormatException], rethrows it
/// with [message] as its message.
@protected
T withErrorMessage<T>(String message, T callback()) {
try {
return callback();
} on SourceSpanFormatException catch (error) {
throw SourceSpanFormatException(message, error.span, error.source);
}
}
/// Prints a source span highlight of the current location being scanned.
///
/// If [message] is passed, prints that as well. This is intended for use when

View File

@ -534,6 +534,10 @@ abstract class StylesheetParser extends Parser {
return _extendRule(start);
case "for":
return _forRule(start, child);
case "forward":
_isUseAllowed = wasUseAllowed;
if (!root) _disallowedAtRule(start);
return _forwardRule(start);
case "function":
return _functionRule(start);
case "if":
@ -850,6 +854,82 @@ abstract class StylesheetParser extends Parser {
});
}
/// Consumes a `@forward` rule.
///
/// [start] should point before the `@`.
ForwardRule _forwardRule(LineScannerState start) {
var url = _urlString();
whitespace();
String prefix;
if (scanIdentifier("as")) {
whitespace();
prefix = identifier();
scanner.expectChar($asterisk);
whitespace();
}
Set<String> shownMixinsAndFunctions;
Set<String> shownVariables;
Set<String> hiddenMixinsAndFunctions;
Set<String> hiddenVariables;
if (scanIdentifier("show")) {
var members = _memberList();
shownMixinsAndFunctions = members.item1;
shownVariables = members.item2;
} else if (scanIdentifier("hide")) {
var members = _memberList();
hiddenMixinsAndFunctions = members.item1;
hiddenVariables = members.item2;
}
expectStatementSeparator("@forward rule");
var span = scanner.spanFrom(start);
if (!_parseUse) {
error(
"@forward is coming soon, but it's not supported in this version of "
"Dart Sass.",
span);
} else if (!_isUseAllowed) {
error("@forward rules must be written before any other rules.", span);
}
if (shownMixinsAndFunctions != null) {
return ForwardRule.show(
url, shownMixinsAndFunctions, shownVariables, span,
prefix: prefix);
} else if (hiddenMixinsAndFunctions != null) {
return ForwardRule.hide(
url, hiddenMixinsAndFunctions, hiddenVariables, span,
prefix: prefix);
} else {
return ForwardRule(url, span, prefix: prefix);
}
}
/// Consumes a list of members that may contain either plain identifiers or
/// variable names.
///
/// The plain identifiers are returned in the first set, and the variable
/// names in the second.
Tuple2<Set<String>, Set<String>> _memberList() {
var identifiers = Set<String>();
var variables = Set<String>();
do {
whitespace();
withErrorMessage("Expected variable, mixin, or function name", () {
if (scanner.peekChar() == $dollar) {
variables.add(variableName());
} else {
identifiers.add(identifier());
}
});
whitespace();
} while (scanner.scanChar($comma));
return Tuple2(identifiers, variables);
}
/// Consumes an `@if` rule.
///
/// [start] should point before the `@`. [child] is called to consume any
@ -1188,13 +1268,7 @@ relase. For details, see http://bit.ly/moz-document.
///
/// [start] should point before the `@`.
UseRule _useRule(LineScannerState start) {
var urlString = string();
Uri url;
try {
url = Uri.parse(urlString);
} on FormatException catch (innerError) {
error("Invalid URL: ${innerError.message}", scanner.spanFrom(start));
}
var url = _urlString();
whitespace();
String namespace;
@ -1223,6 +1297,7 @@ relase. For details, see http://bit.ly/moz-document.
} else if (!_isUseAllowed) {
error("@use rules must be written before any other rules.", span);
}
expectStatementSeparator("@use rule");
return UseRule(url, namespace, span);
}
@ -3236,6 +3311,17 @@ relase. For details, see http://bit.ly/moz-document.
return result;
}
/// Consumes a string that contains a valid URL.
Uri _urlString() {
var start = scanner.state;
var url = string();
try {
return Uri.parse(url);
} on FormatException catch (innerError) {
error("Invalid URL: ${innerError.message}", scanner.spanFrom(start));
}
}
/// Like [identifier], but rejects identifiers that begin with `_` or `-`.
String _publicIdentifier() {
var start = scanner.state;

View File

@ -0,0 +1,45 @@
// 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';
import 'package:collection/collection.dart';
import '../utils.dart';
/// An 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.
class LimitedMapView<K, V> extends UnmodifiableMapBase<K, V> {
/// The wrapped map.
final Map<K, V> _map;
/// The allowed keys in [_map].
final Set<K> _keys;
Iterable<K> get keys => _keys;
int get length => _keys.length;
bool get isEmpty => _keys.isEmpty;
bool get isNotEmpty => _keys.isNotEmpty;
/// Returns a [LimitedMapView] that allows only keys in [whitelist].
///
/// The [whitelist] must have the same notion of equality as the [map].
LimitedMapView.whitelist(this._map, Set<K> whitelist)
: _keys = whitelist.intersection(MapKeySet(_map));
/// Returns a [LimitedMapView] that doesn't allow keys in [blacklist].
///
/// The [blacklist] must have the same notion of equality as the [map].
LimitedMapView.blacklist(this._map, Set<K> blacklist)
: _keys = toSetWithEquality(
_map.keys.where((key) => !blacklist.contains(key)), blacklist);
V operator [](Object key) => _keys.contains(key) ? _map[key] : null;
bool containsKey(Object key) => _keys.contains(key);
}

View File

@ -0,0 +1,72 @@
// 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';
import '../utils.dart';
/// An unmodifiable view of multiple maps merged together as though they were a
/// single map.
///
/// The values in later maps take precedence over those in earlier maps. When a
/// key is set, it's set in the last map that has an existing value for that
/// 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.
class MergedMapView<K, V> extends MapBase<K, V> {
// A map from keys to the maps in which those keys first appear.
final Map<K, Map<K, V>> _mapsByKey;
Iterable<K> get keys => _mapsByKey.keys;
int get length => _mapsByKey.length;
bool get isEmpty => _mapsByKey.isEmpty;
bool get isNotEmpty => _mapsByKey.isNotEmpty;
/// Creates a combined view of [maps].
///
/// Each map must have the same notion of equality, and that notion of
/// equality must also be used for the [equals] and [hashCode] callbacks. The
/// underlying maps' values may change independently of this view, but their
/// set of keys may not.
MergedMapView(Iterable<Map<K, V>> maps,
{bool equals(K key1, K key2), int hashCode(K key)})
: _mapsByKey = LinkedHashMap(equals: equals, hashCode: hashCode) {
for (var map in maps) {
if (map is MergedMapView<K, V>) {
// Flatten nested merged views to avoid O(depth) overhead.
for (var child in map._mapsByKey.values) {
setAll(_mapsByKey, child.keys, child);
}
} else {
setAll(_mapsByKey, map.keys, map);
}
}
}
V operator [](Object key) {
var child = _mapsByKey[key];
return child == null ? null : child[key];
}
operator []=(K key, V value) {
var child = _mapsByKey[key];
if (child == null) {
throw UnsupportedError("New entries may not be added to MergedMapView.");
}
child[key] = value;
}
V remove(Object key) {
throw UnsupportedError("Entries may not be removed from MergedMapView.");
}
void clear() {
throw UnsupportedError("Entries may not be removed from MergedMapView.");
}
bool containsKey(Object key) => _mapsByKey.containsKey(key);
}

View File

@ -0,0 +1,62 @@
// 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';
/// An unmodifiable view of a map with string keys that allows keys to be
/// accessed with an additional prefix.
///
/// Whether or not the underlying map contains keys that aren't allowed, this
/// view will behave as though it doesn't contain them.
class PrefixedMapView<V> extends UnmodifiableMapBase<String, V> {
/// The wrapped map.
final Map<String, V> _map;
/// The prefix to add to 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 => _PrefixedKeys(this);
int get length => _map.length;
bool get isEmpty => _map.isEmpty;
bool get isNotEmpty => _map.isNotEmpty;
/// Creates a new prefixed 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]`).
PrefixedMapView(this._map, this._prefix,
{bool equals(String string1, String string2)})
: _equals = equals ?? ((string1, string2) => string1 == string2);
V operator [](Object key) => key is String && _startsWith(key, _prefix)
? _map[key.substring(_prefix.length)]
: null;
bool containsKey(Object key) => key is String && _startsWith(key, _prefix)
? _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);
}
/// The implementation of [PrefixedMapViews.keys].
class _PrefixedKeys extends IterableBase<String> {
/// The view whose keys are being iterated over.
final PrefixedMapView<Object> _view;
int get length => _view.length;
Iterator<String> get iterator =>
_view._map.keys.map((key) => "${_view._prefix}$key").iterator;
_PrefixedKeys(this._view);
bool contains(Object key) => _view.containsKey(key);
}

View File

@ -10,13 +10,13 @@ import '../utils.dart';
/// begin with `_` or `-`.
///
/// Note that [PublicMemberMap.length] is *not* `O(1)`.
class PublicMemberMap<V> extends UnmodifiableMapBase<String, V> {
class PublicMemberMapView<V> extends UnmodifiableMapBase<String, V> {
/// The wrapped map.
final Map<String, V> _inner;
Iterable<String> get keys => _inner.keys.where(isPublic);
PublicMemberMap(this._inner);
PublicMemberMapView(this._inner);
bool containsKey(Object key) =>
key is String && isPublic(key) && _inner.containsKey(key);

View File

@ -274,6 +274,25 @@ bool startsWithIgnoreCase(String string, String prefix) {
return true;
}
/// Returns whether [string] begins with [prefix] if `-` and `_` are
/// considered equivalent.
bool startsWithIgnoreSeparator(String string, String prefix) {
if (string.length < prefix.length) return false;
for (var i = 0; i < prefix.length; i++) {
var stringCodeUnit = string.codeUnitAt(i);
var prefixCodeUnit = prefix.codeUnitAt(i);
if (stringCodeUnit == prefixCodeUnit) continue;
if (stringCodeUnit == $dash) {
if (prefixCodeUnit != $underscore) return false;
} else if (stringCodeUnit == $underscore) {
if (prefixCodeUnit != $dash) return false;
} else {
return false;
}
}
return true;
}
/// Returns an empty map that uses [equalsIgnoreSeparator] for key equality.
///
/// If [source] is passed, copies it into the map.
@ -308,6 +327,13 @@ Map<String, V2> normalizedMapMap<K, V1, V2>(Map<K, V1> map,
return result;
}
/// Returns a set containing the elements in [elements], whose notion of
/// equality matches that of [matchEquality].
Set<T> toSetWithEquality<T>(Iterable<T> elements, Set<T> matchEquality) =>
matchEquality.toSet()
..clear()
..addAll(elements);
/// Destructively updates every element of [list] with the result of [function].
void mapInPlace<T>(List<T> list, T function(T element)) {
for (var i = 0; i < list.length; i++) {
@ -393,6 +419,13 @@ void mapAddAll2<K1, K2, V>(
});
}
/// Sets all [keys] in [map] to [value].
void setAll<K, V>(Map<K, V> map, Iterable<K> keys, V value) {
for (var key in keys) {
map[key] = value;
}
}
/// Rotates the element in list from [start] (inclusive) to [end] (exclusive)
/// one index higher, looping the final element back to [start].
void rotateSlice(List list, int start, int end) {

View File

@ -374,8 +374,8 @@ class _EvaluateVisitor
var canonicalUrl = stylesheet.span.sourceUrl;
if (_activeModules.contains(canonicalUrl)) {
throw _exception(
"This module is currently being loaded.", nodeForSpan.span);
throw _exception("Module loop: this module is already being loaded.",
nodeForSpan.span);
}
_activeModules.add(canonicalUrl);
@ -975,6 +975,14 @@ class _EvaluateVisitor
}, semiGlobal: true);
}
Future<Value> visitForwardRule(ForwardRule node) async {
await _loadModule(node.url, "@forward", node, (module) {
_environment.forwardModule(module, node);
});
return null;
}
Future<Value> visitFunctionRule(FunctionRule node) async {
_environment.setFunction(UserDefinedCallable(node, _environment.closure()));
return null;
@ -1021,8 +1029,9 @@ class _EvaluateVisitor
_activeModules.add(url);
// TODO(nweiz): If [stylesheet] contains no `@use` rules, just evaluate it
// directly in [_root] rather than making a new stylesheet.
// TODO(nweiz): If [stylesheet] contains no `@use` or `@forward` rules, just
// evaluate it directly in [_root] rather than making a new
// [ModifiableCssStylesheet] and manually copying members.
List<ModifiableCssNode> children;
var environment = _environment.global();
@ -1053,10 +1062,13 @@ class _EvaluateVisitor
});
});
// Create a dummy module with empty CSS and no extensions to combine all
// the CSS from modules used by [stylesheet].
// Create a dummy module with empty CSS and no extensions to make forwarded
// members available in the current import context and to combine all the
// CSS from modules used by [stylesheet].
var module = environment.toModule(
CssStylesheet(const [], stylesheet.span), Extender.empty);
_environment.importForwards(module);
if (module.transitivelyContainsCss) {
// If any transitively used module contains extensions, we need to clone
// all modules' CSS. Otherwise, it's possible that they'll be used or

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/grind/synchronize.dart for details.
//
// Checksum: 36af5d91812c44a7f77ef0b7f580fa9fb8520ad0
// Checksum: bad6506a5b7221d448f4e9156ceb5af097cfb36e
//
// ignore_for_file: unused_import
@ -381,8 +381,8 @@ class _EvaluateVisitor
var canonicalUrl = stylesheet.span.sourceUrl;
if (_activeModules.contains(canonicalUrl)) {
throw _exception(
"This module is currently being loaded.", nodeForSpan.span);
throw _exception("Module loop: this module is already being loaded.",
nodeForSpan.span);
}
_activeModules.add(canonicalUrl);
@ -977,6 +977,14 @@ class _EvaluateVisitor
}, semiGlobal: true);
}
Value visitForwardRule(ForwardRule node) {
_loadModule(node.url, "@forward", node, (module) {
_environment.forwardModule(module, node);
});
return null;
}
Value visitFunctionRule(FunctionRule node) {
_environment.setFunction(UserDefinedCallable(node, _environment.closure()));
return null;
@ -1023,8 +1031,9 @@ class _EvaluateVisitor
_activeModules.add(url);
// TODO(nweiz): If [stylesheet] contains no `@use` rules, just evaluate it
// directly in [_root] rather than making a new stylesheet.
// TODO(nweiz): If [stylesheet] contains no `@use` or `@forward` rules, just
// evaluate it directly in [_root] rather than making a new
// [ModifiableCssStylesheet] and manually copying members.
List<ModifiableCssNode> children;
var environment = _environment.global();
@ -1055,10 +1064,13 @@ class _EvaluateVisitor
});
});
// Create a dummy module with empty CSS and no extensions to combine all
// the CSS from modules used by [stylesheet].
// Create a dummy module with empty CSS and no extensions to make forwarded
// members available in the current import context and to combine all the
// CSS from modules used by [stylesheet].
var module = environment.toModule(
CssStylesheet(const [], stylesheet.span), Extender.empty);
_environment.importForwards(module);
if (module.transitivelyContainsCss) {
// If any transitively used module contains extensions, we need to clone
// all modules' CSS. Otherwise, it's possible that they'll be used or

View File

@ -18,6 +18,7 @@ abstract class StatementVisitor<T> {
T visitErrorRule(ErrorRule node);
T visitExtendRule(ExtendRule node);
T visitForRule(ForRule node);
T visitForwardRule(ForwardRule node);
T visitFunctionRule(FunctionRule node);
T visitIfRule(IfRule node);
T visitImportRule(ImportRule node);

View File

@ -72,6 +72,8 @@ abstract class RecursiveStatementVisitor<T> implements StatementVisitor<T> {
return visitChildren(node);
}
T visitForwardRule(ForwardRule node) => null;
T visitFunctionRule(FunctionRule node) => visitCallableDeclaration(node);
T visitIfRule(IfRule node) {