mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-30 04:39:03 +01:00
commit
c5c7d4c906
@ -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';
|
||||
|
126
lib/src/ast/sass/statement/forward_rule.dart
Normal file
126
lib/src/ast/sass/statement/forward_rule.dart
Normal 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(", ");
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
96
lib/src/module/forwarded_view.dart
Normal file
96
lib/src/module/forwarded_view.dart
Normal 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);
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
45
lib/src/util/limited_map_view.dart
Normal file
45
lib/src/util/limited_map_view.dart
Normal 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);
|
||||
}
|
72
lib/src/util/merged_map_view.dart
Normal file
72
lib/src/util/merged_map_view.dart
Normal 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);
|
||||
}
|
62
lib/src/util/prefixed_map_view.dart
Normal file
62
lib/src/util/prefixed_map_view.dart
Normal 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);
|
||||
}
|
@ -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);
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user