Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for nesting in plain CSS #2198

Merged
merged 3 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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