Skip to content

Commit

Permalink
Add support for nesting in plain CSS (#2198)
Browse files Browse the repository at this point in the history
  • Loading branch information
nex3 committed Mar 22, 2024
1 parent 772280a commit 9302b35
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 114 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
3 changes: 2 additions & 1 deletion lib/src/ast/css/modifiable/style_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(ModifiableCssVisitor<T> visitor) =>
Expand Down
8 changes: 8 additions & 0 deletions lib/src/ast/css/style_rule.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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;
}
42 changes: 26 additions & 16 deletions lib/src/ast/selector/list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<T>(SelectorVisitor<T> visitor) => visitor.visitSelectorList(this);
Expand All @@ -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(
Expand All @@ -114,15 +122,15 @@ 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));
}

var newComplexes = <ComplexSelector>[];
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(
Expand Down Expand Up @@ -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<ComplexSelector>? _resolveParentSelectorsCompound(
Iterable<ComplexSelector>? _nestWithinCompound(
ComplexSelectorComponent component, SelectorList parent) {
var simples = component.selector.components;
var containsSelectorPseudo = simples.any((simple) {
Expand All @@ -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;
Expand Down Expand Up @@ -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<CssValue<Combinator>> combinators) =>
Expand Down
4 changes: 2 additions & 2 deletions lib/src/functions/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});

Expand Down Expand Up @@ -83,7 +83,7 @@ final _append = _function("append", r"$selectors...", (arguments) {
...rest
], span);
}), span)
.resolveParentSelectors(parent);
.nestWithin(parent);
}).asSassList;
});

Expand Down
40 changes: 31 additions & 9 deletions lib/src/parse/selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(() {
Expand Down Expand Up @@ -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) {
Expand All @@ -184,8 +193,8 @@ class SelectorParser extends Parser {
var start = scanner.state;
var components = <SimpleSelector>[_simpleSelector()];

while (isSimpleSelectorStart(scanner.peekChar())) {
components.add(_simpleSelector(allowParent: false));
while (_isSimpleSelectorStart(scanner.peekChar())) {
components.add(_simpleSelector(allowParent: _plainCss));
}

return CompoundSelector(components, spanFrom(start));
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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
};
}
56 changes: 25 additions & 31 deletions lib/src/parse/stylesheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 0 additions & 10 deletions lib/src/util/character.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading

0 comments on commit 9302b35

Please sign in to comment.