From c8979f52f2992140f180117f35aa21d692a74e79 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 8 Dec 2017 14:59:16 -0800 Subject: [PATCH 1/2] Annotate protected parser methods --- lib/src/parse/parser.dart | 26 ++++++++++++++++++++++++++ lib/src/parse/stylesheet.dart | 13 +++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 5e176f71..0a8c4e3a 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -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 string—that 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 callback()) { try { return callback(); diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 237b191c..0de41ecb 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -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(); @@ -2713,12 +2714,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 +2730,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 +2748,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 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 statements(Statement statement()); } From fa2924895581ae76271458a197dde8cc07f79c99 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 8 Dec 2017 15:28:48 -0800 Subject: [PATCH 2/2] Support unquoted imports in the indented syntax Closes #202 --- CHANGELOG.md | 2 ++ lib/src/parse/sass.dart | 34 ++++++++++++++++++++++++++++++++++ lib/src/parse/stylesheet.dart | 18 +++++++++++------- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbb1ef39..df7cc04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(...))`. diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index 2ebfaca9..e70df084 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -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; diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 0de41ecb..239e02c3 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -698,7 +698,12 @@ abstract class StylesheetParser extends Parser { var imports = []; 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"); @@ -709,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) { @@ -729,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); } @@ -742,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);