Skip to content

Commit 9302b35

Browse files
authored
Add support for nesting in plain CSS (#2198)
See sass/sass#3524 Closes #1927
1 parent 772280a commit 9302b35

File tree

13 files changed

+192
-114
lines changed

13 files changed

+192
-114
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
## 1.72.1
1+
## 1.73.0
2+
3+
* Add support for nesting in plain CSS files. This is not processed by Sass at
4+
all; it's emitted exactly as-is in the CSS.
25

36
* Add linux-riscv64 and windows-arm64 releases.
47

lib/src/ast/css/modifiable/style_rule.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ final class ModifiableCssStyleRule extends ModifiableCssParentNode
2121

2222
final SelectorList originalSelector;
2323
final FileSpan span;
24+
final bool fromPlainCss;
2425

2526
/// Creates a new [ModifiableCssStyleRule].
2627
///
2728
/// If [originalSelector] isn't passed, it defaults to [_selector.value].
2829
ModifiableCssStyleRule(this._selector, this.span,
29-
{SelectorList? originalSelector})
30+
{SelectorList? originalSelector, this.fromPlainCss = false})
3031
: originalSelector = originalSelector ?? _selector.value;
3132

3233
T accept<T>(ModifiableCssVisitor<T> visitor) =>

lib/src/ast/css/style_rule.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// MIT-style license that can be found in the LICENSE file or at
33
// https://opensource.org/licenses/MIT.
44

5+
import 'package:meta/meta.dart';
6+
57
import '../selector.dart';
68
import 'node.dart';
79

@@ -16,4 +18,10 @@ abstract interface class CssStyleRule implements CssParentNode {
1618

1719
/// The selector for this rule, before any extensions were applied.
1820
SelectorList get originalSelector;
21+
22+
/// Whether this style rule was originally defined in a plain CSS stylesheet.
23+
///
24+
/// :nodoc:
25+
@internal
26+
bool get fromPlainCss;
1927
}

lib/src/ast/selector/list.dart

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ final class SelectorList extends Selector {
5757

5858
/// Parses a selector list from [contents].
5959
///
60-
/// If passed, [url] is the name of the file from which [contents] comes.
61-
/// [allowParent] and [allowPlaceholder] control whether [ParentSelector]s or
62-
/// [PlaceholderSelector]s are allowed in this selector, respectively.
60+
/// If passed, [url] is the name of the file from which [contents] comes. If
61+
/// [allowParent] is false, this doesn't allow [ParentSelector]s. If
62+
/// [plainCss] is true, this parses the selector as plain CSS rather than
63+
/// unresolved Sass.
6364
///
6465
/// If passed, [interpolationMap] maps the text of [contents] back to the
6566
/// original location of the selector in the source file.
@@ -70,13 +71,13 @@ final class SelectorList extends Selector {
7071
Logger? logger,
7172
InterpolationMap? interpolationMap,
7273
bool allowParent = true,
73-
bool allowPlaceholder = true}) =>
74+
bool plainCss = false}) =>
7475
SelectorParser(contents,
7576
url: url,
7677
logger: logger,
7778
interpolationMap: interpolationMap,
7879
allowParent: allowParent,
79-
allowPlaceholder: allowPlaceholder)
80+
plainCss: plainCss)
8081
.parse();
8182

8283
T accept<T>(SelectorVisitor<T> visitor) => visitor.visitSelectorList(this);
@@ -95,17 +96,24 @@ final class SelectorList extends Selector {
9596
return contents.isEmpty ? null : SelectorList(contents, span);
9697
}
9798

