diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf0228ac..3d430a121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -## 1.72.1 +## 1.73.0 + +* Add support for nesting in plain CSS files. This is not processed by Sass at + all; it's emitted exactly as-is in the CSS. * Add linux-riscv64 and windows-arm64 releases. diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart index a5d2b1f0c..6e242d36c 100644 --- a/lib/src/ast/css/modifiable/style_rule.dart +++ b/lib/src/ast/css/modifiable/style_rule.dart @@ -21,12 +21,13 @@ final class ModifiableCssStyleRule extends ModifiableCssParentNode final SelectorList originalSelector; final FileSpan span; + final bool fromPlainCss; /// Creates a new [ModifiableCssStyleRule]. /// /// If [originalSelector] isn't passed, it defaults to [_selector.value]. ModifiableCssStyleRule(this._selector, this.span, - {SelectorList? originalSelector}) + {SelectorList? originalSelector, this.fromPlainCss = false}) : originalSelector = originalSelector ?? _selector.value; T accept(ModifiableCssVisitor visitor) => diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart index ccce74fdb..8b9da6663 100644 --- a/lib/src/ast/css/style_rule.dart +++ b/lib/src/ast/css/style_rule.dart @@ -2,6 +2,8 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../selector.dart'; import 'node.dart'; @@ -16,4 +18,10 @@ abstract interface class CssStyleRule implements CssParentNode { /// The selector for this rule, before any extensions were applied. SelectorList get originalSelector; + + /// Whether this style rule was originally defined in a plain CSS stylesheet. + /// + /// :nodoc: + @internal + bool get fromPlainCss; } diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index 4fc24f7f6..8da4598f6 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -57,9 +57,10 @@ final class SelectorList extends Selector { /// Parses a selector list from [contents]. /// - /// If passed, [url] is the name of the file from which [contents] comes. - /// [allowParent] and [allowPlaceholder] control whether [ParentSelector]s or - /// [PlaceholderSelector]s are allowed in this selector, respectively. + /// If passed, [url] is the name of the file from which [contents] comes. If + /// [allowParent] is false, this doesn't allow [ParentSelector]s. If + /// [plainCss] is true, this parses the selector as plain CSS rather than + /// unresolved Sass. /// /// If passed, [interpolationMap] maps the text of [contents] back to the /// original location of the selector in the source file. @@ -70,13 +71,13 @@ final class SelectorList extends Selector { Logger? logger, InterpolationMap? interpolationMap, bool allowParent = true, - bool allowPlaceholder = true}) => + bool plainCss = false}) => SelectorParser(contents, url: url, logger: logger, interpolationMap: interpolationMap, allowParent: allowParent, - allowPlaceholder: allowPlaceholder) + plainCss: plainCss) .parse(); T accept(SelectorVisitor visitor) => visitor.visitSelectorList(this); @@ -95,17 +96,24 @@ final class SelectorList extends Selector { return contents.isEmpty ? null : SelectorList(contents, span); } - /// Returns a new list with all [ParentSelector]s replaced with [parent]. + /// Returns a new selector list that represents [this] nested within [parent]. /// - /// If [implicitParent] is true, this treats [ComplexSelector]s that don't - /// contain an explicit [ParentSelector] as though they began with one. + /// By default, this replaces [ParentSelector]s in [this] with [parent]. If + /// [preserveParentSelectors] is true, this instead preserves those selectors + /// as parent selectors. + /// + /// If [implicitParent] is true, this prepends [parent] to any + /// [ComplexSelector]s in this that don't contain explicit [ParentSelector]s, + /// or to _all_ [ComplexSelector]s if [preserveParentSelectors] is true. /// /// The given [parent] may be `null`, indicating that this has no parents. If /// so, this list is returned as-is if it doesn't contain any explicit - /// [ParentSelector]s. If it does, this throws a [SassScriptException]. - SelectorList resolveParentSelectors(SelectorList? parent, - {bool implicitParent = true}) { + /// [ParentSelector]s or if [preserveParentSelectors] is true. Otherwise, this + /// throws a [SassScriptException]. + SelectorList nestWithin(SelectorList? parent, + {bool implicitParent = true, bool preserveParentSelectors = false}) { if (parent == null) { + if (preserveParentSelectors) return this; var parentSelector = accept(const _ParentSelectorVisitor()); if (parentSelector == null) return this; throw SassException( @@ -114,7 +122,7 @@ final class SelectorList extends Selector { } return SelectorList(flattenVertically(components.map((complex) { - if (!_containsParentSelector(complex)) { + if (preserveParentSelectors || !_containsParentSelector(complex)) { if (!implicitParent) return [complex]; return parent.components.map((parentComplex) => parentComplex.concatenate(complex, complex.span)); @@ -122,7 +130,7 @@ final class SelectorList extends Selector { var newComplexes = []; for (var component in complex.components) { - var resolved = _resolveParentSelectorsCompound(component, parent); + var resolved = _nestWithinCompound(component, parent); if (resolved == null) { if (newComplexes.isEmpty) { newComplexes.add(ComplexSelector( @@ -165,7 +173,7 @@ final class SelectorList extends Selector { /// [ParentSelector]s replaced with [parent]. /// /// Returns `null` if [component] doesn't contain any [ParentSelector]s. - Iterable? _resolveParentSelectorsCompound( + Iterable? _nestWithinCompound( ComplexSelectorComponent component, SelectorList parent) { var simples = component.selector.components; var containsSelectorPseudo = simples.any((simple) { @@ -181,8 +189,8 @@ final class SelectorList extends Selector { ? simples.map((simple) => switch (simple) { PseudoSelector(:var selector?) when _containsParentSelector(selector) => - simple.withSelector(selector.resolveParentSelectors(parent, - implicitParent: false)), + simple.withSelector( + selector.nestWithin(parent, implicitParent: false)), _ => simple }) : simples; @@ -261,6 +269,8 @@ final class SelectorList extends Selector { /// Returns a copy of `this` with [combinators] added to the end of each /// complex selector in [components]. + /// + /// @nodoc @internal SelectorList withAdditionalCombinators( List> combinators) => diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart index d6fddf9de..08e133dbf 100644 --- a/lib/src/functions/selector.dart +++ b/lib/src/functions/selector.dart @@ -52,7 +52,7 @@ final _nest = _function("nest", r"$selectors...", (arguments) { first = false; return result; }) - .reduce((parent, child) => child.resolveParentSelectors(parent)) + .reduce((parent, child) => child.nestWithin(parent)) .asSassList; }); @@ -83,7 +83,7 @@ final _append = _function("append", r"$selectors...", (arguments) { ...rest ], span); }), span) - .resolveParentSelectors(parent); + .nestWithin(parent); }).asSassList; }); diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 0e5b7a04f..97a334166 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -31,17 +31,24 @@ class SelectorParser extends Parser { /// Whether this parser allows the parent selector `&`. final bool _allowParent; - /// Whether this parser allows placeholder selectors beginning with `%`. - final bool _allowPlaceholder; + /// Whether to parse the selector as plain CSS. + final bool _plainCss; + /// Creates a parser that parses CSS selectors. + /// + /// If [allowParent] is `false`, this will throw a [SassFormatException] if + /// the selector includes the parent selector `&`. + /// + /// If [plainCss] is `true`, this will parse the selector as a plain CSS + /// selector rather than a Sass selector. SelectorParser(super.contents, {super.url, super.logger, super.interpolationMap, bool allowParent = true, - bool allowPlaceholder = true}) + bool plainCss = false}) : _allowParent = allowParent, - _allowPlaceholder = allowPlaceholder; + _plainCss = plainCss; SelectorList parse() { return wrapSpanFormatException(() { @@ -165,7 +172,9 @@ class SelectorParser extends Parser { } } - if (lastCompound != null) { + if (combinators.isNotEmpty && _plainCss) { + scanner.error("expected selector."); + } else if (lastCompound != null) { components.add(ComplexSelectorComponent( lastCompound, combinators, spanFrom(componentStart))); } else if (combinators.isNotEmpty) { @@ -184,8 +193,8 @@ class SelectorParser extends Parser { var start = scanner.state; var components = [_simpleSelector()]; - while (isSimpleSelectorStart(scanner.peekChar())) { - components.add(_simpleSelector(allowParent: false)); + while (_isSimpleSelectorStart(scanner.peekChar())) { + components.add(_simpleSelector(allowParent: _plainCss)); } return CompoundSelector(components, spanFrom(start)); @@ -207,8 +216,8 @@ class SelectorParser extends Parser { return _idSelector(); case $percent: var selector = _placeholderSelector(); - if (!_allowPlaceholder) { - error("Placeholder selectors aren't allowed here.", + if (_plainCss) { + error("Placeholder selectors aren't allowed in plain CSS.", scanner.spanFrom(start)); } return selector; @@ -340,6 +349,11 @@ class SelectorParser extends Parser { var start = scanner.state; scanner.expectChar($ampersand); var suffix = lookingAtIdentifierBody() ? identifierBody() : null; + if (_plainCss && suffix != null) { + scanner.error("Parent selectors can't have suffixes in plain CSS.", + position: start.position, length: scanner.position - start.position); + } + return ParentSelector(spanFrom(start), suffix: suffix); } @@ -457,4 +471,12 @@ class SelectorParser extends Parser { spanFrom(start)); } } + + // Returns whether [character] can start a simple selector in the middle of a + // compound selector. + bool _isSimpleSelectorStart(int? character) => switch (character) { + $asterisk || $lbracket || $dot || $hash || $percent || $colon => true, + $ampersand => _plainCss, + _ => false + }; } diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index d0c620f24..82f3f4565 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -324,10 +324,6 @@ abstract class StylesheetParser extends Parser { /// parsed as a selector and never as a property with nested properties /// beneath it. Statement _declarationOrStyleRule() { - if (plainCss && _inStyleRule && !_inUnknownAtRule) { - return _propertyOrVariableDeclaration(); - } - // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. @@ -400,10 +396,7 @@ abstract class StylesheetParser extends Parser { } var postColonWhitespace = rawText(whitespace); - if (lookingAtChildren()) { - return _withChildren(_declarationChild, start, - (children, span) => Declaration.nested(name, children, span)); - } + if (_tryDeclarationChildren(name, start) case var nested?) return nested; midBuffer.write(postColonWhitespace); var couldBeSelector = @@ -439,12 +432,8 @@ abstract class StylesheetParser extends Parser { return nameBuffer; } - if (lookingAtChildren()) { - return _withChildren( - _declarationChild, - start, - (children, span) => - Declaration.nested(name, children, span, value: value)); + if (_tryDeclarationChildren(name, start, value: value) case var nested?) { + return nested; } else { expectStatementSeparator(); return Declaration(name, value, scanner.spanFrom(start)); @@ -549,31 +538,36 @@ abstract class StylesheetParser extends Parser { } whitespace(); - - if (lookingAtChildren()) { - if (plainCss) { - scanner.error("Nested declarations aren't allowed in plain CSS."); - } - return _withChildren(_declarationChild, start, - (children, span) => Declaration.nested(name, children, span)); - } + if (_tryDeclarationChildren(name, start) case var nested?) return nested; var value = _expression(); - if (lookingAtChildren()) { - if (plainCss) { - scanner.error("Nested declarations aren't allowed in plain CSS."); - } - return _withChildren( - _declarationChild, - start, - (children, span) => - Declaration.nested(name, children, span, value: value)); + if (_tryDeclarationChildren(name, start, value: value) case var nested?) { + return nested; } else { expectStatementSeparator(); return Declaration(name, value, scanner.spanFrom(start)); } } + /// Tries parsing nested children of a declaration whose [name] has already + /// been parsed, and returns `null` if it doesn't have any. + /// + /// If [value] is passed, it's used as the value of the peroperty without + /// nesting. + Declaration? _tryDeclarationChildren( + Interpolation name, LineScannerState start, + {Expression? value}) { + if (!lookingAtChildren()) return null; + if (plainCss) { + scanner.error("Nested declarations aren't allowed in plain CSS."); + } + return _withChildren( + _declarationChild, + start, + (children, span) => + Declaration.nested(name, children, span, value: value)); + } + /// Consumes a statement that's allowed within a declaration. Statement _declarationChild() => scanner.peekChar() == $at ? _declarationAtRule() diff --git a/lib/src/util/character.dart b/lib/src/util/character.dart index ea4085d29..7141be67a 100644 --- a/lib/src/util/character.dart +++ b/lib/src/util/character.dart @@ -92,16 +92,6 @@ int combineSurrogates(int highSurrogate, int lowSurrogate) => // high/low surrogates. 0x10000 + ((highSurrogate & 0x3FF) << 10) + (lowSurrogate & 0x3FF); -// Returns whether [character] can start a simple selector other than a type -// selector. -bool isSimpleSelectorStart(int? character) => - character == $asterisk || - character == $lbracket || - character == $dot || - character == $hash || - character == $percent || - character == $colon; - /// Returns whether [identifier] is module-private. /// /// Assumes [identifier] is a valid Sass identifier. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 26679cd8e..ce4105bda 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2011,16 +2011,32 @@ final class _EvaluateVisitor } var parsedSelector = SelectorList.parse(selectorText, - interpolationMap: selectorMap, - allowParent: !_stylesheet.plainCss, - allowPlaceholder: !_stylesheet.plainCss, - logger: _logger) - .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + interpolationMap: selectorMap, + plainCss: _stylesheet.plainCss, + logger: _logger); + + var nest = !(_styleRule?.fromPlainCss ?? false); + if (nest) { + if (_stylesheet.plainCss) { + for (var complex in parsedSelector.components) { + if (complex.leadingCombinators case [var first, ...] + when _stylesheet.plainCss) { + throw _exception( + "Top-level leading combinators aren't allowed in plain CSS.", + first.span); + } + } + } + + parsedSelector = parsedSelector.nestWithin( + _styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: _stylesheet.plainCss); + } var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: parsedSelector); + originalSelector: parsedSelector, fromPlainCss: _stylesheet.plainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; await _withParent(rule, () async { @@ -2030,7 +2046,7 @@ final class _EvaluateVisitor } }); }, - through: (node) => node is CssStyleRule, + through: nest ? (node) => node is CssStyleRule : null, scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; @@ -2048,13 +2064,15 @@ final class _EvaluateVisitor complex.span.trimRight(), Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { - _warn( - 'The selector "${complex.toString().trim()}" is invalid CSS.\n' - 'This will be an error in Dart Sass 2.0.0.\n' - '\n' - 'More info: https://sass-lang.com/d/bogus-combinators', - complex.span.trimRight(), - Deprecation.bogusCombinators); + if (!_stylesheet.plainCss) { + _warn( + 'The selector "${complex.toString().trim()}" is invalid CSS.\n' + 'This will be an error in Dart Sass 2.0.0.\n' + '\n' + 'More info: https://sass-lang.com/d/bogus-combinators', + complex.span.trimRight(), + Deprecation.bogusCombinators); + } } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -3386,12 +3404,15 @@ final class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.resolveParentSelectors( - styleRule?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + var nest = !(_styleRule?.fromPlainCss ?? false); + var originalSelector = nest + ? node.selector.nestWithin(styleRule?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: node.fromPlainCss) + : node.selector; var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: originalSelector); + originalSelector: originalSelector, fromPlainCss: node.fromPlainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; await _withParent(rule, () async { @@ -3400,7 +3421,7 @@ final class _EvaluateVisitor await child.accept(this); } }); - }, through: (node) => node is CssStyleRule, scopeWhen: false); + }, through: nest ? (node) => node is CssStyleRule : null, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; if (_parent.children case [..., var lastChild] when styleRule == null) { diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 096ae21fe..32c4e2764 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 7351193aa9229e1434c09a2cbc9fa596cd924901 +// Checksum: 05cb957cd0c7698d8ad648f31d862dc91f0daa7b // // ignore_for_file: unused_import @@ -2001,16 +2001,32 @@ final class _EvaluateVisitor } var parsedSelector = SelectorList.parse(selectorText, - interpolationMap: selectorMap, - allowParent: !_stylesheet.plainCss, - allowPlaceholder: !_stylesheet.plainCss, - logger: _logger) - .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + interpolationMap: selectorMap, + plainCss: _stylesheet.plainCss, + logger: _logger); + + var nest = !(_styleRule?.fromPlainCss ?? false); + if (nest) { + if (_stylesheet.plainCss) { + for (var complex in parsedSelector.components) { + if (complex.leadingCombinators case [var first, ...] + when _stylesheet.plainCss) { + throw _exception( + "Top-level leading combinators aren't allowed in plain CSS.", + first.span); + } + } + } + + parsedSelector = parsedSelector.nestWithin( + _styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: _stylesheet.plainCss); + } var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: parsedSelector); + originalSelector: parsedSelector, fromPlainCss: _stylesheet.plainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; _withParent(rule, () { @@ -2020,7 +2036,7 @@ final class _EvaluateVisitor } }); }, - through: (node) => node is CssStyleRule, + through: nest ? (node) => node is CssStyleRule : null, scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; @@ -2038,13 +2054,15 @@ final class _EvaluateVisitor complex.span.trimRight(), Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { - _warn( - 'The selector "${complex.toString().trim()}" is invalid CSS.\n' - 'This will be an error in Dart Sass 2.0.0.\n' - '\n' - 'More info: https://sass-lang.com/d/bogus-combinators', - complex.span.trimRight(), - Deprecation.bogusCombinators); + if (!_stylesheet.plainCss) { + _warn( + 'The selector "${complex.toString().trim()}" is invalid CSS.\n' + 'This will be an error in Dart Sass 2.0.0.\n' + '\n' + 'More info: https://sass-lang.com/d/bogus-combinators', + complex.span.trimRight(), + Deprecation.bogusCombinators); + } } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -3354,12 +3372,15 @@ final class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.resolveParentSelectors( - styleRule?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + var nest = !(_styleRule?.fromPlainCss ?? false); + var originalSelector = nest + ? node.selector.nestWithin(styleRule?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: node.fromPlainCss) + : node.selector; var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: originalSelector); + originalSelector: originalSelector, fromPlainCss: node.fromPlainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; _withParent(rule, () { @@ -3368,7 +3389,7 @@ final class _EvaluateVisitor child.accept(this); } }); - }, through: (node) => node is CssStyleRule, scopeWhen: false); + }, through: nest ? (node) => node is CssStyleRule : null, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; if (_parent.children case [..., var lastChild] when styleRule == null) { diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index bd7e9e54f..6e8d05390 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,11 @@ +## 10.0.0 + +* Remove the `allowPlaceholders` argument from `SelectorList.parse()`. Instead, + it now has a more generic `plainCss` argument which tells it to parse the + selector in plain CSS mode. + +* Rename `SelectorList.resolveParentSelectors` to `SelectorList.nestWithin`. + ## 9.5.0 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index aa9b8eaae..4780522de 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -2,7 +2,7 @@ name: sass_api # Note: Every time we add a new Sass AST node, we need to bump the *major* # version because it's a breaking change for anyone who's implementing the # visitor interface(s). -version: 9.5.0 +version: 10.0.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.72.0 + sass: 1.73.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 89a24f5cd..52c8c58df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.72.1-dev +version: 1.73.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass