Merge pull request #204 from sass/unquoted-import

Support unquoted imports in the indented syntax
This commit is contained in:
Natalie Weizenbaum 2017-12-08 15:45:28 -08:00 committed by GitHub
commit 088493e49d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 84 additions and 9 deletions

View File

@ -1,5 +1,7 @@
## 1.0.0-beta.4
* Support unquoted imports in the indented syntax.
* Fix a crash when `:not(...)` extends a selector that appears in
`:not(:not(...))`.

View File

@ -3,6 +3,7 @@
// https://opensource.org/licenses/MIT.
import 'package:charcode/charcode.dart';
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
import 'package:string_scanner/string_scanner.dart';
@ -24,6 +25,7 @@ abstract class Parser {
// ## Tokens
/// Consumes whitespace, including any comments.
@protected
void whitespace() {
do {
whitespaceWithoutComments();
@ -31,6 +33,7 @@ abstract class Parser {
}
/// Like [whitespace], but returns whether any was consumed.
@protected
bool scanWhitespace() {
var start = scanner.position;
whitespace();
@ -38,6 +41,7 @@ abstract class Parser {
}
/// Consumes whitespace, but not comments.
@protected
void whitespaceWithoutComments() {
while (!scanner.isDone && isWhitespace(scanner.peekChar())) {
scanner.readChar();
@ -47,6 +51,7 @@ abstract class Parser {
/// Consumes and ignores a comment if possible.
///
/// Returns whether the comment was consumed.
@protected
bool scanComment() {
if (scanner.peekChar() != $slash) return false;
@ -63,6 +68,7 @@ abstract class Parser {
}
/// Consumes and ignores a silent (Sass-style) comment.
@protected
void silentComment() {
scanner.expect("//");
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
@ -71,6 +77,7 @@ abstract class Parser {
}
/// Consumes and ignores a loud (CSS-style) comment.
@protected
void loudComment() {
scanner.expect("/*");
while (true) {
@ -89,6 +96,7 @@ abstract class Parser {
/// If [unit] is `true`, this doesn't parse a `-` followed by a digit. This
/// ensures that `1px-2px` parses as subtraction rather than the unit
/// `px-2px`.
@protected
String identifier({bool unit: false}) {
// NOTE: this logic is largely duplicated in ScssParser.identifier.
// Most changes here should be mirrored there.
@ -114,6 +122,7 @@ abstract class Parser {
}
/// Consumes a chunk of a plain CSS identifier after the name start.
@protected
String identifierBody() {
var text = new StringBuffer();
_identifierBody(text);
@ -146,6 +155,7 @@ abstract class Parser {
///
/// This returns the parsed contents of the stringthat is, it doesn't include
/// quotes and its escapes are resolved.
@protected
String string() {
// NOTE: this logic is largely duplicated in ScssParser._interpolatedString.
// Most changes here should be mirrored there.
@ -181,6 +191,7 @@ abstract class Parser {
/// Consumes tokens until it reaches a top-level `":"`, `"!"`, `")"`, `"]"`,
/// or `"}"` and returns their contents as a string.
@protected
String declarationValue() {
// NOTE: this logic is largely duplicated in
// StylesheetParser._interpolatedDeclarationValue. Most changes here should
@ -281,6 +292,7 @@ abstract class Parser {
}
/// Consumes a `url()` token if possible, and returns `null` otherwise.
@protected
String tryUrl() {
// NOTE: this logic is largely duplicated in ScssParser._urlContents and
// ScssParser._tryUrlContents. Most changes here should be mirrored there.
@ -327,6 +339,7 @@ abstract class Parser {
/// Consumes a Sass variable name, and returns its name without the dollar
/// sign.
@protected
String variableName() {
scanner.expectChar($dollar);
return identifier();
@ -335,6 +348,7 @@ abstract class Parser {
// ## Characters
/// Consumes an escape sequence and returns the text that defines it.
@protected
String escape() {
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
@ -365,6 +379,7 @@ abstract class Parser {
}
/// Consumes an escape sequence and returns the character it represents.
@protected
int escapeCharacter() {
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
@ -399,6 +414,7 @@ abstract class Parser {
// Consumes the next character if it matches [condition].
//
// Returns whether or not the character was consumed.
@protected
bool scanCharIf(bool condition(int character)) {
var next = scanner.peekChar();
if (!condition(next)) return false;
@ -408,6 +424,7 @@ abstract class Parser {
/// Consumes the next character if it's equal to [letter], ignoring ASCII
/// case.
@protected
bool scanCharIgnoreCase(int letter) {
if (!equalsLetterIgnoreCase(letter, scanner.peekChar())) return false;
scanner.readChar();
@ -416,6 +433,7 @@ abstract class Parser {
/// Consumes the next character and asserts that it's equal to [letter],
/// ignoring ASCII case.
@protected
void expectCharIgnoreCase(int letter) {
var actual = scanner.readChar();
if (equalsLetterIgnoreCase(letter, actual)) return;
@ -431,6 +449,7 @@ abstract class Parser {
/// This follows [the CSS algorithm][].
///
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#starts-with-a-number
@protected
bool lookingAtNumber() {
var first = scanner.peekChar();
if (first == null) return false;
@ -460,6 +479,7 @@ abstract class Parser {
/// start escapes.
///
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
@protected
bool lookingAtIdentifier([int forward]) {
// See also [ScssParser._lookingAtInterpolatedIdentifier].
@ -480,6 +500,7 @@ abstract class Parser {
/// Returns whether the scanner is immediately before a sequence of characters
/// that could be part of a plain CSS identifier body.
@protected
bool lookingAtIdentifierBody() {
var next = scanner.peekChar();
return next != null && (isName(next) || next == $backslash);
@ -488,6 +509,7 @@ abstract class Parser {
/// Consumes an identifier if its name exactly matches [text].
///
/// If [ignoreCase] is `true`, does a case-insensitive match.
@protected
bool scanIdentifier(String text, {bool ignoreCase: false}) {
if (!lookingAtIdentifier()) return false;
@ -507,6 +529,7 @@ abstract class Parser {
/// Consumes an identifier and asserts that its name exactly matches [text].
///
/// If [ignoreCase] is `true`, does a case-insensitive match.
@protected
void expectIdentifier(String text, {String name, bool ignoreCase: false}) {
name ??= '"$text"';
@ -522,6 +545,7 @@ abstract class Parser {
}
/// Runs [consumer] and returns the source text that it consumes.
@protected
String rawText(void consumer()) {
var start = scanner.position;
consumer();
@ -532,6 +556,7 @@ abstract class Parser {
///
/// If [message] is passed, prints that as well. This is intended for use when
/// debugging parser failures.
@protected
void debug([message]) {
if (message == null) {
print(scanner.emptySpan.highlight(color: true));
@ -542,6 +567,7 @@ abstract class Parser {
/// Runs [callback] and wraps any [SourceSpanFormatException] it throws in a
/// [SassFormatException].
@protected
T wrapSpanFormatException<T>(T callback()) {
try {
return callback();

View File

@ -59,6 +59,40 @@ class SassParser extends StylesheetParser {
bool lookingAtChildren() =>
atEndOfStatement() && _peekIndentation() > currentIndentation;
Import importArgument() {
switch (scanner.peekChar()) {
case $u:
case $U:
var start = scanner.state;
if (scanIdentifier("url", ignoreCase: true)) {
if (scanner.scanChar($lparen)) {
scanner.state = start;
return super.importArgument();
} else {
scanner.state = start;
}
}
break;
case $single_quote:
case $double_quote:
return super.importArgument();
}
var start = scanner.state;
var next = scanner.peekChar();
while (next != null &&
next != $comma &&
next != $semicolon &&
!isNewline(next)) {
scanner.readChar();
next = scanner.peekChar();
}
return new DynamicImport(parseImportUrl(scanner.substring(start.position)),
scanner.spanFrom(start));
}
bool scanElse(int ifIndentation) {
if (_peekIndentation() != ifIndentation) return false;
var start = scanner.state;

View File

@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'package:charcode/charcode.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import 'package:string_scanner/string_scanner.dart';
import 'package:tuple/tuple.dart';
@ -57,8 +58,7 @@ abstract class StylesheetParser extends Parser {
var _inParentheses = false;
/// Whether warnings should be emitted using terminal colors.
///
/// This is protected and shouldn't be accessed except by subclasses.
@protected
final bool color;
StylesheetParser(String contents, {url, this.color: false})
@ -114,6 +114,7 @@ abstract class StylesheetParser extends Parser {
}
/// Consumes a variable declaration.
@protected
VariableDeclaration variableDeclaration() {
var start = scanner.state;
var name = variableName();
@ -697,7 +698,12 @@ abstract class StylesheetParser extends Parser {
var imports = <Import>[];
do {
whitespace();
imports.add(_importArgument(start));
var argument = importArgument();
if ((_inControlDirective || _inMixin) && argument is DynamicImport) {
_disallowedAtRule(start);
}
imports.add(argument);
whitespace();
} while (scanner.scanChar($comma));
expectStatementSeparator("@import rule");
@ -708,7 +714,8 @@ abstract class StylesheetParser extends Parser {
/// Consumes an argument to an `@import` rule.
///
/// [ruleStart] should point before the `@`.
Import _importArgument(LineScannerState ruleStart) {
@protected
Import importArgument() {
var start = scanner.state;
var next = scanner.peekChar();
if (next == $u || next == $U) {
@ -728,12 +735,9 @@ abstract class StylesheetParser extends Parser {
return new StaticImport(
new Interpolation([urlSpan.text], urlSpan), scanner.spanFrom(start),
supports: queries?.item1, media: queries?.item2);
} else if (_inControlDirective || _inMixin) {
_disallowedAtRule(ruleStart);
return null;
} else {
try {
return new DynamicImport(_parseImportUrl(url), urlSpan);
return new DynamicImport(parseImportUrl(url), urlSpan);
} on FormatException catch (error) {
throw new SassFormatException("Invalid URL: ${error.message}", urlSpan);
}
@ -741,7 +745,8 @@ abstract class StylesheetParser extends Parser {
}
/// Parses [url] as an import URL.
Uri _parseImportUrl(String url) {
@protected
Uri parseImportUrl(String url) {
// Backwards-compatibility for implementations that allow absolute Windows
// paths in imports.
if (p.windows.isAbsolute(url)) return p.windows.toUri(url);
@ -2713,12 +2718,14 @@ abstract class StylesheetParser extends Parser {
// ## Abstract Methods
/// Whether this is parsing the indented syntax.
@protected
bool get indented;
/// The indentation level at the current scanner position.
///
/// This value isn't used directly by [StylesheetParser]; it's just passed to
/// [scanElse].
@protected
int get currentIndentation;
/// Asserts that the scanner is positioned before a statement separator, or at
@ -2727,13 +2734,16 @@ abstract class StylesheetParser extends Parser {
/// If the [name] of the parent rule is passed, it's used for error reporting.
///
/// This consumes whitespace, but nothing else, including comments.
@protected
void expectStatementSeparator([String name]);
/// Whether the scanner is positioned at the end of a statement.
@protected
bool atEndOfStatement();
/// Whether the scanner is positioned before a block of children that can be
/// parsed with [children].
@protected
bool lookingAtChildren();
/// Tries to scan an `@else` rule after an `@if` block, and returns whether
@ -2742,14 +2752,17 @@ abstract class StylesheetParser extends Parser {
/// This should just scan the rule name, not anything afterwards.
/// [ifIndentation] is the result of [currentIndentation] from before the
/// corresponding `@if` was parsed.
@protected
bool scanElse(int ifIndentation);
/// Consumes a block of child statements.
@protected
List<Statement> children(Statement child());
/// Consumes top-level statements.
///
/// The [statement] callback may return `null`, indicating that a statement
/// was consumed that shouldn't be added to the AST.
@protected
List<Statement> statements(Statement statement());
}