From fd4c50c3d9b881c43210c173500a07e19f0fd6ce Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 15 Jul 2022 15:43:48 -0700 Subject: [PATCH] Expose selector APIs through sass_api (#1741) --- lib/sass.dart | 2 +- lib/src/ast/selector.dart | 5 + lib/src/ast/selector/attribute.dart | 5 + lib/src/ast/selector/class.dart | 7 + lib/src/ast/selector/complex.dart | 30 ++++ lib/src/ast/selector/compound.dart | 7 + lib/src/ast/selector/id.dart | 9 ++ lib/src/ast/selector/list.dart | 9 +- lib/src/ast/selector/parent.dart | 7 + lib/src/ast/selector/placeholder.dart | 9 ++ lib/src/ast/selector/pseudo.dart | 16 +++ lib/src/ast/selector/qualified_name.dart | 7 +- lib/src/ast/selector/simple.dart | 10 ++ lib/src/ast/selector/type.dart | 10 ++ lib/src/ast/selector/universal.dart | 7 + lib/src/parse/selector.dart | 8 ++ lib/src/value.dart | 176 +++++++++++++---------- lib/src/visitor/interface/selector.dart | 2 + lib/src/visitor/recursive_selector.dart | 43 ++++++ pkg/sass_api/CHANGELOG.md | 16 +++ pkg/sass_api/lib/sass_api.dart | 5 +- pkg/sass_api/pubspec.yaml | 2 +- 22 files changed, 310 insertions(+), 82 deletions(-) create mode 100644 lib/src/visitor/recursive_selector.dart diff --git a/lib/sass.dart b/lib/sass.dart index dfdaa50a4..0f77eaea0 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -26,7 +26,7 @@ export 'src/exception.dart' show SassException; export 'src/importer.dart'; export 'src/logger.dart'; export 'src/syntax.dart'; -export 'src/value.dart' hide ColorFormat, SassApiColor, SpanColorFormat; +export 'src/value.dart' hide ColorFormat, SassApiColor, SassApiValue, SpanColorFormat; export 'src/visitor/serialize.dart' show OutputStyle; export 'src/evaluation_context.dart' show warn; diff --git a/lib/src/ast/selector.dart b/lib/src/ast/selector.dart index e2bbe3c02..528eef18a 100644 --- a/lib/src/ast/selector.dart +++ b/lib/src/ast/selector.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 '../visitor/interface/selector.dart'; import '../visitor/serialize.dart'; @@ -25,9 +27,12 @@ export 'selector/universal.dart'; /// [ParentSelector] or a [PlaceholderSelector]. /// /// Selectors have structural equality semantics. +/// +/// {@category Selector} abstract class Selector { /// Whether this selector, and complex selectors containing it, should not be /// emitted. + @internal bool get isInvisible => false; /// Calls the appropriate visit method on [visitor]. diff --git a/lib/src/ast/selector/attribute.dart b/lib/src/ast/selector/attribute.dart index a3fbdcb18..df4d69417 100644 --- a/lib/src/ast/selector/attribute.dart +++ b/lib/src/ast/selector/attribute.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 '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -9,6 +11,9 @@ import '../selector.dart'; /// /// This selects for elements with the given attribute, and optionally with a /// value matching certain conditions as well. +/// +/// {@category Selector} +@sealed class AttributeSelector extends SimpleSelector { /// The name of the attribute being selected for. final QualifiedName name; diff --git a/lib/src/ast/selector/class.dart b/lib/src/ast/selector/class.dart index 7ff267f42..8fc96da0b 100644 --- a/lib/src/ast/selector/class.dart +++ b/lib/src/ast/selector/class.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 '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -9,6 +11,9 @@ import '../selector.dart'; /// /// This selects elements whose `class` attribute contains an identifier with /// the given name. +/// +/// {@category Selector} +@sealed class ClassSelector extends SimpleSelector { /// The class name this selects for. final String name; @@ -20,6 +25,8 @@ class ClassSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitClassSelector(this); + /// @nodoc + @internal ClassSelector addSuffix(String suffix) => ClassSelector(name + suffix); int get hashCode => name.hashCode; diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart index 0bf9076aa..e7d1f32a2 100644 --- a/lib/src/ast/selector/complex.dart +++ b/lib/src/ast/selector/complex.dart @@ -2,7 +2,11 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../../extend/functions.dart'; +import '../../logger.dart'; +import '../../parse/selector.dart'; import '../../utils.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -11,6 +15,9 @@ import '../selector.dart'; /// /// A complex selector is composed of [CompoundSelector]s separated by /// [Combinator]s. It selects elements based on their parent selectors. +/// +/// {@category Selector} +@sealed class ComplexSelector extends Selector { /// The components of this selector. /// @@ -25,6 +32,9 @@ class ComplexSelector extends Selector { final List components; /// Whether a line break should be emitted *before* this selector. + /// + /// @nodoc + @internal final bool lineBreak; /// The minimum possible specificity that this selector can have. @@ -49,6 +59,8 @@ class ComplexSelector extends Selector { int? _maxSpecificity; + /// @nodoc + @internal late final bool isInvisible = components.any( (component) => component is CompoundSelector && component.isInvisible); @@ -60,6 +72,19 @@ class ComplexSelector extends Selector { } } + /// Parses a complex selector from [contents]. + /// + /// If passed, [url] is the name of the file from which [contents] comes. + /// [allowParent] controls whether a [ParentSelector] is allowed in this + /// selector. + /// + /// Throws a [SassFormatException] if parsing fails. + factory ComplexSelector.parse(String contents, + {Object? url, Logger? logger, bool allowParent = true}) => + SelectorParser(contents, + url: url, logger: logger, allowParent: allowParent) + .parseComplexSelector(); + T accept(SelectorVisitor visitor) => visitor.visitComplexSelector(this); /// Whether this is a superselector of [other]. @@ -92,10 +117,15 @@ class ComplexSelector extends Selector { /// A component of a [ComplexSelector]. /// /// This is either a [CompoundSelector] or a [Combinator]. +/// +/// {@category Selector} abstract class ComplexSelectorComponent {} /// A combinator that defines the relationship between selectors in a /// [ComplexSelector]. +/// +/// {@category Selector} +@sealed class Combinator implements ComplexSelectorComponent { /// Matches the right-hand selector if it's immediately adjacent to the /// left-hand selector in the DOM tree. diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index f5241250e..22e58c2f3 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.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 '../../extend/functions.dart'; import '../../logger.dart'; import '../../parse/selector.dart'; @@ -13,6 +15,9 @@ import '../selector.dart'; /// /// A compound selector is composed of [SimpleSelector]s. It matches an element /// that matches all of the component simple selectors. +/// +/// {@category Selector} +@sealed class CompoundSelector extends Selector implements ComplexSelectorComponent { /// The components of this selector. /// @@ -41,6 +46,8 @@ class CompoundSelector extends Selector implements ComplexSelectorComponent { int? _maxSpecificity; + /// @nodoc + @internal bool get isInvisible => components.any((component) => component.isInvisible); CompoundSelector(Iterable components) diff --git a/lib/src/ast/selector/id.dart b/lib/src/ast/selector/id.dart index 21c3eb1b3..db1108cdc 100644 --- a/lib/src/ast/selector/id.dart +++ b/lib/src/ast/selector/id.dart @@ -4,12 +4,17 @@ import 'dart:math' as math; +import 'package:meta/meta.dart'; + import '../../visitor/interface/selector.dart'; import '../selector.dart'; /// An ID selector. /// /// This selects elements whose `id` attribute exactly matches the given name. +/// +/// {@category Selector} +@sealed class IDSelector extends SimpleSelector { /// The ID name this selects for. final String name; @@ -20,8 +25,12 @@ class IDSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitIDSelector(this); + /// @nodoc + @internal IDSelector addSuffix(String suffix) => IDSelector(name + suffix); + /// @nodoc + @internal List? unify(List compound) { // A given compound selector may only contain one ID. if (compound.any((simple) => simple is IDSelector && simple != this)) { diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index e8670e3a7..72e1a0054 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.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 '../../extend/functions.dart'; import '../../logger.dart'; import '../../parse/selector.dart'; @@ -13,8 +15,11 @@ import '../selector.dart'; /// A selector list. /// -/// A selector list is composed of [ComplexSelector]s. It matches an element +/// A selector list is composed of [ComplexSelector]s. It matches any element /// that matches any of the component selectors. +/// +/// {@category Selector} +@sealed class SelectorList extends Selector { /// The components of this selector. /// @@ -25,6 +30,8 @@ class SelectorList extends Selector { bool get _containsParentSelector => components.any(_complexContainsParentSelector); + /// @nodoc + @internal bool get isInvisible => components.every((complex) => complex.isInvisible); /// Returns a SassScript list that represents this selector. diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart index ed2b77068..01bdb5084 100644 --- a/lib/src/ast/selector/parent.dart +++ b/lib/src/ast/selector/parent.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 '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -9,6 +11,9 @@ import '../selector.dart'; /// /// This is not a plain CSS selector—it should be removed before emitting a CSS /// document. +/// +/// {@category Selector} +@sealed class ParentSelector extends SimpleSelector { /// The suffix that will be added to the parent selector after it's been /// resolved. @@ -21,6 +26,8 @@ class ParentSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitParentSelector(this); + /// @nodoc + @internal List unify(List compound) => throw UnsupportedError("& doesn't support unification."); } diff --git a/lib/src/ast/selector/placeholder.dart b/lib/src/ast/selector/placeholder.dart index 66b0cd472..ffdf7d9a9 100644 --- a/lib/src/ast/selector/placeholder.dart +++ b/lib/src/ast/selector/placeholder.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 '../../util/character.dart' as character; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -11,10 +13,15 @@ import '../selector.dart'; /// This doesn't match any elements. It's intended to be extended using /// `@extend`. It's not a plain CSS selector—it should be removed before /// emitting a CSS document. +/// +/// {@category Selector} +@sealed class PlaceholderSelector extends SimpleSelector { /// The name of the placeholder. final String name; + /// @nodoc + @internal bool get isInvisible => true; /// Returns whether this is a private selector (that is, whether it begins @@ -26,6 +33,8 @@ class PlaceholderSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitPlaceholderSelector(this); + /// @nodoc + @internal PlaceholderSelector addSuffix(String suffix) => PlaceholderSelector(name + suffix); diff --git a/lib/src/ast/selector/pseudo.dart b/lib/src/ast/selector/pseudo.dart index 815a4d2ba..df20dc0c6 100644 --- a/lib/src/ast/selector/pseudo.dart +++ b/lib/src/ast/selector/pseudo.dart @@ -17,11 +17,17 @@ import '../selector.dart'; /// selectors take arguments, including other selectors. Sass manually encodes /// logic for each pseudo selector that takes a selector as an argument, to /// ensure that extension and other selector operations work properly. +/// +/// {@category Selector} +@sealed class PseudoSelector extends SimpleSelector { /// The name of this selector. final String name; /// Like [name], but without any vendor prefixes. + /// + /// @nodoc + @internal final String normalizedName; /// Whether this is a pseudo-class selector. @@ -49,10 +55,14 @@ class PseudoSelector extends SimpleSelector { bool get isSyntacticElement => !isSyntacticClass; /// Whether this is a valid `:host` selector. + /// + /// @nodoc @internal bool get isHost => isClass && name == 'host'; /// Whether this is a valid `:host-context` selector. + /// + /// @nodoc @internal bool get isHostContext => isClass && name == 'host-context' && selector != null; @@ -83,6 +93,8 @@ class PseudoSelector extends SimpleSelector { int? _maxSpecificity; + /// @nodoc + @internal bool get isInvisible { var selector = this.selector; if (selector == null) return false; @@ -128,11 +140,15 @@ class PseudoSelector extends SimpleSelector { PseudoSelector withSelector(SelectorList selector) => PseudoSelector(name, element: isElement, argument: argument, selector: selector); + /// @nodoc + @internal PseudoSelector addSuffix(String suffix) { if (argument != null || selector != null) super.addSuffix(suffix); return PseudoSelector(name + suffix, element: isElement); } + /// @nodoc + @internal List? unify(List compound) { if (name == 'host' || name == 'host-context') { if (!compound.every((simple) => diff --git a/lib/src/ast/selector/qualified_name.dart b/lib/src/ast/selector/qualified_name.dart index 9abcfe0f7..a995c5440 100644 --- a/lib/src/ast/selector/qualified_name.dart +++ b/lib/src/ast/selector/qualified_name.dart @@ -2,9 +2,14 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -/// A [qualified name][]. +import 'package:meta/meta.dart'; + +/// A [qualified name]. /// /// [qualified name]: https://www.w3.org/TR/css3-namespace/#css-qnames +/// +/// {@category Selector} +@sealed class QualifiedName { /// The identifier name. final String name; diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index 2bc094e4f..fb980ec18 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -2,12 +2,16 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../../exception.dart'; import '../../logger.dart'; import '../../parse/selector.dart'; import '../selector.dart'; /// An abstract superclass for simple selectors. +/// +/// {@category Selector} abstract class SimpleSelector extends Selector { /// The minimum possible specificity that this selector can have. /// @@ -45,6 +49,9 @@ abstract class SimpleSelector extends Selector { /// /// Assumes [suffix] is a valid identifier suffix. If this wouldn't produce a /// valid [SimpleSelector], throws a [SassScriptException]. + /// + /// @nodoc + @internal SimpleSelector addSuffix(String suffix) => throw SassScriptException('Invalid parent selector "$this"'); @@ -57,6 +64,9 @@ abstract class SimpleSelector extends Selector { /// /// Returns `null` if unification is impossible—for example, if there are /// multiple ID selectors. + /// + /// @nodoc + @internal List? unify(List compound) { if (compound.length == 1) { var other = compound.first; diff --git a/lib/src/ast/selector/type.dart b/lib/src/ast/selector/type.dart index 28d1ba8f4..0d9276ce0 100644 --- a/lib/src/ast/selector/type.dart +++ b/lib/src/ast/selector/type.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 '../../extend/functions.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -9,7 +11,11 @@ import '../selector.dart'; /// A type selector. /// /// This selects elements whose name equals the given name. +/// +/// {@category Selector} +@sealed class TypeSelector extends SimpleSelector { + /// The element name being selected. final QualifiedName name; int get minSpecificity => 1; @@ -18,9 +24,13 @@ class TypeSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitTypeSelector(this); + /// @nodoc + @internal TypeSelector addSuffix(String suffix) => TypeSelector( QualifiedName(name.name + suffix, namespace: name.namespace)); + /// @nodoc + @internal List? unify(List compound) { if (compound.first is UniversalSelector || compound.first is TypeSelector) { var unified = unifyUniversalAndElement(this, compound.first); diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index ee4c676f4..f42cf4015 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -2,11 +2,16 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../../extend/functions.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; /// Matches any element in the given namespace. +/// +/// {@category Selector} +@sealed class UniversalSelector extends SimpleSelector { /// The selector namespace. /// @@ -23,6 +28,8 @@ class UniversalSelector extends SimpleSelector { T accept(SelectorVisitor visitor) => visitor.visitUniversalSelector(this); + /// @nodoc + @internal List? unify(List compound) { var first = compound.first; if (first is UniversalSelector || first is TypeSelector) { diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 77d156885..4442275ad 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -51,6 +51,14 @@ class SelectorParser extends Parser { }); } + ComplexSelector parseComplexSelector() { + return wrapSpanFormatException(() { + var complex = _complexSelector(); + if (!scanner.isDone) scanner.error("expected selector."); + return complex; + }); + } + CompoundSelector parseCompoundSelector() { return wrapSpanFormatException(() { var compound = _compoundSelector(); diff --git a/lib/src/value.dart b/lib/src/value.dart index 950f39cae..ed138c05d 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -187,83 +187,6 @@ abstract class Value { SassString assertString([String? name]) => throw _exception("$this is not a string.", name); - /// Parses [this] as a selector list, in the same manner as the - /// `selector-parse()` function. - /// - /// Throws a [SassScriptException] if this isn't a type that can be parsed as a - /// selector, or if parsing fails. If [allowParent] is `true`, this allows - /// [ParentSelector]s. Otherwise, they're considered parse errors. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - /// - /// @nodoc - @internal - SelectorList assertSelector({String? name, bool allowParent = false}) { - var string = _selectorString(name); - try { - return SelectorList.parse(string, allowParent: allowParent); - } on SassFormatException catch (error, stackTrace) { - // TODO(nweiz): colorize this if we're running in an environment where - // that works. - throwWithTrace( - _exception(error.toString().replaceFirst("Error: ", ""), name), - stackTrace); - } - } - - /// Parses [this] as a simple selector, in the same manner as the - /// `selector-parse()` function. - /// - /// Throws a [SassScriptException] if this isn't a type that can be parsed as a - /// selector, or if parsing fails. If [allowParent] is `true`, this allows - /// [ParentSelector]s. Otherwise, they're considered parse errors. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - /// - /// @nodoc - @internal - SimpleSelector assertSimpleSelector( - {String? name, bool allowParent = false}) { - var string = _selectorString(name); - try { - return SimpleSelector.parse(string, allowParent: allowParent); - } on SassFormatException catch (error, stackTrace) { - // TODO(nweiz): colorize this if we're running in an environment where - // that works. - throwWithTrace( - _exception(error.toString().replaceFirst("Error: ", ""), name), - stackTrace); - } - } - - /// Parses [this] as a compound selector, in the same manner as the - /// `selector-parse()` function. - /// - /// Throws a [SassScriptException] if this isn't a type that can be parsed as a - /// selector, or if parsing fails. If [allowParent] is `true`, this allows - /// [ParentSelector]s. Otherwise, they're considered parse errors. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - /// - /// @nodoc - @internal - CompoundSelector assertCompoundSelector( - {String? name, bool allowParent = false}) { - var string = _selectorString(name); - try { - return CompoundSelector.parse(string, allowParent: allowParent); - } on SassFormatException catch (error, stackTrace) { - // TODO(nweiz): colorize this if we're running in an environment where - // that works. - throwWithTrace( - _exception(error.toString().replaceFirst("Error: ", ""), name), - stackTrace); - } - } - /// Converts a `selector-parse()`-style input into a string that can be /// parsed. /// @@ -466,3 +389,102 @@ abstract class Value { SassScriptException _exception(String message, [String? name]) => SassScriptException(name == null ? message : "\$$name: $message"); } + +/// Extension methods that are only visible through the `sass_api` package. +/// +/// These methods are considered less general-purpose and more liable to change +/// than the main [Value] interface. +/// +/// {@category Value} +extension SassApiValue on Value { + /// Parses [this] as a selector list, in the same manner as the + /// `selector-parse()` function. + /// + /// Throws a [SassScriptException] if this isn't a type that can be parsed as a + /// selector, or if parsing fails. If [allowParent] is `true`, this allows + /// [ParentSelector]s. Otherwise, they're considered parse errors. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + SelectorList assertSelector({String? name, bool allowParent = false}) { + var string = _selectorString(name); + try { + return SelectorList.parse(string, allowParent: allowParent); + } on SassFormatException catch (error, stackTrace) { + // TODO(nweiz): colorize this if we're running in an environment where + // that works. + throwWithTrace( + _exception(error.toString().replaceFirst("Error: ", ""), name), + stackTrace); + } + } + + /// Parses [this] as a simple selector, in the same manner as the + /// `selector-parse()` function. + /// + /// Throws a [SassScriptException] if this isn't a type that can be parsed as a + /// selector, or if parsing fails. If [allowParent] is `true`, this allows + /// [ParentSelector]s. Otherwise, they're considered parse errors. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + SimpleSelector assertSimpleSelector( + {String? name, bool allowParent = false}) { + var string = _selectorString(name); + try { + return SimpleSelector.parse(string, allowParent: allowParent); + } on SassFormatException catch (error, stackTrace) { + // TODO(nweiz): colorize this if we're running in an environment where + // that works. + throwWithTrace( + _exception(error.toString().replaceFirst("Error: ", ""), name), + stackTrace); + } + } + + /// Parses [this] as a compound selector, in the same manner as the + /// `selector-parse()` function. + /// + /// Throws a [SassScriptException] if this isn't a type that can be parsed as a + /// selector, or if parsing fails. If [allowParent] is `true`, this allows + /// [ParentSelector]s. Otherwise, they're considered parse errors. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + CompoundSelector assertCompoundSelector( + {String? name, bool allowParent = false}) { + var string = _selectorString(name); + try { + return CompoundSelector.parse(string, allowParent: allowParent); + } on SassFormatException catch (error, stackTrace) { + // TODO(nweiz): colorize this if we're running in an environment where + // that works. + throwWithTrace( + _exception(error.toString().replaceFirst("Error: ", ""), name), + stackTrace); + } + } + + /// Parses [this] as a complex selector, in the same manner as the + /// `selector-parse()` function. + /// + /// Throws a [SassScriptException] if this isn't a type that can be parsed as a + /// selector, or if parsing fails. If [allowParent] is `true`, this allows + /// [ParentSelector]s. Otherwise, they're considered parse errors. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. + ComplexSelector assertComplexSelector( + {String? name, bool allowParent = false}) { + var string = _selectorString(name); + try { + return ComplexSelector.parse(string, allowParent: allowParent); + } on SassFormatException catch (error, stackTrace) { + // TODO(nweiz): colorize this if we're running in an environment where + // that works. + throwWithTrace( + _exception(error.toString().replaceFirst("Error: ", ""), name), + stackTrace); + } + } +} diff --git a/lib/src/visitor/interface/selector.dart b/lib/src/visitor/interface/selector.dart index 108c6ef67..91b68913c 100644 --- a/lib/src/visitor/interface/selector.dart +++ b/lib/src/visitor/interface/selector.dart @@ -7,6 +7,8 @@ import '../../ast/selector.dart'; /// An interface for [visitors][] that traverse selectors. /// /// [visitors]: https://en.wikipedia.org/wiki/Visitor_pattern +/// +/// {@category Visitor} abstract class SelectorVisitor { T visitAttributeSelector(AttributeSelector attribute); T visitClassSelector(ClassSelector klass); diff --git a/lib/src/visitor/recursive_selector.dart b/lib/src/visitor/recursive_selector.dart new file mode 100644 index 000000000..390e92bf6 --- /dev/null +++ b/lib/src/visitor/recursive_selector.dart @@ -0,0 +1,43 @@ +// Copyright 2022 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import '../ast/selector.dart'; +import '../util/nullable.dart'; +import 'interface/selector.dart'; + +/// A visitor that recursively traverses each component of a Selector AST. +/// +/// {@category Visitor} +abstract class RecursiveSelectorVisitor implements SelectorVisitor { + const RecursiveSelectorVisitor(); + + void visitAttributeSelector(AttributeSelector attribute) {} + void visitClassSelector(ClassSelector klass) {} + void visitIDSelector(IDSelector id) {} + void visitParentSelector(ParentSelector placeholder) {} + void visitPlaceholderSelector(PlaceholderSelector placeholder) {} + void visitTypeSelector(TypeSelector type) {} + void visitUniversalSelector(UniversalSelector universal) {} + + void visitComplexSelector(ComplexSelector complex) { + for (var component in complex.components) { + if (component is CompoundSelector) visitCompoundSelector(component); + } + } + + void visitCompoundSelector(CompoundSelector compound) { + for (var simple in compound.components) { + simple.accept(this); + } + } + + void visitPseudoSelector(PseudoSelector pseudo) => + pseudo.selector.andThen(visitSelectorList); + + void visitSelectorList(SelectorList list) { + for (var complex in list.components) { + visitComplexSelector(complex); + } + } +} diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 0d537dc4e..dab80c12e 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,19 @@ +## 1.1.0 + +* Provide access to Sass's selector AST, including the following classes: + `Selector`, `ListSelector`, `ComplexSelector`, `ComplexSelectorComponent`, + `Combinator`, `CompoundSelector`, `SimpleSelector`, `AttributeSelector`, + `AttributeOperator`, `ClassSelector`, `IdSelector`, `ParentSelector`, + `PlaceholderSelector`, `PseudoSelector`, `TypeSelector`, `UniversalSelector`, + and `QualifiedName`. + +* Provide access to the `SelectorVisitor` and `RecursiveSelectorVisitor` + classes. + +* Provide access to the `Value.assertSelector()`, + `Value.assertComplexSelector()`, `Value.assertCompoundSelector()`, and + `Value.assertSimpleSelector()` methods. + ## 1.0.0 * First stable release. diff --git a/pkg/sass_api/lib/sass_api.dart b/pkg/sass_api/lib/sass_api.dart index 6dee9ad35..c839afe17 100644 --- a/pkg/sass_api/lib/sass_api.dart +++ b/pkg/sass_api/lib/sass_api.dart @@ -13,14 +13,17 @@ import 'package:sass/src/parse/parser.dart'; export 'package:sass/sass.dart'; export 'package:sass/src/ast/node.dart'; export 'package:sass/src/ast/sass.dart' hide AtRootQuery; +export 'package:sass/src/ast/selector.dart'; export 'package:sass/src/async_import_cache.dart'; export 'package:sass/src/exception.dart' show SassFormatException; export 'package:sass/src/import_cache.dart'; -export 'package:sass/src/value/color.dart' hide ColorFormat, SpanColorFormat; +export 'package:sass/src/value.dart' hide ColorFormat, SpanColorFormat; export 'package:sass/src/visitor/find_dependencies.dart'; export 'package:sass/src/visitor/interface/expression.dart'; +export 'package:sass/src/visitor/interface/selector.dart'; export 'package:sass/src/visitor/interface/statement.dart'; export 'package:sass/src/visitor/recursive_ast.dart'; +export 'package:sass/src/visitor/recursive_selector.dart'; export 'package:sass/src/visitor/recursive_statement.dart'; export 'package:sass/src/visitor/statement_search.dart'; diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 5ebe2caed..55dcb4ddb 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: 1.0.0 +version: 1.1.0-dev description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass