diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index 08844998..932e7aa5 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -11,7 +11,6 @@ import 'stylesheet.dart'; /// A parser for the indented syntax. class SassParser extends StylesheetParser { - /// The indentation level at the current scanner position. int get currentIndentation => _currentIndentation; var _currentIndentation = 0; @@ -87,6 +86,11 @@ class SassParser extends StylesheetParser { return statements; } + /// Consumes a child of the current statement. + /// + /// This consumes children that are allowed at all levels of the document; the + /// [child] parameter is called to consume any children that are specifically + /// allowed in the caller's context. Statement _child(Statement child()) { switch (scanner.peekChar()) { case $dollar: @@ -113,6 +117,7 @@ class SassParser extends StylesheetParser { } } + /// Consumes an indented-style silent comment. Comment _silentComment() { var start = scanner.state; scanner.expect("//"); @@ -141,6 +146,7 @@ class SassParser extends StylesheetParser { silent: true); } + /// Consumes an indented-style loud context. Comment _loudComment() { var start = scanner.state; scanner.expect("/*"); @@ -173,8 +179,9 @@ class SassParser extends StylesheetParser { silent: false); } - // Doesn't consume newlines, doesn't support comments. void whitespace() { + // This overrides whitespace consumption so that it doesn't consume newlines + // or loud comments. while (!scanner.isDone) { var next = scanner.peekChar(); if (next != $tab && next != $space) break; @@ -186,6 +193,8 @@ class SassParser extends StylesheetParser { } } + /// As long as the scanner's position is indented beneath the starting line, + /// runs [body] to consume the next statement. void _whileIndentedLower(void body()) { var parentIndentation = currentIndentation; int childIndentation; @@ -203,6 +212,8 @@ class SassParser extends StylesheetParser { } } + /// Consumes indentation whitespace and returns the indentation level of the + /// next line. int _readIndentation() { if (_nextIndentation == null) _peekIndentation(); _currentIndentation = _nextIndentation; @@ -212,6 +223,7 @@ class SassParser extends StylesheetParser { return currentIndentation; } + /// Returns the indentation level of the next line. int _peekIndentation() { if (_nextIndentation != null) return _nextIndentation; @@ -262,6 +274,10 @@ class SassParser extends StylesheetParser { return _nextIndentation; } + /// Ensures that the document uses consistent characters for indentation. + /// + /// The [containsTab] and [containsSpace] parameters refer to a single line of + /// indentation that has just been parsed. void _checkIndentationConsistency(bool containsTab, bool containsSpace) { if (containsTab) { if (containsSpace) { diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index 8a8bc487..b6dda9a7 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -8,6 +8,7 @@ import '../ast/sass.dart'; import '../util/character.dart'; import 'stylesheet.dart'; +/// A parser for the CSS-compatible syntax. class ScssParser extends StylesheetParser { bool get indented => false; int get currentIndentation => null; @@ -109,6 +110,7 @@ class ScssParser extends StylesheetParser { return statements; } + /// Consumes a statement-level silent comment block. Comment _silentComment() { var start = scanner.state; scanner.expect("//"); @@ -124,6 +126,7 @@ class ScssParser extends StylesheetParser { silent: true); } + /// Consumes a statement-level loud comment block. Comment _loudComment() { var start = scanner.state; scanner.expect("/*"); diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 215ec427..b01f4f9b 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -9,13 +9,13 @@ import '../util/character.dart'; import '../utils.dart'; import 'parser.dart'; +/// Pseudo-class selectors that take unadorned selectors as arguments. final _selectorPseudoClasses = new Set.from( ["not", "matches", "current", "any", "has", "host", "host-context"]); -final _prefixedSelectorPseudoClasses = - new Set.from(["nth-child", "nth-last-child"]); - +/// A parser for selectors. class SelectorParser extends Parser { + /// Whether this parser allows the parent selector `&`. final bool _allowParent; SelectorParser(String contents, {url, bool allowParent: true}) @@ -46,6 +46,7 @@ class SelectorParser extends Parser { }); } + /// Consumes a selector list. SelectorList _selectorList() { var components = []; @@ -65,6 +66,10 @@ class SelectorParser extends Parser { return new SelectorList(components); } + /// Consumes a complex selector. + /// + /// If [lineBreak] is `true`, that indicates that there was a line break + /// before this selector. ComplexSelector _complexSelector({bool lineBreak: false}) { var components = []; @@ -113,6 +118,7 @@ class SelectorParser extends Parser { return new ComplexSelector(components, lineBreak: lineBreak); } + /// Consumes a compound selector. CompoundSelector _compoundSelector() { var components = [_simpleSelector()]; @@ -124,6 +130,10 @@ class SelectorParser extends Parser { return new CompoundSelector(components); } + /// Consumes a simple selector. + /// + /// If [allowParent] is passed, it controls whether the parent selector `&` is + /// allowed. Otherwise, it defaults to [_allowParent]. SimpleSelector _simpleSelector({bool allowParent}) { allowParent ??= _allowParent; switch (scanner.peekChar()) { @@ -146,6 +156,7 @@ class SelectorParser extends Parser { } } + /// Consumes an attribute selector. AttributeSelector _attributeSelector() { scanner.expectChar($lbracket); whitespace(); @@ -170,6 +181,7 @@ class SelectorParser extends Parser { return new AttributeSelector.withOperator(name, operator, value); } + /// Consumes a qualified name as part of an attribute selector. QualifiedName _attributeName() { if (scanner.scanChar($asterisk)) { scanner.expectChar($pipe); @@ -185,6 +197,7 @@ class SelectorParser extends Parser { return new QualifiedName(identifier(), namespace: nameOrNamespace); } + /// Consumes an attribute selector's operator. AttributeOperator _attributeOperator() { var start = scanner.state; switch (scanner.readChar()) { @@ -217,24 +230,28 @@ class SelectorParser extends Parser { } } + /// Consumes a class selector. ClassSelector _classSelector() { scanner.expectChar($dot); var name = identifier(); return new ClassSelector(name); } + /// Consumes an ID selector. IDSelector _idSelector() { scanner.expectChar($hash); var name = identifier(); return new IDSelector(name); } + /// Consumes a placeholder selector. PlaceholderSelector _placeholderSelector() { scanner.expectChar($percent); var name = identifier(); return new PlaceholderSelector(name); } + /// Consumes a parent selector. ParentSelector _parentSelector() { scanner.expectChar($ampersand); var next = scanner.peekChar(); @@ -244,6 +261,7 @@ class SelectorParser extends Parser { return new ParentSelector(suffix: suffix); } + /// Consumes a pseudo selector. PseudoSelector _pseudoSelector() { scanner.expectChar($colon); var element = scanner.scanChar($colon); @@ -261,7 +279,7 @@ class SelectorParser extends Parser { argument = declarationValue(); } else if (_selectorPseudoClasses.contains(unvendored)) { selector = _selectorList(); - } else if (_prefixedSelectorPseudoClasses.contains(unvendored)) { + } else if (unvendored == "nth-child" || unvendored == "nth-last-child") { argument = rawText(_aNPlusB); if (scanWhitespace()) { expectIdentifier("of", ignoreCase: true); @@ -279,6 +297,9 @@ class SelectorParser extends Parser { element: element, argument: argument, selector: selector); } + /// Consumes an [`An+B` production][An+B]. + /// + /// [An+B]: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax void _aNPlusB() { switch (scanner.peekChar()) { case $e: @@ -321,6 +342,9 @@ class SelectorParser extends Parser { } } + /// Consumes a type selector or a universal selector. + /// + /// These are combined because either one could start with `*`. SimpleSelector _typeOrUniversalSelector() { var first = scanner.peekChar(); if (first == $asterisk) { diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 3f17c8b0..a1fef5a4 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -29,20 +29,23 @@ import 'parser.dart'; /// private, except where they have to be public for subclasses to refer to /// them. abstract class StylesheetParser extends Parser { + /// Whether the parser is currently parsing the contents of a mixin + /// declaration. var _inMixin = false; - var _inContentBlock = false; - - var _inControlDirective = false; - + /// Whether the current mixin contains at least one `@content` rule. + /// + /// This is `null` unless [_inMixin] is `true`. bool _mixinHasContent; - StylesheetParser(String contents, {url}) : super(contents, url: url); + /// Whether the parser is currently parsing a content block passed to a mixin. + var _inContentBlock = false; - // Conventions: - // - // * All statement functions consume through following whitespace, including - // comments. No other functions do so unless explicitly specified. + /// Whether the parser is currently parsing a control directive such as `@if` + /// or `@each`. + var _inControlDirective = false; + + StylesheetParser(String contents, {url}) : super(contents, url: url); // ## Statements @@ -63,11 +66,13 @@ abstract class StylesheetParser extends Parser { }); } + /// Consumes a statement that's allowed at the top level of the stylesheet. Statement _topLevelStatement() { if (scanner.peekChar() == $at) return _atRule(_topLevelStatement); return _styleRule(); } + /// Consumes a variable declaration. VariableDeclaration variableDeclaration() { var start = scanner.state; var name = variableName(); @@ -98,6 +103,7 @@ abstract class StylesheetParser extends Parser { guarded: guarded, global: global); } + /// Consumes a style rule. StyleRule _styleRule() { var start = scanner.state; var selector = _almostAnyValue(); @@ -105,21 +111,13 @@ abstract class StylesheetParser extends Parser { return new StyleRule(selector, children, scanner.spanFrom(start)); } + /// Consumes a statement that's allowed within a style rule. Statement _ruleChild() { if (scanner.peekChar() == $at) return _atRule(_ruleChild); return _declarationOrStyleRule(); } - Expression _declarationExpression() { - if (lookingAtChildren()) { - return new StringExpression(new Interpolation([], scanner.emptySpan), - quotes: true); - } - - return _expression(); - } - - /// Parses a [Declaration] or a [StyleRule]. + /// Consumes a [Declaration] or a [StyleRule]. /// /// When parsing the contents of a style rule, it can be difficult to tell /// declarations apart from nested style rules. Since we don't thoroughly @@ -251,6 +249,11 @@ abstract class StylesheetParser extends Parser { children: lookingAtChildren() ? children(_declarationChild) : null); } + /// Consumes a property declaration. + /// + /// This is only used in contexts where declarations are allowed but style + /// rules are not, such as nested declarations. Otherwise, + /// [_declarationOrStyleRule] is used instead. Declaration _declaration() { var start = scanner.state; var name = _interpolatedIdentifier(); @@ -269,6 +272,20 @@ abstract class StylesheetParser extends Parser { children: lookingAtChildren() ? children(_declarationChild) : null); } + /// Consumes an expression after a property declaration. + /// + /// This parses an empty identifier expression if the declaration has no value + /// but has children. + Expression _declarationExpression() { + if (lookingAtChildren()) { + return new StringExpression(new Interpolation([], scanner.emptySpan), + quotes: true); + } + + return _expression(); + } + + /// Consumes a statement that's allowed within a declaration. Statement _declarationChild() { if (scanner.peekChar() == $at) return _declarationAtRule(); return _declaration(); @@ -276,6 +293,11 @@ abstract class StylesheetParser extends Parser { // ## At Rules + /// Consumes an at-rule. + /// + /// This consumes at-rules that are allowed at all levels of the document; the + /// [child] parameter is called to consume any at-rules that are specifically + /// allowed in the caller's context. Statement _atRule(Statement child()) { var start = scanner.state; var name = _atRuleName(); @@ -322,6 +344,7 @@ abstract class StylesheetParser extends Parser { } } + /// Consumes an at-rule allowed within a property declaration. Statement _declarationAtRule() { var start = scanner.state; var name = _atRuleName(); @@ -352,6 +375,7 @@ abstract class StylesheetParser extends Parser { } } + /// Consumes an at-rule allowed within a function. Statement _functionAtRule() { var start = scanner.state; switch (_atRuleName()) { @@ -378,6 +402,7 @@ abstract class StylesheetParser extends Parser { } } + /// Consumes an at-rule's name. String _atRuleName() { scanner.expectChar($at); var name = identifier(); @@ -385,6 +410,9 @@ abstract class StylesheetParser extends Parser { return name; } + /// Consumes an `@at-root` rule. + /// + /// [start] should point before the `@`. AtRootRule _atRootRule(LineScannerState start) { var next = scanner.peekChar(); var query = next == $hash || next == $lparen ? _queryExpression() : null; @@ -393,6 +421,9 @@ abstract class StylesheetParser extends Parser { query: query); } + /// Consumes a `@content` rule. + /// + /// [start] should point before the `@`. ContentRule _contentRule(LineScannerState start) { if (_inMixin) { _mixinHasContent = true; @@ -404,9 +435,16 @@ abstract class StylesheetParser extends Parser { return null; } + /// Consumes a `@debug` rule. + /// + /// [start] should point before the `@`. DebugRule _debugRule(LineScannerState start) => new DebugRule(_expression(), scanner.spanFrom(start)); + /// Consumes an `@each` rule. + /// + /// [start] should point before the `@`. [child] is called to consume any + /// children that are specifically allowed in the caller's context. EachRule _eachRule(LineScannerState start, Statement child()) { var wasInControlDirective = _inControlDirective; _inControlDirective = true; @@ -429,9 +467,15 @@ abstract class StylesheetParser extends Parser { return new EachRule(variables, list, children, scanner.spanFrom(start)); } + /// Consumes an `@error` rule. + /// + /// [start] should point before the `@`. ErrorRule _errorRule(LineScannerState start) => new ErrorRule(_expression(), scanner.spanFrom(start)); + /// Consumes an `@extend` rule. + /// + /// [start] should point before the `@`. ExtendRule _extendRule(LineScannerState start) { var value = _almostAnyValue(); var optional = scanner.scanChar($exclamation); @@ -439,6 +483,9 @@ abstract class StylesheetParser extends Parser { return new ExtendRule(value, scanner.spanFrom(start), optional: optional); } + /// Consumes a function declaration. + /// + /// [start] should point before the `@`. FunctionRule _functionRule(LineScannerState start) { var name = identifier(); whitespace(); @@ -457,6 +504,10 @@ abstract class StylesheetParser extends Parser { return new FunctionRule(name, arguments, children, scanner.spanFrom(start)); } + /// Consumes a `@for` rule. + /// + /// [start] should point before the `@`. [child] is called to consume any + /// children that are specifically allowed in the caller's context. ForRule _forRule(LineScannerState start, Statement child()) { var wasInControlDirective = _inControlDirective; _inControlDirective = true; @@ -491,6 +542,10 @@ abstract class StylesheetParser extends Parser { exclusive: exclusive); } + /// Consumes an `@if` rule. + /// + /// [start] should point before the `@`. [child] is called to consume any + /// children that are specifically allowed in the caller's context. IfRule _ifRule(LineScannerState start, Statement child()) { var ifIndentation = currentIndentation; var wasInControlDirective = _inControlDirective; @@ -516,6 +571,9 @@ abstract class StylesheetParser extends Parser { return new IfRule(clauses, scanner.spanFrom(start), lastClause: lastClause); } + /// Consumes an `@import` rule. + /// + /// [start] should point before the `@`. Statement _importRule(LineScannerState start) { if (_inControlDirective) { _disallowedAtRule(start); @@ -547,6 +605,7 @@ abstract class StylesheetParser extends Parser { } } + /// Returns whether [url] indicates that an `@import` is a plain CSS import. bool _isPlainImportUrl(String url) { if (url.length < "//".length) return false; @@ -556,6 +615,9 @@ abstract class StylesheetParser extends Parser { return url.startsWith("http://") || url.startsWith("https://"); } + /// Consumes an `@include` rule. + /// + /// [start] should point before the `@`. IncludeRule _includeRule(LineScannerState start) { var name = identifier(); whitespace(); @@ -575,9 +637,15 @@ abstract class StylesheetParser extends Parser { children: children); } + /// Consumes a `@media` rule. + /// + /// [start] should point before the `@`. MediaRule _mediaRule(LineScannerState start) => new MediaRule( _mediaQueryList(), children(_ruleChild), scanner.spanFrom(start)); + /// Consumes a mixin declaration. + /// + /// [start] should point before the `@`. MixinRule _mixinRule(LineScannerState start) { var name = identifier(); whitespace(); @@ -597,14 +665,21 @@ abstract class StylesheetParser extends Parser { _mixinHasContent = false; var children = this.children(_ruleChild); _inMixin = false; + _mixinHasContent = null; return new MixinRule(name, arguments, children, scanner.spanFrom(start), hasContent: _mixinHasContent); } + /// Consumes a `@return` rule. + /// + /// [start] should point before the `@`. ReturnRule _returnRule(LineScannerState start) => new ReturnRule(_expression(), scanner.spanFrom(start)); + /// Consumes a `@supports` rule. + /// + /// [start] should point before the `@`. SupportsRule _supportsRule(LineScannerState start) { var condition = _supportsCondition(); whitespace(); @@ -612,9 +687,16 @@ abstract class StylesheetParser extends Parser { condition, children(_ruleChild), scanner.spanFrom(start)); } + /// Consumes a `@warn` rule. + /// + /// [start] should point before the `@`. WarnRule _warnRule(LineScannerState start) => new WarnRule(_expression(), scanner.spanFrom(start)); + /// Consumes a `@while` rule. + /// + /// [start] should point before the `@`. [child] is called to consume any + /// children that are specifically allowed in the caller's context. WhileRule _whileRule(LineScannerState start, Statement child()) { var wasInControlDirective = _inControlDirective; _inControlDirective = true; @@ -624,6 +706,9 @@ abstract class StylesheetParser extends Parser { return new WhileRule(expression, children, scanner.spanFrom(start)); } + /// Consumes an at-rule that's not explicitly supported by Sass. + /// + /// [start] should point before the `@`. [name] is the name of the at-rule. AtRule _unknownAtRule(LineScannerState start, String name) { Interpolation value; var next = scanner.peekChar(); @@ -634,7 +719,11 @@ abstract class StylesheetParser extends Parser { children: lookingAtChildren() ? children(_ruleChild) : null); } - // This returns [Statement] so that it can be returned within case statements. + /// Throws a [StringScannerException] indicating that the at-rule starting at + /// [start] is not allowed in the current context. + /// + /// This declares a return type of [Statement] so that it can be returned + /// within case statements. Statement _disallowedAtRule(LineScannerState start) { _almostAnyValue(); scanner.error("This at-rule is not allowed here.", @@ -643,6 +732,7 @@ abstract class StylesheetParser extends Parser { return null; } + /// Consumes an argument declaration. ArgumentDeclaration _argumentDeclaration() { var start = scanner.state; scanner.expectChar($lparen); @@ -684,6 +774,7 @@ abstract class StylesheetParser extends Parser { // ## Expressions + /// Consumes an argument invocation. ArgumentInvocation _argumentInvocation() { var start = scanner.state; scanner.expectChar($lparen); @@ -731,6 +822,11 @@ abstract class StylesheetParser extends Parser { rest: rest, keywordRest: keywordRest); } + /// Consumes an expression. + /// + /// If [until] is passed, it's called each time the expression could end and + /// still be a valid expression. When it returns `true`, this returns the + /// expression. Expression _expression({bool until()}) { if (until != null && until()) scanner.error("Expected expression."); @@ -1001,10 +1097,11 @@ abstract class StylesheetParser extends Parser { } } + /// Consumes an expression until it reaches a top-level comma. Expression _expressionUntilComma() => _expression(until: () => scanner.peekChar() == $comma); - // non-list expression + /// Consumes an expression that doesn't contain any top-level whitespace. Expression _singleExpression() { var first = scanner.peekChar(); switch (first) { @@ -1113,6 +1210,7 @@ abstract class StylesheetParser extends Parser { } } + /// Consumes a bracketed list. ListExpression _bracketedList() { var start = scanner.state; scanner.expectChar($lbracket); @@ -1129,6 +1227,7 @@ abstract class StylesheetParser extends Parser { brackets: true, span: scanner.spanFrom(start)); } + /// Parse a parenthesized expression. Expression _parentheses() { var start = scanner.state; scanner.expectChar($lparen); @@ -1991,6 +2090,10 @@ abstract class StylesheetParser extends Parser { 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]. int get currentIndentation; bool atEndOfStatement();