Merge pull request #593 from sass/merge-use

Merge branch 'feature.use' into master
This commit is contained in:
Natalie Weizenbaum 2019-02-13 14:56:17 -08:00 committed by GitHub
commit 4c7b6cc0e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 277 additions and 32 deletions

View File

@ -53,6 +53,7 @@ export 'sass/statement/silent_comment.dart';
export 'sass/statement/style_rule.dart';
export 'sass/statement/stylesheet.dart';
export 'sass/statement/supports_rule.dart';
export 'sass/statement/use_rule.dart';
export 'sass/statement/variable_declaration.dart';
export 'sass/statement/warn_rule.dart';
export 'sass/statement/while_rule.dart';

View File

@ -4,7 +4,6 @@
import 'package:source_span/source_span.dart';
import '../../../utils.dart';
import '../../../visitor/interface/expression.dart';
import '../expression.dart';
import '../argument_invocation.dart';
@ -15,6 +14,10 @@ import '../interpolation.dart';
///
/// This may be a plain CSS function or a Sass function.
class FunctionExpression implements Expression, CallableInvocation {
/// The namespace of the function being invoked, or `null` if it's invoked
/// without a namespace.
final String namespace;
/// The name of the function being invoked.
///
/// If this is interpolated, the function will be interpreted as plain CSS,
@ -24,12 +27,17 @@ class FunctionExpression implements Expression, CallableInvocation {
/// The arguments to pass to the function.
final ArgumentInvocation arguments;
FileSpan get span => spanForList([name, arguments]);
final FileSpan span;
FunctionExpression(this.name, this.arguments);
FunctionExpression(this.name, this.arguments, this.span, {this.namespace});
T accept<T>(ExpressionVisitor<T> visitor) =>
visitor.visitFunctionExpression(this);
String toString() => "$name$arguments";
String toString() {
var buffer = StringBuffer();
if (namespace != null) buffer.write("$namespace.");
buffer.write("$name$arguments");
return buffer.toString();
}
}

View File

@ -9,15 +9,24 @@ import '../expression.dart';
/// A Sass variable.
class VariableExpression implements Expression {
/// The namespace of the variable being referenced, or `null` if it's
/// referenced without a namespace.
final String namespace;
/// The name of this variable.
final String name;
final FileSpan span;
VariableExpression(this.name, this.span);
VariableExpression(this.name, this.span, {this.namespace});
T accept<T>(ExpressionVisitor<T> visitor) =>
visitor.visitVariableExpression(this);
String toString() => "\$$name";
String toString() {
var buffer = StringBuffer("\$");
if (namespace != null) buffer.write("$namespace.");
buffer.write(name);
return buffer.toString();
}
}

View File

@ -12,6 +12,10 @@ import 'content_block.dart';
/// A mixin invocation.
class IncludeRule implements Statement, CallableInvocation {
/// The namespace of the mixin being invoked, or `null` if it's invoked
/// without a namespace.
final String namespace;
/// The name of the mixin being invoked.
final String name;
@ -24,12 +28,15 @@ class IncludeRule implements Statement, CallableInvocation {
final FileSpan span;
IncludeRule(this.name, this.arguments, this.span, {this.content});
IncludeRule(this.name, this.arguments, this.span,
{this.namespace, this.content});
T accept<T>(StatementVisitor<T> visitor) => visitor.visitIncludeRule(this);
String toString() {
var buffer = StringBuffer("@include $name");
var buffer = StringBuffer("@include ");
if (namespace != null) buffer.write("$namespace.");
buffer.write(name);
if (!arguments.isEmpty) buffer.write("($arguments)");
buffer.write(content == null ? ";" : " $content");
return buffer.toString();

View File

@ -0,0 +1,30 @@
// Copyright 2016 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'package:source_span/source_span.dart';
import '../../../visitor/interface/statement.dart';
import '../expression/string.dart';
import '../statement.dart';
/// A `@use` rule.
class UseRule implements Statement {
/// The URI of the module to use.
///
/// If this is relative, it's relative to the containing file.
final Uri url;
/// The namespace for members of the used module, or `null` if the members
/// can be accessed without a namespace.
final String namespace;
final FileSpan span;
UseRule(this.url, this.namespace, this.span);
T accept<T>(StatementVisitor<T> visitor) => visitor.visitUseRule(this);
String toString() => "@use ${StringExpression.quoteText(url.toString())} as "
"${namespace ?? "*"};";
}

View File

@ -15,6 +15,10 @@ import 'silent_comment.dart';
///
/// This defines or sets a variable.
class VariableDeclaration implements Statement {
/// The namespace of the variable being set, or `null` if it's defined or set
/// without a namespace.
final String namespace;
/// The name of the variable.
final String name;
@ -37,10 +41,18 @@ class VariableDeclaration implements Statement {
final FileSpan span;
VariableDeclaration(this.name, this.expression, this.span,
{bool guarded = false, bool global = false, SilentComment comment})
{this.namespace,
bool guarded = false,
bool global = false,
SilentComment comment})
: isGuarded = guarded,
isGlobal = global,
comment = comment;
comment = comment {
if (namespace != null && global) {
throw ArgumentError(
"Other modules' members can't be defined with !global.");
}
}
/// Parses a variable declaration from [contents].
///
@ -53,5 +65,10 @@ class VariableDeclaration implements Statement {
T accept<T>(StatementVisitor<T> visitor) =>
visitor.visitVariableDeclaration(this);
String toString() => "\$$name: $expression;";
String toString() {
var buffer = StringBuffer("\$");
if (namespace != null) buffer.write("$namespace.");
buffer.write("$name: $expression;");
return buffer.toString();
}
}

View File

@ -139,6 +139,7 @@ class CssParser extends ScssParser {
// as plain CSS, rather than calling a user-defined function.
Interpolation([StringExpression(identifier)], identifier.span),
ArgumentInvocation(
arguments, const {}, scanner.spanFrom(beforeArguments)));
arguments, const {}, scanner.spanFrom(beforeArguments)),
scanner.spanFrom(start));
}
}

View File

@ -17,7 +17,7 @@ import '../utils.dart';
/// This provides utility methods and common token parsing. Unless specified
/// otherwise, a parse method throws a [SassFormatException] if it fails to
/// parse.
abstract class Parser {
class Parser {
/// The scanner that scans through the text being parsed.
final SpanScanner scanner;
@ -25,10 +25,25 @@ abstract class Parser {
@protected
final Logger logger;
/// Parses [text] as a CSS identifier and returns the result.
///
/// Throws a [SassFormatException] if parsing fails.
static String parseIdentifier(String text, {Logger logger}) =>
Parser(text, logger: logger)._parseIdentifier();
@protected
Parser(String contents, {url, Logger logger})
: scanner = SpanScanner(contents, sourceUrl: url),
logger = logger ?? const Logger.stderr();
String _parseIdentifier() {
return wrapSpanFormatException(() {
var result = identifier();
scanner.expectDone();
return result;
});
}
// ## Tokens
/// Consumes whitespace, including any comments.
@ -114,8 +129,8 @@ abstract class Parser {
@protected
String identifier({bool unit = false}) {
// NOTE: this logic is largely duplicated in
// StylesheetParser._interpolatedIdentifier. Most changes here should be
// mirrored there.
// StylesheetParser._interpolatedIdentifier and isIdentifier in utils.dart.
// Most changes here should be mirrored there.
var text = StringBuffer();
while (scanner.scanChar($dash)) {

View File

@ -13,6 +13,7 @@ import 'package:tuple/tuple.dart';
import '../ast/sass.dart';
import '../color_names.dart';
import '../exception.dart';
import '../interpolation_buffer.dart';
import '../logger.dart';
import '../util/character.dart';
@ -21,6 +22,14 @@ import '../value.dart';
import '../value/color.dart';
import 'parser.dart';
/// Whether to parse `@use` rules.
///
/// This is set to `false` on Dart Sass's master branch and `true` on the
/// `feature.use` branch. It allows us to avoid having separate development
/// tracks as much as possible without shipping `@use` support until we're
/// ready.
const _parseUse = false;
/// The base class for both the SCSS and indented syntax parsers.
///
/// Having a base class that's separate from both parsers allows us to make
@ -33,6 +42,10 @@ import 'parser.dart';
/// private, except where they have to be public for subclasses to refer to
/// them.
abstract class StylesheetParser extends Parser {
/// Whether we've consumed a rule other than `@charset`, `@forward`, or
/// `@use`.
var _isUseAllowed = true;
/// Whether the parser is currently parsing the contents of a mixin
/// declaration.
var _inMixin = false;
@ -131,18 +144,21 @@ abstract class StylesheetParser extends Parser {
case $plus:
if (!indented || !lookingAtIdentifier(1)) return _styleRule();
_isUseAllowed = false;
var start = scanner.state;
scanner.readChar();
return _includeRule(start);
case $equal:
if (!indented) return _styleRule();
_isUseAllowed = false;
var start = scanner.state;
scanner.readChar();
whitespace();
return _mixinRule(start);
default:
_isUseAllowed = false;
return _inStyleRule || _inUnknownAtRule || _inMixin || _inContentBlock
? _declarationOrStyleRule()
: _styleRule();
@ -155,7 +171,13 @@ abstract class StylesheetParser extends Parser {
var precedingComment = lastSilentComment;
lastSilentComment = null;
var start = scanner.state;
String namespace;
var name = variableName();
if (scanner.scanChar($dot)) {
namespace = name;
name = _publicIdentifier();
}
if (plainCss) {
error("Sass variables aren't allowed in plain CSS.",
@ -170,23 +192,32 @@ abstract class StylesheetParser extends Parser {
var guarded = false;
var global = false;
while (scanner.scanChar($exclamation)) {
var flagStart = scanner.state;
while (scanner.scanChar($exclamation)) {
var flag = identifier();
if (flag == 'default') {
guarded = true;
} else if (flag == 'global') {
if (namespace != null) {
error("!global isn't allowed for variables in other modules.",
scanner.spanFrom(flagStart));
}
global = true;
} else {
error("Invalid flag name.", scanner.spanFrom(flagStart));
}
whitespace();
flagStart = scanner.state;
}
expectStatementSeparator("variable declaration");
return VariableDeclaration(name, value, scanner.spanFrom(start),
guarded: guarded, global: global, comment: precedingComment);
namespace: namespace,
guarded: guarded,
global: global,
comment: precedingComment);
}
/// Consumes a style rule.
@ -455,10 +486,18 @@ abstract class StylesheetParser extends Parser {
var name = interpolatedIdentifier();
whitespace();
// We want to set [_isUseAllowed] to `false` *unless* we're parsing
// `@charset`, `@forward`, or `@use`. To avoid double-comparing the rule
// name, we always set it to `false` and then set it back to its previous
// value if we're parsing an allowed rule.
var wasUseAllowed = _isUseAllowed;
_isUseAllowed = false;
switch (name.asPlain) {
case "at-root":
return _atRootRule(start);
case "charset":
_isUseAllowed = wasUseAllowed;
if (!root) _disallowedAtRule(start);
string();
return null;
@ -494,6 +533,10 @@ abstract class StylesheetParser extends Parser {
return _disallowedAtRule(start);
case "supports":
return supportsRule(start);
case "use":
_isUseAllowed = wasUseAllowed;
if (!root) _disallowedAtRule(start);
return _useRule(start);
case "warn":
return _warnRule(start);
case "while":
@ -933,7 +976,13 @@ abstract class StylesheetParser extends Parser {
///
/// [start] should point before the `@`.
IncludeRule _includeRule(LineScannerState start) {
String namespace;
var name = identifier();
if (scanner.scanChar($dot)) {
namespace = name;
name = _publicIdentifier();
}
whitespace();
var arguments = scanner.peekChar() == $lparen
? _argumentInvocation(mixin: true)
@ -965,7 +1014,8 @@ abstract class StylesheetParser extends Parser {
var span =
scanner.spanFrom(start, start).expand((content ?? arguments).span);
return IncludeRule(name, arguments, span, content: content);
return IncludeRule(name, arguments, span,
namespace: namespace, content: content);
}
/// Consumes a `@media` rule.
@ -1115,6 +1165,49 @@ relase. For details, see http://bit.ly/moz-document.
(children, span) => SupportsRule(condition, children, span));
}
/// Consumes a `@use` rule.
///
/// [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));
}
whitespace();
String namespace;
if (scanIdentifier("as")) {
whitespace();
namespace = scanner.scanChar($asterisk) ? null : identifier();
} else {
var basename = url.pathSegments.isEmpty ? "" : url.pathSegments.last;
var dot = basename.indexOf(".");
namespace = basename.substring(0, dot == -1 ? basename.length : dot);
try {
namespace = Parser.parseIdentifier(namespace, logger: logger);
} on SassFormatException {
error('Invalid Sass identifier "$namespace"', scanner.spanFrom(start));
}
}
expectStatementSeparator("@use rule");
var span = scanner.spanFrom(start);
if (!_parseUse) {
error(
"@use is coming soon, but it's not supported in this version of "
"Dart Sass.",
span);
} else if (!_isUseAllowed) {
error("@use rules must be written before any other rules.", span);
}
return UseRule(url, namespace, span);
}
/// Consumes a `@warn` rule.
///
/// [start] should point before the `@`.
@ -2143,11 +2236,22 @@ relase. For details, see http://bit.ly/moz-document.
/// Consumes a variable expression.
VariableExpression _variable() {
var start = scanner.state;
var name = variableName();
if (!plainCss) return VariableExpression(name, scanner.spanFrom(start));
error(
"Sass variables aren't allowed in plain CSS.", scanner.spanFrom(start));
String namespace;
var name = variableName();
if (scanner.peekChar() == $dot && scanner.peekChar(1) != $dot) {
scanner.readChar();
namespace = name;
name = _publicIdentifier();
}
if (plainCss) {
error("Sass variables aren't allowed in plain CSS.",
scanner.spanFrom(start));
}
return VariableExpression(name, scanner.spanFrom(start),
namespace: namespace);
}
/// Consumes a selector expression.
@ -2254,9 +2358,31 @@ relase. For details, see http://bit.ly/moz-document.
if (specialFunction != null) return specialFunction;
}
return scanner.peekChar() == $lparen
? FunctionExpression(identifier, _argumentInvocation())
: StringExpression(identifier);
switch (scanner.peekChar()) {
case $dot:
if (scanner.peekChar(1) == $dot) return StringExpression(identifier);
var namespace = identifier.asPlain;
scanner.readChar();
var beforeName = scanner.state;
var name = Interpolation(
[this._publicIdentifier()], scanner.spanFrom(beforeName));
if (namespace == null) {
error("Interpolation isn't allowed in namespaces.", identifier.span);
}
return FunctionExpression(
name, _argumentInvocation(), scanner.spanFrom(start),
namespace: namespace);
case $lparen:
return FunctionExpression(
identifier, _argumentInvocation(), scanner.spanFrom(start));
default:
return StringExpression(identifier);
}
}
/// If [name] is the name of a function with special syntax, consumes it.
@ -2510,8 +2636,8 @@ relase. For details, see http://bit.ly/moz-document.
var contents = _tryUrlContents(start);
if (contents != null) return StringExpression(contents);
return FunctionExpression(
Interpolation(["url"], scanner.spanFrom(start)), _argumentInvocation());
return FunctionExpression(Interpolation(["url"], scanner.spanFrom(start)),
_argumentInvocation(), scanner.spanFrom(start));
}
/// Consumes tokens up to "{", "}", ";", or "!".
@ -2620,8 +2746,8 @@ relase. For details, see http://bit.ly/moz-document.
///
/// Unlike [declarationValue], this allows interpolation.
StringExpression _interpolatedDeclarationValue({bool allowEmpty = false}) {
// NOTE: this logic is largely duplicated in Parser.declarationValue. Most
// changes here should be mirrored there.
// NOTE: this logic is largely duplicated in Parser.declarationValue and
// isIdentifier in utils.dart. Most changes here should be mirrored there.
var start = scanner.state;
var buffer = InterpolationBuffer();
@ -3088,6 +3214,20 @@ relase. For details, see http://bit.ly/moz-document.
return result;
}
/// Like [identifier], but rejects identifiers that begin with `_` or `-`.
String _publicIdentifier() {
var start = scanner.state;
var result = identifier();
var first = result.codeUnitAt(0);
if (first == $dash || first == $underscore) {
error("Private members can't be accessed from outside their modules.",
scanner.spanFrom(start));
}
return result;
}
// ## Abstract Methods
/// Whether this is parsing the indented syntax.

View File

@ -290,7 +290,9 @@ class _EvaluateVisitor
deprecation: true);
var expression = FunctionExpression(
Interpolation([function.text], _callableNode.span), invocation);
Interpolation([function.text], _callableNode.span),
invocation,
_callableNode.span);
return await expression.accept(this);
}
@ -1130,6 +1132,10 @@ class _EvaluateVisitor
return null;
}
Future<Value> visitUseRule(UseRule node) async {
return null;
}
Future<Value> visitWarnRule(WarnRule node) async {
var value =
await _addExceptionSpanAsync(node, () => node.expression.accept(this));

View File

@ -5,7 +5,7 @@
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/synchronize.dart for details.
//
// Checksum: 262c642b0cda0a5ed4984e763e9970422405efaa
// Checksum: 08c3aaa09f3be71dd315bf36665e249983ce3d53
//
// ignore_for_file: unused_import
@ -297,7 +297,9 @@ class _EvaluateVisitor
deprecation: true);
var expression = FunctionExpression(
Interpolation([function.text], _callableNode.span), invocation);
Interpolation([function.text], _callableNode.span),
invocation,
_callableNode.span);
return expression.accept(this);
}
@ -1125,6 +1127,10 @@ class _EvaluateVisitor
return null;
}
Value visitUseRule(UseRule node) {
return null;
}
Value visitWarnRule(WarnRule node) {
var value = _addExceptionSpan(node, () => node.expression.accept(this));
_logger.warn(

View File

@ -28,6 +28,10 @@ class _FindImportsVisitor extends RecursiveStatementVisitor {
void visitInterpolation(Interpolation interpolation) {}
void visitSupportsCondition(SupportsCondition condition) {}
void visitUseRule(UseRule node) {
_imports.add(DynamicImport(node.url.toString(), node.span));
}
void visitImportRule(ImportRule node) {
for (var import in node.imports) {
if (import is DynamicImport) _imports.add(import);

View File

@ -30,6 +30,7 @@ abstract class StatementVisitor<T> {
T visitStyleRule(StyleRule node);
T visitStylesheet(Stylesheet node);
T visitSupportsRule(SupportsRule node);
T visitUseRule(UseRule node);
T visitVariableDeclaration(VariableDeclaration node);
T visitWarnRule(WarnRule node);
T visitWhileRule(WhileRule node);