98-
/// Returns a new list with all [ParentSelector]s replaced with [parent].
99+
/// Returns a new selector list that represents [this] nested within [parent].
99100
///
100-
/// If [implicitParent] is true, this treats [ComplexSelector]s that don't
101-
/// contain an explicit [ParentSelector] as though they began with one.
101+
/// By default, this replaces [ParentSelector]s in [this] with [parent]. If
102+
/// [preserveParentSelectors] is true, this instead preserves those selectors
103+
/// as parent selectors.
104+
///
105+
/// If [implicitParent] is true, this prepends [parent] to any
106+
/// [ComplexSelector]s in this that don't contain explicit [ParentSelector]s,
107+
/// or to _all_ [ComplexSelector]s if [preserveParentSelectors] is true.
102108
///
103109
/// The given [parent] may be `null`, indicating that this has no parents. If
104110
/// so, this list is returned as-is if it doesn't contain any explicit
105-
/// [ParentSelector]s. If it does, this throws a [SassScriptException].
106-
SelectorList resolveParentSelectors(SelectorList? parent,
107-
{bool implicitParent = true}) {
111+
/// [ParentSelector]s or if [preserveParentSelectors] is true. Otherwise, this
112+
/// throws a [SassScriptException].
113+
SelectorList nestWithin(SelectorList? parent,
114+
{bool implicitParent = true, bool preserveParentSelectors = false}) {
108115
if (parent == null) {
116+
if (preserveParentSelectors) return this;
109117
var parentSelector = accept(const _ParentSelectorVisitor());
110118
if (parentSelector == null) return this;
111119
throw SassException(
@@ -114,15 +122,15 @@ final class SelectorList extends Selector {
114122
}
115123

116124
return SelectorList(flattenVertically(components.map((complex) {
117-
if (!_containsParentSelector(complex)) {
125+
if (preserveParentSelectors || !_containsParentSelector(complex)) {
118126
if (!implicitParent) return [complex];
119127
return parent.components.map((parentComplex) =>
120128
parentComplex.concatenate(complex, complex.span));
121129
}
122130

123131
var newComplexes = <ComplexSelector>[];
124132
for (var component in complex.components) {
125-
var resolved = _resolveParentSelectorsCompound(component, parent);
133+
var resolved = _nestWithinCompound(component, parent);
126134
if (resolved == null) {
127135
if (newComplexes.isEmpty) {
128136
newComplexes.add(ComplexSelector(
@@ -165,7 +173,7 @@ final class SelectorList extends Selector {
165173
/// [ParentSelector]s replaced with [parent].
166174
///
167175
/// Returns `null` if [component] doesn't contain any [ParentSelector]s.
168-
Iterable<ComplexSelector>? _resolveParentSelectorsCompound(
176+
Iterable<ComplexSelector>? _nestWithinCompound(
169177
ComplexSelectorComponent component, SelectorList parent) {
170178
var simples = component.selector.components;
171179
var containsSelectorPseudo = simples.any((simple) {
@@ -181,8 +189,8 @@ final class SelectorList extends Selector {
181189
? simples.map((simple) => switch (simple) {
182190
PseudoSelector(:var selector?)
183191
when _containsParentSelector(selector) =>
184-
simple.withSelector(selector.resolveParentSelectors(parent,
185-
implicitParent: false)),
192+
simple.withSelector(
193+
selector.nestWithin(parent, implicitParent: false)),
186194
_ => simple
187195
})
188196
: simples;
@@ -261,6 +269,8 @@ final class SelectorList extends Selector {
261269

262270
/// Returns a copy of `this` with [combinators] added to the end of each
263271
/// complex selector in [components].
272+
///
273+
/// @nodoc
264274
@internal
265275
SelectorList withAdditionalCombinators(
266276
List<CssValue<Combinator>> combinators) =>

lib/src/functions/selector.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ final _nest = _function("nest", r"$selectors...", (arguments) {
5252
first = false;
5353
return result;
5454
})
55-
.reduce((parent, child) => child.resolveParentSelectors(parent))
55+
.reduce((parent, child) => child.nestWithin(parent))
5656
.asSassList;
5757
});
5858

@@ -83,7 +83,7 @@ final _append = _function("append", r"$selectors...", (arguments) {
8383
...rest
8484
], span);
8585
}), span)
86-
.resolveParentSelectors(parent);
86+
.nestWithin(parent);
8787
}).asSassList;
8888
});
8989

lib/src/parse/selector.dart

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,24 @@ class SelectorParser extends Parser {
3131
/// Whether this parser allows the parent selector `&`.
3232
final bool _allowParent;
3333

34-
/// Whether this parser allows placeholder selectors beginning with `%`.
35-
final bool _allowPlaceholder;
34+
/// Whether to parse the selector as plain CSS.
35+
final bool _plainCss;
3636

37+
/// Creates a parser that parses CSS selectors.
38+
///
39+
/// If [allowParent] is `false`, this will throw a [SassFormatException] if
40+
/// the selector includes the parent selector `&`.
41+
///
42+
/// If [plainCss] is `true`, this will parse the selector as a plain CSS
43+
/// selector rather than a Sass selector.
3744
SelectorParser(super.contents,
3845
{super.url,
3946
super.logger,
4047
super.interpolationMap,
4148
bool allowParent = true,
42-
bool allowPlaceholder = true})
49+
bool plainCss = false})
4350
: _allowParent = allowParent,
44-
_allowPlaceholder = allowPlaceholder;
51+
_plainCss = plainCss;
4552

4653
SelectorList parse() {
4754
return wrapSpanFormatException(() {
@@ -165,7 +172,9 @@ class SelectorParser extends Parser {
165172
}
166173
}
167174

168-
if (lastCompound != null) {
175+
if (combinators.isNotEmpty && _plainCss) {
176+
scanner.error("expected selector.");
177+
} else if (lastCompound != null) {
169178
components.add(ComplexSelectorComponent(
170179
lastCompound, combinators, spanFrom(componentStart)));
171180
} else if (combinators.isNotEmpty) {
@@ -184,8 +193,8 @@ class SelectorParser extends Parser {
184193
var start = scanner.state;
185194
var components = <SimpleSelector>[_simpleSelector()];
186195

187-
while (isSimpleSelectorStart(scanner.peekChar())) {
188-
components.add(_simpleSelector(allowParent: false));
196+
while (_isSimpleSelectorStart(scanner.peekChar())) {
197+
components.add(_simpleSelector(allowParent: _plainCss));
189198
}
190199

191200
return CompoundSelector(components, spanFrom(start));
@@ -207,8 +216,8 @@ class SelectorParser extends Parser {
207216
return _idSelector();
208217
case $percent:
209218
var selector = _placeholderSelector();
210-
if (!_allowPlaceholder) {
211-
error("Placeholder selectors aren't allowed here.",
219+
if (_plainCss) {
220+
error("Placeholder selectors aren't allowed in plain CSS.",
212221
scanner.spanFrom(start));
213222
}
214223
return selector;
@@ -340,6 +349,11 @@ class SelectorParser extends Parser {
340349
var start = scanner.state;
341350
scanner.expectChar($ampersand);
342351
var suffix = lookingAtIdentifierBody() ? identifierBody() : null;
352+
if (_plainCss && suffix != null) {
353+
scanner.error("Parent selectors can't have suffixes in plain CSS.",
354+
position: start.position, length: scanner.position - start.position);
355+
}
356+
343357
return ParentSelector(spanFrom(start), suffix: suffix);
344358
}
345359

@@ -457,4 +471,12 @@ class SelectorParser extends Parser {
457471
spanFrom(start));
458472
}
459473
}
474+
475+
// Returns whether [character] can start a simple selector in the middle of a
476+
// compound selector.
477+
bool _isSimpleSelectorStart(int? character) => switch (character) {
478+
$asterisk || $lbracket || $dot || $hash || $percent || $colon => true,
479+
$ampersand => _plainCss,
480+
_ => false
481+
};
460482
}

lib/src/parse/stylesheet.dart

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -324,10 +324,6 @@ abstract class StylesheetParser extends Parser {
324324
/// parsed as a selector and never as a property with nested properties
325325
/// beneath it.
326326
Statement _declarationOrStyleRule() {
327-
if (plainCss && _inStyleRule && !_inUnknownAtRule) {
328-
return _propertyOrVariableDeclaration();
329-
}
330-
331327
// The indented syntax allows a single backslash to distinguish a style rule
332328
// from old-style property syntax. We don't support old property syntax, but
333329
// we do support the backslash because it's easy to do.
@@ -400,10 +396,7 @@ abstract class StylesheetParser extends Parser {
400396
}
401397

402398
var postColonWhitespace = rawText(whitespace);
403-
if (lookingAtChildren()) {
404-
return _withChildren(_declarationChild, start,
405-
(children, span) => Declaration.nested(name, children, span));
406-
}
399+
if (_tryDeclarationChildren(name, start) case var nested?) return nested;
407400

408401
midBuffer.write(postColonWhitespace);
409402
var couldBeSelector =
@@ -439,12 +432,8 @@ abstract class StylesheetParser extends Parser {
439432
return nameBuffer;
440433
}
441434

442-
if (lookingAtChildren()) {
443-
return _withChildren(
444-
_declarationChild,
445-
start,
446-
(children, span) =>
447-
Declaration.nested(name, children, span, value: value));
435+
if (_tryDeclarationChildren(name, start, value: value) case var nested?) {
436+
return nested;
448437
} else {
449438
expectStatementSeparator();
450439
return Declaration(name, value, scanner.spanFrom(start));
@@ -549,31 +538,36 @@ abstract class StylesheetParser extends Parser {
549538
}
550539

551540
whitespace();
552-
553-
if (lookingAtChildren()) {
554-
if (plainCss) {
555-
scanner.error("Nested declarations aren't allowed in plain CSS.");
556-
}
557-
return _withChildren(_declarationChild, start,
558-
(children, span) => Declaration.nested(name, children, span));
559-
}
541+
if (_tryDeclarationChildren(name, start) case var nested?) return nested;
560542

561543
var value = _expression();
562-
if (lookingAtChildren()) {
563-
if (plainCss) {
564-
scanner.error("Nested declarations aren't allowed in plain CSS.");
565-
}
566-
return _withChildren(
567-
_declarationChild,
568-
start,
569-
(children, span) =>
570-
Declaration.nested(name, children, span, value: value));
544+
if (_tryDeclarationChildren(name, start, value: value) case var nested?) {
545+
return nested;
571546
} else {
572547
expectStatementSeparator();
573548
return Declaration(name, value, scanner.spanFrom(start));
574549
}
575550
}
576551

552+
/// Tries parsing nested children of a declaration whose [name] has already
553+
/// been parsed, and returns `null` if it doesn't have any.
554+
///
555+
/// If [value] is passed, it's used as the value of the peroperty without
556+
/// nesting.
557+
Declaration? _tryDeclarationChildren(
558+
Interpolation name, LineScannerState start,
559+
{Expression? value}) {
560+
if (!lookingAtChildren()) return null;
561+
if (plainCss) {
562+
scanner.error("Nested declarations aren't allowed in plain CSS.");
563+
}
564+
return _withChildren(
565+
_declarationChild,
566+
start,
567+
(children, span) =>
568+
Declaration.nested(name, children, span, value: value));
569+
}
570+
577571
/// Consumes a statement that's allowed within a declaration.
578572
Statement _declarationChild() => scanner.peekChar() == $at
579573
? _declarationAtRule()

lib/src/util/character.dart

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,6 @@ int combineSurrogates(int highSurrogate, int lowSurrogate) =>
9292
// high/low surrogates.
9393
0x10000 + ((highSurrogate & 0x3FF) << 10) + (lowSurrogate & 0x3FF);
9494

95-
// Returns whether [character] can start a simple selector other than a type
96-
// selector.
97-
bool isSimpleSelectorStart(int? character) =>
98-
character == $asterisk ||
99-
character == $lbracket ||
100-
character == $dot ||
101-
character == $hash ||
102-
character == $percent ||
103-
character == $colon;
104-
10595
/// Returns whether [identifier] is module-private.
10696
///
10797
/// Assumes [identifier] is a valid Sass identifier.

0 commit comments

Comments
 (0)