Skip to content

Commit 088493e

Browse files
authored
Merge pull request #204 from sass/unquoted-import
Support unquoted imports in the indented syntax
2 parents d8ffd47 + fa29248 commit 088493e

File tree

4 files changed

+84
-9
lines changed

4 files changed

+84
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## 1.0.0-beta.4
22

3+
* Support unquoted imports in the indented syntax.
4+
35
* Fix a crash when `:not(...)` extends a selector that appears in
46
`:not(:not(...))`.
57

lib/src/parse/parser.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// https://opensource.org/licenses/MIT.
44

55
import 'package:charcode/charcode.dart';
6+
import 'package:meta/meta.dart';
67
import 'package:source_span/source_span.dart';
78
import 'package:string_scanner/string_scanner.dart';
89

@@ -24,20 +25,23 @@ abstract class Parser {
2425
// ## Tokens
2526

2627
/// Consumes whitespace, including any comments.
28+
@protected
2729
void whitespace() {
2830
do {
2931
whitespaceWithoutComments();
3032
} while (scanComment());
3133
}
3234

3335
/// Like [whitespace], but returns whether any was consumed.
36+
@protected
3437
bool scanWhitespace() {
3538
var start = scanner.position;
3639
whitespace();
3740
return scanner.position != start;
3841
}
3942

4043
/// Consumes whitespace, but not comments.
44+
@protected
4145
void whitespaceWithoutComments() {
4246
while (!scanner.isDone && isWhitespace(scanner.peekChar())) {
4347
scanner.readChar();
@@ -47,6 +51,7 @@ abstract class Parser {
4751
/// Consumes and ignores a comment if possible.
4852
///
4953
/// Returns whether the comment was consumed.
54+
@protected
5055
bool scanComment() {
5156
if (scanner.peekChar() != $slash) return false;
5257

@@ -63,6 +68,7 @@ abstract class Parser {
6368
}
6469

6570
/// Consumes and ignores a silent (Sass-style) comment.
71+
@protected
6672
void silentComment() {
6773
scanner.expect("//");
6874
while (!scanner.isDone && !isNewline(scanner.peekChar())) {
@@ -71,6 +77,7 @@ abstract class Parser {
7177
}
7278

7379
/// Consumes and ignores a loud (CSS-style) comment.
80+
@protected
7481
void loudComment() {
7582
scanner.expect("/*");
7683
while (true) {
@@ -89,6 +96,7 @@ abstract class Parser {
8996
/// If [unit] is `true`, this doesn't parse a `-` followed by a digit. This
9097
/// ensures that `1px-2px` parses as subtraction rather than the unit
9198
/// `px-2px`.
99+
@protected
92100
String identifier({bool unit: false}) {
93101
// NOTE: this logic is largely duplicated in ScssParser.identifier.
94102
// Most changes here should be mirrored there.
@@ -114,6 +122,7 @@ abstract class Parser {
114122
}
115123

116124
/// Consumes a chunk of a plain CSS identifier after the name start.
125+
@protected
117126
String identifierBody() {
118127
var text = new StringBuffer();
119128
_identifierBody(text);
@@ -146,6 +155,7 @@ abstract class Parser {
146155
///
147156
/// This returns the parsed contents of the string—that is, it doesn't include
148157
/// quotes and its escapes are resolved.
158+
@protected
149159
String string() {
150160
// NOTE: this logic is largely duplicated in ScssParser._interpolatedString.
151161
// Most changes here should be mirrored there.
@@ -181,6 +191,7 @@ abstract class Parser {
181191

182192
/// Consumes tokens until it reaches a top-level `":"`, `"!"`, `")"`, `"]"`,
183193
/// or `"}"` and returns their contents as a string.
194+
@protected
184195
String declarationValue() {
185196
// NOTE: this logic is largely duplicated in
186197
// StylesheetParser._interpolatedDeclarationValue. Most changes here should
@@ -281,6 +292,7 @@ abstract class Parser {
281292
}
282293

283294
/// Consumes a `url()` token if possible, and returns `null` otherwise.
295+
@protected
284296
String tryUrl() {
285297
// NOTE: this logic is largely duplicated in ScssParser._urlContents and
286298
// ScssParser._tryUrlContents. Most changes here should be mirrored there.
@@ -327,6 +339,7 @@ abstract class Parser {
327339

328340
/// Consumes a Sass variable name, and returns its name without the dollar
329341
/// sign.
342+
@protected
330343
String variableName() {
331344
scanner.expectChar($dollar);
332345
return identifier();
@@ -335,6 +348,7 @@ abstract class Parser {
335348
// ## Characters
336349

337350
/// Consumes an escape sequence and returns the text that defines it.
351+
@protected
338352
String escape() {
339353
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
340354

@@ -365,6 +379,7 @@ abstract class Parser {
365379
}
366380

367381
/// Consumes an escape sequence and returns the character it represents.
382+
@protected
368383
int escapeCharacter() {
369384
// See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
370385

@@ -399,6 +414,7 @@ abstract class Parser {
399414
// Consumes the next character if it matches [condition].
400415
//
401416
// Returns whether or not the character was consumed.
417+
@protected
402418
bool scanCharIf(bool condition(int character)) {
403419
var next = scanner.peekChar();
404420
if (!condition(next)) return false;
@@ -408,6 +424,7 @@ abstract class Parser {
408424

409425
/// Consumes the next character if it's equal to [letter], ignoring ASCII
410426
/// case.
427+
@protected
411428
bool scanCharIgnoreCase(int letter) {
412429
if (!equalsLetterIgnoreCase(letter, scanner.peekChar())) return false;
413430
scanner.readChar();
@@ -416,6 +433,7 @@ abstract class Parser {
416433

417434
/// Consumes the next character and asserts that it's equal to [letter],
418435
/// ignoring ASCII case.
436+
@protected
419437
void expectCharIgnoreCase(int letter) {
420438
var actual = scanner.readChar();
421439
if (equalsLetterIgnoreCase(letter, actual)) return;
@@ -431,6 +449,7 @@ abstract class Parser {
431449
/// This follows [the CSS algorithm][].
432450
///
433451
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#starts-with-a-number
452+
@protected
434453
bool lookingAtNumber() {
435454
var first = scanner.peekChar();
436455
if (first == null) return false;
@@ -460,6 +479,7 @@ abstract class Parser {
460479
/// start escapes.
461480
///
462481
/// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier
482+
@protected
463483
bool lookingAtIdentifier([int forward]) {
464484
// See also [ScssParser._lookingAtInterpolatedIdentifier].
465485

@@ -480,6 +500,7 @@ abstract class Parser {
480500

481501
/// Returns whether the scanner is immediately before a sequence of characters
482502
/// that could be part of a plain CSS identifier body.
503+
@protected
483504
bool lookingAtIdentifierBody() {
484505
var next = scanner.peekChar();
485506
return next != null && (isName(next) || next == $backslash);
@@ -488,6 +509,7 @@ abstract class Parser {
488509
/// Consumes an identifier if its name exactly matches [text].
489510
///
490511
/// If [ignoreCase] is `true`, does a case-insensitive match.
512+
@protected
491513
bool scanIdentifier(String text, {bool ignoreCase: false}) {
492514
if (!lookingAtIdentifier()) return false;
493515

@@ -507,6 +529,7 @@ abstract class Parser {
507529
/// Consumes an identifier and asserts that its name exactly matches [text].
508530
///
509531
/// If [ignoreCase] is `true`, does a case-insensitive match.
532+
@protected
510533
void expectIdentifier(String text, {String name, bool ignoreCase: false}) {
511534
name ??= '"$text"';
512535

@@ -522,6 +545,7 @@ abstract class Parser {
522545
}
523546

524547
/// Runs [consumer] and returns the source text that it consumes.
548+
@protected
525549
String rawText(void consumer()) {
526550
var start = scanner.position;
527551
consumer();
@@ -532,6 +556,7 @@ abstract class Parser {
532556
///
533557
/// If [message] is passed, prints that as well. This is intended for use when
534558
/// debugging parser failures.
559+
@protected
535560
void debug([message]) {
536561
if (message == null) {
537562
print(scanner.emptySpan.highlight(color: true));
@@ -542,6 +567,7 @@ abstract class Parser {
542567

543568
/// Runs [callback] and wraps any [SourceSpanFormatException] it throws in a
544569
/// [SassFormatException].
570+
@protected
545571
T wrapSpanFormatException<T>(T callback()) {
546572
try {
547573
return callback();

lib/src/parse/sass.dart

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,40 @@ class SassParser extends StylesheetParser {
5959
bool lookingAtChildren() =>
6060
atEndOfStatement() && _peekIndentation() > currentIndentation;
6161

62+
Import importArgument() {
63+
switch (scanner.peekChar()) {
64+
case $u:
65+
case $U:
66+
var start = scanner.state;
67+
if (scanIdentifier("url", ignoreCase: true)) {
68+
if (scanner.scanChar($lparen)) {
69+
scanner.state = start;
70+
return super.importArgument();
71+
} else {
72+
scanner.state = start;
73+
}
74+
}
75+
break;
76+
77+
case $single_quote:
78+
case $double_quote:
79+
return super.importArgument();
80+
}
81+
82+
var start = scanner.state;
83+
var next = scanner.peekChar();
84+
while (next != null &&
85+
next != $comma &&
86+
next != $semicolon &&
87+
!isNewline(next)) {
88+
scanner.readChar();
89+
next = scanner.peekChar();
90+
}
91+
92+
return new DynamicImport(parseImportUrl(scanner.substring(start.position)),
93+
scanner.spanFrom(start));
94+
}
95+
6296
bool scanElse(int ifIndentation) {
6397
if (_peekIndentation() != ifIndentation) return false;
6498
var start = scanner.state;

lib/src/parse/stylesheet.dart

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:math' as math;
66

77
import 'package:charcode/charcode.dart';
8+
import 'package:meta/meta.dart';
89
import 'package:path/path.dart' as p;
910
import 'package:string_scanner/string_scanner.dart';
1011
import 'package:tuple/tuple.dart';
@@ -57,8 +58,7 @@ abstract class StylesheetParser extends Parser {
5758
var _inParentheses = false;
5859

5960
/// Whether warnings should be emitted using terminal colors.
60-
///
61-
/// This is protected and shouldn't be accessed except by subclasses.
61+
@protected
6262
final bool color;
6363

6464
StylesheetParser(String contents, {url, this.color: false})
@@ -114,6 +114,7 @@ abstract class StylesheetParser extends Parser {
114114
}
115115

116116
/// Consumes a variable declaration.
117+
@protected
117118
VariableDeclaration variableDeclaration() {
118119
var start = scanner.state;
119120
var name = variableName();
@@ -697,7 +698,12 @@ abstract class StylesheetParser extends Parser {
697698
var imports = <Import>[];
698699
do {
699700
whitespace();
700-
imports.add(_importArgument(start));
701+
var argument = importArgument();
702+
if ((_inControlDirective || _inMixin) && argument is DynamicImport) {
703+
_disallowedAtRule(start);
704+
}
705+
706+
imports.add(argument);
701707
whitespace();
702708
} while (scanner.scanChar($comma));
703709
expectStatementSeparator("@import rule");
@@ -708,7 +714,8 @@ abstract class StylesheetParser extends Parser {
708714
/// Consumes an argument to an `@import` rule.
709715
///
710716
/// [ruleStart] should point before the `@`.
711-
Import _importArgument(LineScannerState ruleStart) {
717+
@protected
718+
Import importArgument() {
712719
var start = scanner.state;
713720
var next = scanner.peekChar();
714721
if (next == $u || next == $U) {
@@ -728,20 +735,18 @@ abstract class StylesheetParser extends Parser {
728735
return new StaticImport(
729736
new Interpolation([urlSpan.text], urlSpan), scanner.spanFrom(start),
730737
supports: queries?.item1, media: queries?.item2);
731-
} else if (_inControlDirective || _inMixin) {
732-
_disallowedAtRule(ruleStart);
733-
return null;
734738
} else {
735739
try {
736-
return new DynamicImport(_parseImportUrl(url), urlSpan);
740+
return new DynamicImport(parseImportUrl(url), urlSpan);
737741
} on FormatException catch (error) {
738742
throw new SassFormatException("Invalid URL: ${error.message}", urlSpan);
739743
}
740744
}
741745
}
742746

743747
/// Parses [url] as an import URL.
744-
Uri _parseImportUrl(String url) {
748+
@protected
749+
Uri parseImportUrl(String url) {
745750
// Backwards-compatibility for implementations that allow absolute Windows
746751
// paths in imports.
747752
if (p.windows.isAbsolute(url)) return p.windows.toUri(url);
@@ -2713,12 +2718,14 @@ abstract class StylesheetParser extends Parser {
27132718
// ## Abstract Methods
27142719

27152720
/// Whether this is parsing the indented syntax.
2721+
@protected
27162722
bool get indented;
27172723

27182724
/// The indentation level at the current scanner position.
27192725
///
27202726
/// This value isn't used directly by [StylesheetParser]; it's just passed to
27212727
/// [scanElse].
2728+
@protected
27222729
int get currentIndentation;
27232730

27242731
/// Asserts that the scanner is positioned before a statement separator, or at
@@ -2727,13 +2734,16 @@ abstract class StylesheetParser extends Parser {
27272734
/// If the [name] of the parent rule is passed, it's used for error reporting.
27282735
///
27292736
/// This consumes whitespace, but nothing else, including comments.
2737+
@protected
27302738
void expectStatementSeparator([String name]);
27312739

27322740
/// Whether the scanner is positioned at the end of a statement.
2741+
@protected
27332742
bool atEndOfStatement();
27342743

27352744
/// Whether the scanner is positioned before a block of children that can be
27362745
/// parsed with [children].
2746+
@protected
27372747
bool lookingAtChildren();
27382748

27392749
/// Tries to scan an `@else` rule after an `@if` block, and returns whether
@@ -2742,14 +2752,17 @@ abstract class StylesheetParser extends Parser {
27422752
/// This should just scan the rule name, not anything afterwards.
27432753
/// [ifIndentation] is the result of [currentIndentation] from before the
27442754
/// corresponding `@if` was parsed.
2755+
@protected
27452756
bool scanElse(int ifIndentation);
27462757

27472758
/// Consumes a block of child statements.
2759+
@protected
27482760
List<Statement> children(Statement child());
27492761

27502762
/// Consumes top-level statements.
27512763
///
27522764
/// The [statement] callback may return `null`, indicating that a statement
27532765
/// was consumed that shouldn't be added to the AST.
2766+
@protected
27542767
List<Statement> statements(Statement statement());
27552768
}

0 commit comments

Comments
 (0)