diff --git a/CHANGELOG.md b/CHANGELOG.md index 37309686e..3096f4dc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ * Fix a race condition where `meta.load-css()` could trigger an internal error when running in asynchronous mode. +### Dart API + +* Use the `@internal` annotation to indicate which `Value` APIs are available + for public use. + ## 1.35.1 * Fix a bug where the quiet dependency flag didn't silence warnings in some diff --git a/lib/sass.dart b/lib/sass.dart index c52b7187a..106192938 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -24,8 +24,7 @@ export 'src/exception.dart' show SassException; export 'src/importer.dart'; export 'src/logger.dart'; export 'src/syntax.dart'; -export 'src/value.dart' show ListSeparator; -export 'src/value/external/value.dart'; +export 'src/value.dart'; export 'src/visitor/serialize.dart' show OutputStyle; export 'src/warn.dart' show warn; diff --git a/lib/src/callable.dart b/lib/src/callable.dart index 51e774413..91222f312 100644 --- a/lib/src/callable.dart +++ b/lib/src/callable.dart @@ -7,7 +7,6 @@ import 'package:meta/meta.dart'; import 'callable/async.dart'; import 'callable/built_in.dart'; import 'value.dart'; -import 'value/external/value.dart' as ext; export 'callable/async.dart'; export 'callable/async_built_in.dart'; @@ -67,7 +66,7 @@ export 'callable/user_defined.dart'; abstract class Callable extends AsyncCallable { @Deprecated('Use `Callable.function` instead.') factory Callable(String name, String arguments, - ext.Value callback(List arguments)) => + Value callback(List arguments)) => Callable.function(name, arguments, callback); /// Creates a function with the given [name] and [arguments] that runs @@ -113,7 +112,6 @@ abstract class Callable extends AsyncCallable { /// which provides access to keyword arguments using /// [SassArgumentList.keywords]. factory Callable.function(String name, String arguments, - ext.Value callback(List arguments)) => - BuiltInCallable.function( - name, arguments, (arguments) => callback(arguments) as Value); + Value callback(List arguments)) => + BuiltInCallable.function(name, arguments, callback); } diff --git a/lib/src/callable/async.dart b/lib/src/callable/async.dart index a4cbc10ea..03fc40948 100644 --- a/lib/src/callable/async.dart +++ b/lib/src/callable/async.dart @@ -7,7 +7,6 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../value.dart'; -import '../value/external/value.dart' as ext; import 'async_built_in.dart'; /// An interface for functions and mixins that can be invoked from Sass by @@ -25,7 +24,7 @@ abstract class AsyncCallable { @Deprecated('Use `AsyncCallable.function` instead.') factory AsyncCallable(String name, String arguments, - FutureOr callback(List arguments)) => + FutureOr callback(List arguments)) => AsyncCallable.function(name, arguments, callback); /// Creates a callable with the given [name] and [arguments] that runs @@ -36,10 +35,6 @@ abstract class AsyncCallable { /// /// See [new Callable] for more details. factory AsyncCallable.function(String name, String arguments, - FutureOr callback(List arguments)) => - AsyncBuiltInCallable.function(name, arguments, (arguments) { - var result = callback(arguments); - if (result is ext.Value) return result as Value; - return result.then((value) => value as Value); - }); + FutureOr callback(List arguments)) => + AsyncBuiltInCallable.function(name, arguments, callback); } diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 1811e0d96..8dcb08020 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -78,7 +78,7 @@ body::before { } } -/// A [SassException] that's also a [MultiSpanSassException]. +/// A [SassException] that's also a [MultiSourceSpanException]. class MultiSpanSassException extends SassException implements MultiSourceSpanException { final String primaryLabel; diff --git a/lib/src/functions/meta.dart b/lib/src/functions/meta.dart index b959e0be7..c385d91af 100644 --- a/lib/src/functions/meta.dart +++ b/lib/src/functions/meta.dart @@ -40,7 +40,7 @@ final global = UnmodifiableListView([ if (value is SassColor) return SassString("color", quotes: false); if (value is SassList) return SassString("list", quotes: false); if (value is SassMap) return SassString("map", quotes: false); - if (value is SassNull) return SassString("null", quotes: false); + if (value == sassNull) return SassString("null", quotes: false); if (value is SassNumber) return SassString("number", quotes: false); if (value is SassFunction) return SassString("function", quotes: false); assert(value is SassString); diff --git a/lib/src/node/exports.dart b/lib/src/node/exports.dart index 06a550f78..db7739969 100644 --- a/lib/src/node/exports.dart +++ b/lib/src/node/exports.dart @@ -14,7 +14,7 @@ class Exports { external set renderSync(Function function); external set info(String info); external set types(Types types); - external set NULL(SassNull sassNull); + external set NULL(Value sassNull); external set TRUE(SassBoolean sassTrue); external set FALSE(SassBoolean sassFalse); } diff --git a/lib/src/value.dart b/lib/src/value.dart index 2e5759700..95b22e44b 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -8,7 +8,6 @@ import 'ast/selector.dart'; import 'exception.dart'; import 'value/boolean.dart'; import 'value/color.dart'; -import 'value/external/value.dart' as ext; import 'value/function.dart'; import 'value/list.dart'; import 'value/map.dart'; @@ -27,25 +26,49 @@ export 'value/null.dart'; export 'value/number.dart'; export 'value/string.dart'; -// TODO(nweiz): Just mark members as @internal when sdk#28066 is fixed. -/// The implementation of [ext.Value]. +/// A SassScript value. /// -/// This is a separate class to avoid exposing more API surface than necessary -/// to users outside this package. -abstract class Value implements ext.Value { +/// All SassScript values are unmodifiable. New values can be constructed using +/// subclass constructors like [new SassString]. Untyped values can be cast to +/// particular types using `assert*()` functions like [assertString], which +/// throw user-friendly error messages if they fail. +@sealed +abstract class Value { + /// Whether the value counts as `true` in an `@if` statement and other + /// contexts. bool get isTruthy => true; + + /// The separator for this value as a list. + /// + /// All SassScript values can be used as lists. Maps count as lists of pairs, + /// and all other values count as single-value lists. ListSeparator get separator => ListSeparator.undecided; + + /// Whether this value as a list has brackets. + /// + /// All SassScript values can be used as lists. Maps count as lists of pairs, + /// and all other values count as single-value lists. bool get hasBrackets => false; + + /// This value as a list. + /// + /// All SassScript values can be used as lists. Maps count as lists of pairs, + /// and all other values count as single-value lists. List get asList => [this]; /// The length of [asList]. /// /// This is used to compute [sassIndexToListIndex] without allocating a new /// list. + /// + /// @nodoc @protected int get lengthAsList => 1; /// Whether the value will be represented in CSS as the empty string. + /// + /// @nodoc + @internal bool get isBlank => false; /// Whether this is a value that CSS may treat as a number, such as `calc()` @@ -53,6 +76,9 @@ abstract class Value implements ext.Value { /// /// Functions that shadow plain CSS functions need to gracefully handle when /// these arguments are passed in. + /// + /// @nodoc + @internal bool get isSpecialNumber => false; /// Whether this is a call to `var()`, which may be substituted in CSS for a @@ -60,21 +86,35 @@ abstract class Value implements ext.Value { /// /// Functions that shadow plain CSS functions need to gracefully handle when /// these arguments are passed in. + /// + /// @nodoc + @internal bool get isVar => false; /// Returns Dart's `null` value if this is [sassNull], and returns [this] /// otherwise. Value? get realNull => this; + /// @nodoc const Value(); /// Calls the appropriate visit method on [visitor]. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal T accept(ValueVisitor visitor); - int sassIndexToListIndex(ext.Value sassIndex, [String? name]) { + /// Converts [sassIndex] into a Dart-style index into the list returned by + /// [asList]. + /// + /// Sass indexes are one-based, while Dart indexes are zero-based. Sass + /// indexes may also be negative in order to index from the end of the list. + /// + /// Throws a [SassScriptException] if [sassIndex] isn't a number, if that + /// number isn't an integer, or if that integer isn't a valid index for + /// [asList]. If [sassIndex] came from a function argument, [name] is the + /// argument name (without the `$`). It's used for error reporting. + int sassIndexToListIndex(Value sassIndex, [String? name]) { var index = sassIndex.assertNumber(name).assertInt(name); if (index == 0) throw _exception("List index may not be 0.", name); if (index.abs() > lengthAsList) { @@ -86,23 +126,52 @@ abstract class Value implements ext.Value { return index < 0 ? lengthAsList + index : index - 1; } + /// Throws a [SassScriptException] if [this] isn't a boolean. + /// + /// Note that generally, functions should use [isTruthy] rather than requiring + /// a literal boolean. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassBoolean assertBoolean([String? name]) => throw _exception("$this is not a boolean.", name); + /// Throws a [SassScriptException] if [this] isn't a color. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassColor assertColor([String? name]) => throw _exception("$this is not a color.", name); + /// Throws a [SassScriptException] if [this] isn't a function reference. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassFunction assertFunction([String? name]) => throw _exception("$this is not a function reference.", name); + /// Throws a [SassScriptException] if [this] isn't a map. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassMap assertMap([String? name]) => throw _exception("$this is not a map.", name); + /// Returns [this] as a [SassMap] if it is one (including empty lists, which + /// count as empty maps) or returns `null` if it's not. SassMap? tryMap() => null; + /// Throws a [SassScriptException] if [this] isn't a number. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassNumber assertNumber([String? name]) => throw _exception("$this is not a number.", name); + /// Throws a [SassScriptException] if [this] isn't a string. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassString assertString([String? name]) => throw _exception("$this is not a string.", name); @@ -115,6 +184,9 @@ abstract class Value implements ext.Value { /// /// 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 { @@ -135,6 +207,9 @@ abstract class Value implements ext.Value { /// /// 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); @@ -156,6 +231,9 @@ abstract class Value implements ext.Value { /// /// 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); @@ -227,6 +305,9 @@ abstract class Value implements ext.Value { /// Returns a new list containing [contents] that defaults to this value's /// separator and brackets. + /// + /// @nodoc + @internal SassList changeListContents(Iterable contents, {ListSeparator? separator, bool? brackets}) { return SassList(contents, separator ?? this.separator, @@ -235,57 +316,57 @@ abstract class Value implements ext.Value { /// The SassScript `=` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value singleEquals(Value other) => SassString("${toCssString()}=${other.toCssString()}", quotes: false); /// The SassScript `>` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal SassBoolean greaterThan(Value other) => throw SassScriptException('Undefined operation "$this > $other".'); /// The SassScript `>=` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal SassBoolean greaterThanOrEquals(Value other) => throw SassScriptException('Undefined operation "$this >= $other".'); /// The SassScript `<` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal SassBoolean lessThan(Value other) => throw SassScriptException('Undefined operation "$this < $other".'); /// The SassScript `<=` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal SassBoolean lessThanOrEquals(Value other) => throw SassScriptException('Undefined operation "$this <= $other".'); /// The SassScript `*` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value times(Value other) => throw SassScriptException('Undefined operation "$this * $other".'); /// The SassScript `%` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value modulo(Value other) => throw SassScriptException('Undefined operation "$this % $other".'); /// The SassScript `+` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value plus(Value other) { if (other is SassString) { return SassString(toCssString() + other.text, quotes: other.hasQuotes); @@ -296,48 +377,48 @@ abstract class Value implements ext.Value { /// The SassScript `-` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value minus(Value other) => SassString("${toCssString()}-${other.toCssString()}", quotes: false); /// The SassScript `/` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value dividedBy(Value other) => SassString("${toCssString()}/${other.toCssString()}", quotes: false); /// The SassScript unary `+` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value unaryPlus() => SassString("+${toCssString()}", quotes: false); /// The SassScript unary `-` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value unaryMinus() => SassString("-${toCssString()}", quotes: false); /// The SassScript unary `/` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value unaryDivide() => SassString("/${toCssString()}", quotes: false); /// The SassScript unary `not` operation. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value unaryNot() => sassFalse; /// Returns a copy of [this] without [SassNumber.asSlash] set. /// /// If this isn't a [SassNumber], returns it as-is. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal Value withoutSlash() => this; /// Returns a valid CSS representation of [this]. @@ -345,10 +426,17 @@ abstract class Value implements ext.Value { /// Throws a [SassScriptException] if [this] can't be represented in plain /// CSS. Use [toString] instead to get a string representation even if this /// isn't valid CSS. - /// - /// If [quote] is `false`, quoted strings are emitted without quotes. - String toCssString({bool quote = true}) => serializeValue(this, quote: quote); + // + // Internal-only: If [quote] is `false`, quoted strings are emitted without + // quotes. + String toCssString({@internal bool quote = true}) => + serializeValue(this, quote: quote); + /// Returns a string representation of [this]. + /// + /// Note that this is equivalent to calling `inspect()` on the value, and thus + /// won't reflect the user's output settings. [toCssString] should be used + /// instead to convert [this] to CSS. String toString() => serializeValue(this, inspect: true); /// Throws a [SassScriptException] with the given [message]. diff --git a/lib/src/value/argument_list.dart b/lib/src/value/argument_list.dart index 4e506ec00..7d2126f2c 100644 --- a/lib/src/value/argument_list.dart +++ b/lib/src/value/argument_list.dart @@ -2,10 +2,20 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../value.dart'; -import 'external/value.dart' as ext; -class SassArgumentList extends SassList implements ext.SassArgumentList { +/// A SassScript argument list. +/// +/// An argument list comes from a rest argument. It's distinct from a normal +/// [SassList] in that it may contain a keyword map as well as the positional +/// arguments. +@sealed +class SassArgumentList extends SassList { + /// The keyword arguments attached to this argument list. + /// + /// The argument names don't include `$`. Map get keywords { _wereKeywordsAccessed = true; return _keywords; @@ -18,8 +28,8 @@ class SassArgumentList extends SassList implements ext.SassArgumentList { /// This is used to determine whether to throw an exception about passing /// unexpected keywords. /// - /// **Note:** this function should not be called outside the `sass` package. - /// It's not guaranteed to be stable across versions. + /// @nodoc + @internal bool get wereKeywordsAccessed => _wereKeywordsAccessed; var _wereKeywordsAccessed = false; diff --git a/lib/src/value/boolean.dart b/lib/src/value/boolean.dart index 8b755428f..8c3fc64ad 100644 --- a/lib/src/value/boolean.dart +++ b/lib/src/value/boolean.dart @@ -2,9 +2,10 @@ // 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/value.dart'; import '../value.dart'; -import 'external/value.dart' as ext; /// The SassScript `true` value. const sassTrue = SassBoolean._(true); @@ -12,18 +13,29 @@ const sassTrue = SassBoolean._(true); /// The SassScript `false` value. const sassFalse = SassBoolean._(false); -class SassBoolean extends Value implements ext.SassBoolean { +/// A SassScript boolean value. +@sealed +class SassBoolean extends Value { + /// Whether this value is `true` or `false`. final bool value; bool get isTruthy => value; + /// Returns a [SassBoolean] corresponding to [value]. + /// + /// This just returns [sassTrue] or [sassFalse]; it doesn't allocate a new + /// value. factory SassBoolean(bool value) => value ? sassTrue : sassFalse; const SassBoolean._(this.value); + /// @nodoc + @internal T accept(ValueVisitor visitor) => visitor.visitBoolean(this); SassBoolean assertBoolean([String? name]) => this; + /// @nodoc + @internal Value unaryNot() => value ? sassFalse : sassTrue; } diff --git a/lib/src/value/color.dart b/lib/src/value/color.dart index 686b4e9ec..bf65a63a0 100644 --- a/lib/src/value/color.dart +++ b/lib/src/value/color.dart @@ -4,15 +4,18 @@ import 'dart:math' as math; +import 'package:meta/meta.dart'; import 'package:source_span/source_span.dart'; import '../exception.dart'; import '../util/number.dart'; import '../value.dart'; import '../visitor/interface/value.dart'; -import 'external/value.dart' as ext; -class SassColor extends Value implements ext.SassColor { +/// A SassScript color. +@sealed +class SassColor extends Value { + /// This color's red channel, between `0` and `255`. int get red { if (_red == null) _hslToRgb(); return _red!; @@ -20,6 +23,7 @@ class SassColor extends Value implements ext.SassColor { int? _red; + /// This color's green channel, between `0` and `255`. int get green { if (_green == null) _hslToRgb(); return _green!; @@ -27,6 +31,7 @@ class SassColor extends Value implements ext.SassColor { int? _green; + /// This color's blue channel, between `0` and `255`. int get blue { if (_blue == null) _hslToRgb(); return _blue!; @@ -34,6 +39,7 @@ class SassColor extends Value implements ext.SassColor { int? _blue; + /// This color's hue, between `0` and `360`. num get hue { if (_hue == null) _rgbToHsl(); return _hue!; @@ -41,6 +47,7 @@ class SassColor extends Value implements ext.SassColor { num? _hue; + /// This color's saturation, a percentage between `0` and `100`. num get saturation { if (_saturation == null) _rgbToHsl(); return _saturation!; @@ -48,6 +55,7 @@ class SassColor extends Value implements ext.SassColor { num? _saturation; + /// This color's lightness, a percentage between `0` and `100`. num get lightness { if (_lightness == null) _rgbToHsl(); return _lightness!; @@ -55,6 +63,7 @@ class SassColor extends Value implements ext.SassColor { num? _lightness; + /// This color's whiteness, a percentage between `0` and `100`. num get whiteness { // Because HWB is (currently) used much less frequently than HSL or RGB, we // don't cache its values because we expect the memory overhead of doing so @@ -62,6 +71,7 @@ class SassColor extends Value implements ext.SassColor { return math.min(math.min(red, green), blue) / 255 * 100; } + /// This color's blackness, a percentage between `0` and `100`. num get blackness { // Because HWB is (currently) used much less frequently than HSL or RGB, we // don't cache its values because we expect the memory overhead of doing so @@ -69,17 +79,28 @@ class SassColor extends Value implements ext.SassColor { return 100 - math.max(math.max(red, green), blue) / 255 * 100; } + /// This color's alpha channel, between `0` and `1`. final num alpha; /// The original string representation of this color, or `null` if one is /// unavailable. + /// + /// @nodoc + @internal String? get original => originalSpan?.text; /// The span tracking the location in which this color was originally defined. /// /// This is tracked as a span to avoid extra substring allocations. + /// + /// @nodoc + @internal final FileSpan? originalSpan; + /// Creates an RGB color. + /// + /// Throws a [RangeError] if [red], [green], and [blue] aren't between `0` and + /// `255`, or if [alpha] isn't between `0` and `1`. SassColor.rgb(this._red, this._green, this._blue, [num? alpha, this.originalSpan]) : alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha") { @@ -88,6 +109,10 @@ class SassColor extends Value implements ext.SassColor { RangeError.checkValueInInterval(blue, 0, 255, "blue"); } + /// Creates an HSL color. + /// + /// Throws a [RangeError] if [saturation] or [lightness] aren't between `0` + /// and `100`, or if [alpha] isn't between `0` and `1`. SassColor.hsl(num hue, num saturation, num lightness, [num? alpha]) : _hue = hue % 360, _saturation = fuzzyAssertRange(saturation, 0, 100, "saturation"), @@ -95,6 +120,10 @@ class SassColor extends Value implements ext.SassColor { alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha"), originalSpan = null; + /// Creates an HWB color. + /// + /// Throws a [RangeError] if [whiteness] or [blackness] aren't between `0` and + /// `100`, or if [alpha] isn't between `0` and `1`. factory SassColor.hwb(num hue, num whiteness, num blackness, [num? alpha]) { // From https://www.w3.org/TR/css-color-4/#hwb-to-rgb var scaledHue = hue % 360 / 360; @@ -127,36 +156,48 @@ class SassColor extends Value implements ext.SassColor { this._lightness, this.alpha) : originalSpan = null; + /// @nodoc + @internal T accept(ValueVisitor visitor) => visitor.visitColor(this); SassColor assertColor([String? name]) => this; + /// Changes one or more of this color's RGB channels and returns the result. SassColor changeRgb({int? red, int? green, int? blue, num? alpha}) => SassColor.rgb(red ?? this.red, green ?? this.green, blue ?? this.blue, alpha ?? this.alpha); + /// Changes one or more of this color's HSL channels and returns the result. SassColor changeHsl( {num? hue, num? saturation, num? lightness, num? alpha}) => SassColor.hsl(hue ?? this.hue, saturation ?? this.saturation, lightness ?? this.lightness, alpha ?? this.alpha); + /// Changes one or more of this color's HWB channels and returns the result. SassColor changeHwb({num? hue, num? whiteness, num? blackness, num? alpha}) => SassColor.hwb(hue ?? this.hue, whiteness ?? this.whiteness, blackness ?? this.blackness, alpha ?? this.alpha); + /// Returns a new copy of this color with the alpha channel set to [alpha]. SassColor changeAlpha(num alpha) => SassColor._(_red, _green, _blue, _hue, _saturation, _lightness, fuzzyAssertRange(alpha, 0, 1, "alpha")); + /// @nodoc + @internal Value plus(Value other) { if (other is! SassNumber && other is! SassColor) return super.plus(other); throw SassScriptException('Undefined operation "$this + $other".'); } + /// @nodoc + @internal Value minus(Value other) { if (other is! SassNumber && other is! SassColor) return super.minus(other); throw SassScriptException('Undefined operation "$this - $other".'); } + /// @nodoc + @internal Value dividedBy(Value other) { if (other is! SassNumber && other is! SassColor) { return super.dividedBy(other); @@ -164,9 +205,6 @@ class SassColor extends Value implements ext.SassColor { throw SassScriptException('Undefined operation "$this / $other".'); } - Value modulo(Value other) => - throw SassScriptException('Undefined operation "$this % $other".'); - bool operator ==(Object other) => other is SassColor && other.red == red && @@ -248,6 +286,9 @@ class SassColor extends Value implements ext.SassColor { /// Returns an `rgb()` or `rgba()` function call that will evaluate to this /// color. + /// + /// @nodoc + @internal String toStringAsRgb() { var isOpaque = fuzzyEquals(alpha, 1); var buffer = StringBuffer(isOpaque ? "rgb" : "rgba") diff --git a/lib/src/value/external/argument_list.dart b/lib/src/value/external/argument_list.dart deleted file mode 100644 index 088110e51..000000000 --- a/lib/src/value/external/argument_list.dart +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import '../../value.dart' show ListSeparator; -import 'value.dart'; - -/// A SassScript argument list. -/// -/// An argument list comes from a rest argument. It's distinct from a normal -/// [SassList] in that it may contain a keyword map as well as the positional -/// arguments. -@sealed -abstract class SassArgumentList extends SassList { - /// The keyword arguments attached to this argument list. - /// - /// The argument names don't include `$`. - Map get keywords; - - factory SassArgumentList(Iterable contents, - Map keywords, ListSeparator separator) => - internal.SassArgumentList(contents.cast(), keywords.cast(), separator); -} diff --git a/lib/src/value/external/boolean.dart b/lib/src/value/external/boolean.dart deleted file mode 100644 index f983a30d5..000000000 --- a/lib/src/value/external/boolean.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import 'value.dart'; - -/// The SassScript `true` value. -SassBoolean get sassTrue => internal.sassTrue; - -/// The SassScript `false` value. -SassBoolean get sassFalse => internal.sassFalse; - -/// A SassScript boolean value. -@sealed -abstract class SassBoolean extends Value { - /// Whether this value is `true` or `false`. - bool get value; - - /// Returns a [SassBoolean] corresponding to [value]. - /// - /// This just returns [sassTrue] or [sassFalse]; it doesn't allocate a new - /// value. - factory SassBoolean(bool value) = internal.SassBoolean; -} diff --git a/lib/src/value/external/color.dart b/lib/src/value/external/color.dart deleted file mode 100644 index cf961bbb2..000000000 --- a/lib/src/value/external/color.dart +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import 'value.dart'; - -/// A SassScript color. -@sealed -abstract class SassColor extends Value { - /// This color's red channel, between `0` and `255`. - int get red; - - /// This color's green channel, between `0` and `255`. - int get green; - - /// This color's blue channel, between `0` and `255`. - int get blue; - - /// This color's hue, between `0` and `360`. - num get hue; - - /// This color's saturation, a percentage between `0` and `100`. - num get saturation; - - /// This color's lightness, a percentage between `0` and `100`. - num get lightness; - - /// This color's whiteness, a percentage between `0` and `100`. - num get whiteness; - - /// This color's blackness, a percentage between `0` and `100`. - num get blackness; - - /// This color's alpha channel, between `0` and `1`. - num get alpha; - - /// Creates an RGB color. - /// - /// Throws a [RangeError] if [red], [green], and [blue] aren't between `0` and - /// `255`, or if [alpha] isn't between `0` and `1`. - factory SassColor.rgb(int red, int green, int blue, [num? alpha]) = - internal.SassColor.rgb; - - /// Creates an HSL color. - /// - /// Throws a [RangeError] if [saturation] or [lightness] aren't between `0` - /// and `100`, or if [alpha] isn't between `0` and `1`. - factory SassColor.hsl(num hue, num saturation, num lightness, [num? alpha]) = - internal.SassColor.hsl; - - /// Creates an HWB color. - /// - /// Throws a [RangeError] if [whiteness] or [blackness] aren't between `0` and - /// `100`, or if [alpha] isn't between `0` and `1`. - factory SassColor.hwb(num hue, num whiteness, num blackness, [num? alpha]) = - internal.SassColor.hwb; - - /// Changes one or more of this color's RGB channels and returns the result. - SassColor changeRgb({int? red, int? green, int? blue, num? alpha}); - - /// Changes one or more of this color's HSL channels and returns the result. - SassColor changeHsl({num? hue, num? saturation, num? lightness, num? alpha}); - - /// Changes one or more of this color's HWB channels and returns the result. - SassColor changeHwb({num? hue, num? whiteness, num? blackness, num? alpha}); - - /// Returns a new copy of this color with the alpha channel set to [alpha]. - SassColor changeAlpha(num alpha); -} diff --git a/lib/src/value/external/function.dart b/lib/src/value/external/function.dart deleted file mode 100644 index a0ce21793..000000000 --- a/lib/src/value/external/function.dart +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../callable.dart'; -import '../../value.dart' as internal; -import 'value.dart'; - -/// A SassScript function reference. -/// -/// A function reference captures a function from the local environment so that -/// it may be passed between modules. -@sealed -abstract class SassFunction extends Value { - /// The callable that this function invokes. - /// - /// Note that this is typed as an [AsyncCallable] so that it will work with - /// both synchronous and asynchronous evaluate visitors, but in practice the - /// synchronous evaluate visitor will crash if this isn't a [Callable]. - AsyncCallable get callable; - - factory SassFunction(AsyncCallable callable) = internal.SassFunction; -} diff --git a/lib/src/value/external/list.dart b/lib/src/value/external/list.dart deleted file mode 100644 index d97124c2f..000000000 --- a/lib/src/value/external/list.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import '../../value.dart' show ListSeparator; -import 'value.dart'; - -/// A SassScript list. -@sealed -abstract class SassList extends Value { - ListSeparator get separator; - - bool get hasBrackets; - - /// Returns an empty list with the given [separator] and [brackets]. - /// - /// The [separator] defaults to [ListSeparator.undecided], and [brackets] defaults to `false`. - const factory SassList.empty({ListSeparator? separator, bool brackets}) = - internal.SassList.empty; - - /// Returns an empty list with the given [separator] and [brackets]. - factory SassList(Iterable contents, ListSeparator separator, - {bool brackets = false}) => - internal.SassList(contents.cast(), separator, brackets: brackets); -} diff --git a/lib/src/value/external/map.dart b/lib/src/value/external/map.dart deleted file mode 100644 index b8f91e0da..000000000 --- a/lib/src/value/external/map.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import 'value.dart'; - -/// A SassScript map. -@sealed -abstract class SassMap extends Value { - // TODO(nweiz): Use persistent data structures rather than copying here. We - // need to preserve the order, which can be done by tracking an RRB vector of - // keys along with the hash-mapped array trie representing the map. - // - // We may also want to fall back to a plain unmodifiable Map for small maps - // (<32 items?). - /// The contents of the map. - Map get contents; - - /// Returns an empty map. - const factory SassMap.empty() = internal.SassMap.empty; - - factory SassMap(Map contents) => - internal.SassMap(contents.cast()); -} diff --git a/lib/src/value/external/number.dart b/lib/src/value/external/number.dart deleted file mode 100644 index 3db7c90a5..000000000 --- a/lib/src/value/external/number.dart +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2016 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import 'value.dart'; - -/// A SassScript number. -/// -/// Numbers can have units. Although there's no literal syntax for it, numbers -/// support scientific-style numerator and denominator units (for example, -/// `miles/hour`). These are expected to be resolved before being emitted to -/// CSS. -@sealed -abstract class SassNumber extends Value { - /// The number of distinct digits that are emitted when converting a number to - /// CSS. - static const precision = 10; - - /// The value of this number. - /// - /// Note that due to details of floating-point arithmetic, this may be a - /// [double] even if [this] represents an int from Sass's perspective. Use - /// [isInt] to determine whether this is an integer, [asInt] to get its - /// integer value, or [assertInt] to do both at once. - num get value; - - /// This number's numerator units. - List get numeratorUnits; - - /// This number's denominator units. - List get denominatorUnits; - - /// Whether [this] has any units. - /// - /// If a function expects a number to have no units, it should use - /// [assertNoUnits]. If it expects the number to have a particular unit, it - /// should use [assertUnit]. - bool get hasUnits; - - /// Whether [this] is an integer, according to [fuzzyEquals]. - /// - /// The [int] value can be accessed using [asInt] or [assertInt]. Note that - /// this may return `false` for very large doubles even though they may be - /// mathematically integers, because not all platforms have a valid - /// representation for integers that large. - bool get isInt; - - /// If [this] is an integer according to [isInt], returns [value] as an [int]. - /// - /// Otherwise, returns `null`. - int? get asInt; - - /// Creates a number, optionally with a single numerator unit. - /// - /// This matches the numbers that can be written as literals. - /// [SassNumber.withUnits] can be used to construct more complex units. - factory SassNumber(num value, [String? unit]) = internal.SassNumber; - - /// Creates a number with full [numeratorUnits] and [denominatorUnits]. - factory SassNumber.withUnits(num value, - {List? numeratorUnits, - List? denominatorUnits}) = internal.SassNumber.withUnits; - - /// Returns [value] as an [int], if it's an integer value according to - /// [isInt]. - /// - /// Throws a [SassScriptException] if [value] isn't an integer. If this came - /// from a function argument, [name] is the argument name (without the `$`). - /// It's used for error reporting. - int assertInt([String? name]); - - /// If [value] is between [min] and [max], returns it. - /// - /// If [value] is [fuzzyEquals] to [min] or [max], it's clamped to the - /// appropriate value. Otherwise, this throws a [SassScriptException]. If this - /// came from a function argument, [name] is the argument name (without the - /// `$`). It's used for error reporting. - num valueInRange(num min, num max, [String? name]); - - /// Returns whether [this] has [unit] as its only unit (and as a numerator). - bool hasUnit(String unit); - - /// Returns whether [this] can be coerced to the given [unit]. - /// - /// This always returns `true` for a unitless number. - bool compatibleWithUnit(String unit); - - /// Throws a [SassScriptException] unless [this] has [unit] as its only unit - /// (and as a numerator). - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - void assertUnit(String unit, [String? name]); - - /// Throws a [SassScriptException] unless [this] has no units. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - void assertNoUnits([String? name]); - - /// Returns a copy of this number, converted to the same units as [other]. - /// - /// Unlike [convertToMatch], this does *not* throw an error if this number is - /// unitless and [other] is not, or vice versa. Instead, it treats all - /// unitless numbers as convertible to and from all units without changing the - /// value. - /// - /// Note that [coerceValueToMatch] is generally more efficient if the value is - /// going to be accessed directly. - /// - /// Throws a [SassScriptException] if this number's units aren't compatible - /// with [other]'s units. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`) and [otherName] is the argument name for [other]. These - /// are used for error reporting. - SassNumber coerceToMatch(SassNumber other, [String? name, String? otherName]); - - /// Returns [value], converted to the same units as [other]. - /// - /// Unlike [convertValueToMatch], this does *not* throw an error if this - /// number is unitless and [other] is not, or vice versa. Instead, it treats - /// all unitless numbers as convertible to and from all units without changing - /// the value. - /// - /// Throws a [SassScriptException] if this number's units aren't compatible - /// with [other]'s units. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`) and [otherName] is the argument name for [other]. These - /// are used for error reporting. - num coerceValueToMatch(SassNumber other, [String? name, String? otherName]); - - /// Returns a copy of this number, converted to the same units as [other]. - /// - /// Note that [convertValueToMatch] is generally more efficient if the value - /// is going to be accessed directly. - /// - /// Throws a [SassScriptException] if this number's units aren't compatible - /// with [other]'s units, or if either number is unitless but the other is - /// not. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`) and [otherName] is the argument name for [other]. These - /// are used for error reporting. - SassNumber convertToMatch(SassNumber other, - [String? name, String? otherName]); - - /// Returns [value], converted to the same units as [other]. - /// - /// Throws a [SassScriptException] if this number's units aren't compatible - /// with [other]'s units, or if either number is unitless but the other is - /// not. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`) and [otherName] is the argument name for [other]. These - /// are used for error reporting. - num convertValueToMatch(SassNumber other, [String? name, String? otherName]); - - /// Returns a copy of this number, converted to the units represented by - /// [newNumerators] and [newDenominators]. - /// - /// This does *not* throw an error if this number is unitless and - /// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead, - /// it treats all unitless numbers as convertible to and from all units - /// without changing the value. - /// - /// Note that [coerceValue] is generally more efficient if the value is going - /// to be accessed directly. - /// - /// Throws a [SassScriptException] if this number's units aren't compatible - /// with [newNumerators] and [newDenominators]. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassNumber coerce(List newNumerators, List newDenominators, - [String? name]); - - /// Returns [value], converted to the units represented by [newNumerators] and - /// [newDenominators]. - /// - /// This does *not* throw an error if this number is unitless and - /// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead, - /// it treats all unitless numbers as convertible to and from all units - /// without changing the value. - /// - /// Throws a [SassScriptException] if this number's units aren't compatible - /// with [newNumerators] and [newDenominators]. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - num coerceValue(List newNumerators, List newDenominators, - [String? name]); - - /// This has been renamed [coerceValue] for consistency with [coerceToMatch], - /// [coerceValueToMatch], [convertToMatch], and [convertValueToMatch]. - @deprecated - num valueInUnits(List newNumerators, List newDenominators, - [String? name]); - - /// A shorthand for [coerceValue] with only one numerator unit. - num coerceValueToUnit(String unit, [String? name]); -} diff --git a/lib/src/value/external/string.dart b/lib/src/value/external/string.dart deleted file mode 100644 index eec9de362..000000000 --- a/lib/src/value/external/string.dart +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import 'value.dart'; - -/// A SassScript string. -/// -/// Strings can either be quoted or unquoted. Unquoted strings are usually CSS -/// identifiers, but they may contain any text. -@sealed -abstract class SassString extends Value { - /// The contents of the string. - /// - /// For quoted strings, this is the semantic content—any escape sequences that - /// were been written in the source text are resolved to their Unicode values. - /// For unquoted strings, though, escape sequences are preserved as literal - /// backslashes. - /// - /// This difference allows us to distinguish between identifiers with escapes, - /// such as `url\u28 http://example.com\u29`, and unquoted strings that - /// contain characters that aren't valid in identifiers, such as - /// `url(http://example.com)`. Unfortunately, it also means that we don't - /// consider `foo` and `f\6F\6F` the same string. - String get text; - - /// Whether this string has quotes. - bool get hasQuotes; - - /// Sass's notion of the length of this string. - /// - /// Sass treats strings as a series of Unicode code points while Dart treats - /// them as a series of UTF-16 code units. For example, the character U+1F60A, - /// Smiling Face With Smiling Eyes, is a single Unicode code point but is - /// represented in UTF-16 as two code units (`0xD83D` and `0xDE0A`). So in - /// Dart, `"a😊b".length` returns `4`, whereas in Sass `str-length("a😊b")` - /// returns `3`. - /// - /// This returns the same value as `text.runes.length`, but it's more - /// efficient. - int get sassLength; - - /// Creates an empty string. - /// - /// The [quotes] argument defaults to `false`. - factory SassString.empty({bool quotes}) = internal.SassString.empty; - - /// Creates a string with the given [text]. - /// - /// The [quotes] argument defaults to `false`. - factory SassString(String text, {bool quotes}) = internal.SassString; - - /// Converts [sassIndex] into a Dart-style index into [text]. - /// - /// Sass indexes are one-based, while Dart indexes are zero-based. Sass - /// indexes may also be negative in order to index from the end of the string. - /// - /// In addition, Sass indices refer to Unicode code points while Dart string - /// indices refer to UTF-16 code units. For example, the character U+1F60A, - /// Smiling Face With Smiling Eyes, is a single Unicode code point but is - /// represented in UTF-16 as two code units (`0xD83D` and `0xDE0A`). So in - /// Dart, `"a😊b".codeUnitAt(1)` returns `0xD83D`, whereas in Sass - /// `str-slice("a😊b", 1, 1)` returns `"😊"`. - /// - /// This function converts Sass's code point indexes to Dart's code unit - /// indexes. This means it's O(n) in the length of [text]. See also - /// [sassIndexToRuneIndex], which is O(1) and returns an index into the - /// string's code points (accessible via `text.runes`). - /// - /// Throws a [SassScriptException] if [sassIndex] isn't a number, if that - /// number isn't an integer, or if that integer isn't a valid index for this - /// string. If [sassIndex] came from a function argument, [name] is the - /// argument name (without the `$`). It's used for error reporting. - int sassIndexToStringIndex(Value sassIndex, [String? name]); - - /// Converts [sassIndex] into a Dart-style index into [text]`.runes`. - /// - /// Sass indexes are one-based, while Dart indexes are zero-based. Sass - /// indexes may also be negative in order to index from the end of the string. - /// - /// See also [sassIndexToStringIndex], which an index into [text] directly. - /// - /// Throws a [SassScriptException] if [sassIndex] isn't a number, if that - /// number isn't an integer, or if that integer isn't a valid index for this - /// string. If [sassIndex] came from a function argument, [name] is the - /// argument name (without the `$`). It's used for error reporting. - int sassIndexToRuneIndex(Value sassIndex, [String? name]); -} diff --git a/lib/src/value/external/value.dart b/lib/src/value/external/value.dart deleted file mode 100644 index f6b19ec9f..000000000 --- a/lib/src/value/external/value.dart +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2018 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 'package:meta/meta.dart'; - -import '../../value.dart' as internal; -import '../../value.dart' show ListSeparator; -import 'boolean.dart'; -import 'color.dart'; -import 'function.dart'; -import 'map.dart'; -import 'number.dart'; -import 'string.dart'; - -export 'argument_list.dart'; -export 'boolean.dart'; -export 'color.dart'; -export 'function.dart'; -export 'list.dart'; -export 'map.dart'; -export 'number.dart'; -export 'string.dart'; - -/// The SassScript `null` value. -Value get sassNull => internal.sassNull; - -// TODO(nweiz): Just mark members as @internal when sdk#28066 is fixed. -// -// We separate out the externally-visible Value type and subtypes (in this -// directory) from the internally-visible types (in the parent directory) so -// that we can add members that are only accessible from within this package. - -/// A SassScript value. -/// -/// All SassScript values are unmodifiable. New values can be constructed using -/// subclass constructors like [new SassString]. Untyped values can be cast to -/// particular types using `assert*()` functions like [assertString], which -/// throw user-friendly error messages if they fail. -@sealed -abstract class Value { - /// Whether the value counts as `true` in an `@if` statement and other - /// contexts. - bool get isTruthy; - - /// The separator for this value as a list. - /// - /// All SassScript values can be used as lists. Maps count as lists of pairs, - /// and all other values count as single-value lists. - ListSeparator get separator; - - /// Whether this value as a list has brackets. - /// - /// All SassScript values can be used as lists. Maps count as lists of pairs, - /// and all other values count as single-value lists. - bool get hasBrackets; - - /// This value as a list. - /// - /// All SassScript values can be used as lists. Maps count as lists of pairs, - /// and all other values count as single-value lists. - List get asList; - - /// Returns Dart's `null` value if this is [sassNull], and returns [this] - /// otherwise. - Value? get realNull; - - /// Converts [sassIndex] into a Dart-style index into the list returned by - /// [asList]. - /// - /// Sass indexes are one-based, while Dart indexes are zero-based. Sass - /// indexes may also be negative in order to index from the end of the list. - /// - /// Throws a [SassScriptException] if [sassIndex] isn't a number, if that - /// number isn't an integer, or if that integer isn't a valid index for - /// [asList]. If [sassIndex] came from a function argument, [name] is the - /// argument name (without the `$`). It's used for error reporting. - int sassIndexToListIndex(Value sassIndex, [String? name]); - - /// Throws a [SassScriptException] if [this] isn't a boolean. - /// - /// Note that generally, functions should use [isTruthy] rather than requiring - /// a literal boolean. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassBoolean assertBoolean([String? name]); - - /// Throws a [SassScriptException] if [this] isn't a color. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassColor assertColor([String? name]); - - /// Throws a [SassScriptException] if [this] isn't a function reference. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassFunction assertFunction([String? name]); - - /// Throws a [SassScriptException] if [this] isn't a map. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassMap assertMap([String? name]); - - /// Returns [this] as a [SassMap] if it is one (including empty lists, which - /// count as empty maps) or returns `null` if it's not. - SassMap? tryMap(); - - /// Throws a [SassScriptException] if [this] isn't a number. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassNumber assertNumber([String? name]); - - /// Throws a [SassScriptException] if [this] isn't a string. - /// - /// If this came from a function argument, [name] is the argument name - /// (without the `$`). It's used for error reporting. - SassString assertString([String? name]); - - /// Returns a valid CSS representation of [this]. - /// - /// Throws a [SassScriptException] if [this] can't be represented in plain - /// CSS. Use [toString] instead to get a string representation even if this - /// isn't valid CSS. - String toCssString(); - - /// Returns a string representation of [this]. - /// - /// Note that this is equivalent to calling `inspect()` on the value, and thus - /// won't reflect the user's output settings. [toCssString] should be used - /// instead to convert [this] to CSS. - String toString(); -} diff --git a/lib/src/value/function.dart b/lib/src/value/function.dart index 3f86a2620..95bb7a612 100644 --- a/lib/src/value/function.dart +++ b/lib/src/value/function.dart @@ -2,16 +2,29 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../callable.dart'; import '../visitor/interface/value.dart'; import '../value.dart'; -import 'external/value.dart' as internal; -class SassFunction extends Value implements internal.SassFunction { +/// A SassScript function reference. +/// +/// A function reference captures a function from the local environment so that +/// it may be passed between modules. +@sealed +class SassFunction extends Value { + /// The callable that this function invokes. + /// + /// Note that this is typed as an [AsyncCallable] so that it will work with + /// both synchronous and asynchronous evaluate visitors, but in practice the + /// synchronous evaluate visitor will crash if this isn't a [Callable]. final AsyncCallable callable; SassFunction(this.callable); + /// @nodoc + @internal T accept(ValueVisitor visitor) => visitor.visitFunction(this); SassFunction assertFunction([String? name]) => this; diff --git a/lib/src/value/list.dart b/lib/src/value/list.dart index 7ed8124e2..c4784792d 100644 --- a/lib/src/value/list.dart +++ b/lib/src/value/list.dart @@ -7,9 +7,10 @@ import 'package:meta/meta.dart'; import '../utils.dart'; import '../visitor/interface/value.dart'; import '../value.dart'; -import 'external/value.dart' as ext; -class SassList extends Value implements ext.SassList { +/// A SassScript list. +@sealed +class SassList extends Value { // TODO(nweiz): Use persistent data structures rather than copying here. An // RRB vector should fit our use-cases well. // @@ -21,17 +22,25 @@ class SassList extends Value implements ext.SassList { final bool hasBrackets; + /// @nodoc + @internal bool get isBlank => asList.every((element) => element.isBlank); List get asList => _contents; + /// @nodoc + @internal int get lengthAsList => asList.length; + /// Returns an empty list with the given [separator] and [brackets]. + /// + /// The [separator] defaults to [ListSeparator.undecided], and [brackets] defaults to `false`. const SassList.empty({ListSeparator? separator, bool brackets = false}) : _contents = const [], separator = separator ?? ListSeparator.undecided, hasBrackets = brackets; + /// Returns an empty list with the given [separator] and [brackets]. SassList(Iterable contents, this.separator, {bool brackets = false}) : _contents = List.unmodifiable(contents), hasBrackets = brackets { @@ -41,6 +50,8 @@ class SassList extends Value implements ext.SassList { } } + /// @nodoc + @internal T accept(ValueVisitor visitor) => visitor.visitList(this); SassMap assertMap([String? name]) => diff --git a/lib/src/value/map.dart b/lib/src/value/map.dart index 1609fb2ef..989983d72 100644 --- a/lib/src/value/map.dart +++ b/lib/src/value/map.dart @@ -2,12 +2,22 @@ // 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/value.dart'; import '../value.dart'; import '../utils.dart'; -import 'external/value.dart' as ext; -class SassMap extends Value implements ext.SassMap { +/// A SassScript map. +@sealed +class SassMap extends Value { + // TODO(nweiz): Use persistent data structures rather than copying here. We + // need to preserve the order, which can be done by tracking an RRB vector of + // keys along with the hash-mapped array trie representing the map. + // + // We may also want to fall back to a plain unmodifiable Map for small maps + // (<32 items?). + /// The contents of the map. final Map contents; ListSeparator get separator => @@ -21,6 +31,8 @@ class SassMap extends Value implements ext.SassMap { return result; } + /// @nodoc + @internal int get lengthAsList => contents.length; /// Returns an empty map. @@ -28,6 +40,8 @@ class SassMap extends Value implements ext.SassMap { SassMap(Map contents) : contents = Map.unmodifiable(contents); + /// @nodoc + @internal T accept(ValueVisitor visitor) => visitor.visitMap(this); SassMap assertMap([String? name]) => this; diff --git a/lib/src/value/null.dart b/lib/src/value/null.dart index 183ac43d1..a3b730a2b 100644 --- a/lib/src/value/null.dart +++ b/lib/src/value/null.dart @@ -6,21 +6,21 @@ import '../visitor/interface/value.dart'; import '../value.dart'; /// The SassScript `null` value. -const sassNull = SassNull._(); +const Value sassNull = _SassNull(); /// A SassScript null value. /// /// This can't be constructed directly; it can only be accessed via [sassNull]. -class SassNull extends Value { +class _SassNull extends Value { bool get isTruthy => false; bool get isBlank => true; Value? get realNull => null; - const SassNull._(); + const _SassNull(); - T accept(ValueVisitor visitor) => visitor.visitNull(this); + T accept(ValueVisitor visitor) => visitor.visitNull(); Value unaryNot() => sassTrue; } diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 92a67a8f3..c539e5a4a 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -12,7 +12,6 @@ import '../util/number.dart'; import '../utils.dart'; import '../value.dart'; import '../visitor/interface/value.dart'; -import 'external/value.dart' as ext; import 'number/complex.dart'; import 'number/single_unit.dart'; import 'number/unitless.dart'; @@ -160,27 +159,69 @@ final _typesByUnit = { for (var unit in entry.value) unit: entry.key }; -abstract class SassNumber extends Value implements ext.SassNumber { - static const precision = ext.SassNumber.precision; - +/// A SassScript number. +/// +/// Numbers can have units. Although there's no literal syntax for it, numbers +/// support scientific-style numerator and denominator units (for example, +/// `miles/hour`). These are expected to be resolved before being emitted to +/// CSS. +@sealed +abstract class SassNumber extends Value { + /// The number of distinct digits that are emitted when converting a number to + /// CSS. + static const precision = 10; + + /// The value of this number. + /// + /// Note that due to details of floating-point arithmetic, this may be a + /// [double] even if [this] represents an int from Sass's perspective. Use + /// [isInt] to determine whether this is an integer, [asInt] to get its + /// integer value, or [assertInt] to do both at once. final num value; + /// This number's numerator units. + List get numeratorUnits; + + /// This number's denominator units. + List get denominatorUnits; + + /// Whether [this] has any units. + /// + /// If a function expects a number to have no units, it should use + /// [assertNoUnits]. If it expects the number to have a particular unit, it + /// should use [assertUnit]. + bool get hasUnits; + /// The representation of this number as two slash-separated numbers, if it /// has one. final Tuple2? asSlash; + /// Whether [this] is an integer, according to [fuzzyEquals]. + /// + /// The [int] value can be accessed using [asInt] or [assertInt]. Note that + /// this may return `false` for very large doubles even though they may be + /// mathematically integers, because not all platforms have a valid + /// representation for integers that large. bool get isInt => fuzzyIsInt(value); + /// If [this] is an integer according to [isInt], returns [value] as an [int]. + /// + /// Otherwise, returns `null`. int? get asInt => fuzzyAsInt(value); /// Returns a human readable string representation of this number's units. String get unitString => hasUnits ? _unitString(numeratorUnits, denominatorUnits) : ''; + /// Creates a number, optionally with a single numerator unit. + /// + /// This matches the numbers that can be written as literals. + /// [SassNumber.withUnits] can be used to construct more complex units. factory SassNumber(num value, [String? unit]) => unit == null ? UnitlessSassNumber(value) : SingleUnitSassNumber(value, unit); + /// Creates a number with full [numeratorUnits] and [denominatorUnits]. factory SassNumber.withUnits(num value, {List? numeratorUnits, List? denominatorUnits}) { if (denominatorUnits == null || denominatorUnits.isEmpty) { @@ -202,6 +243,7 @@ abstract class SassNumber extends Value implements ext.SassNumber { } } + /// @nodoc @protected SassNumber.protected(this.value, this.asSlash); @@ -209,6 +251,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// Returns a number with the same units as [this] but with [value] as its /// value. + /// + /// @nodoc @protected SassNumber withValue(num value); @@ -221,12 +265,24 @@ abstract class SassNumber extends Value implements ext.SassNumber { SassNumber assertNumber([String? name]) => this; + /// Returns [value] as an [int], if it's an integer value according to + /// [isInt]. + /// + /// Throws a [SassScriptException] if [value] isn't an integer. If this came + /// from a function argument, [name] is the argument name (without the `$`). + /// It's used for error reporting. int assertInt([String? name]) { var integer = fuzzyAsInt(value); if (integer != null) return integer; throw _exception("$this is not an int.", name); } + /// If [value] is between [min] and [max], returns it. + /// + /// If [value] is [fuzzyEquals] to [min] or [max], it's clamped to the + /// appropriate value. Otherwise, this throws a [SassScriptException]. If this + /// came from a function argument, [name] is the argument name (without the + /// `$`). It's used for error reporting. num valueInRange(num min, num max, [String? name]) { var result = fuzzyCheckRange(value, min, max); if (result != null) return result; @@ -235,34 +291,100 @@ abstract class SassNumber extends Value implements ext.SassNumber { name); } + /// Returns whether [this] has [unit] as its only unit (and as a numerator). + bool hasUnit(String unit); + + /// Returns whether [this] can be coerced to the given [unit]. + /// + /// This always returns `true` for a unitless number. + bool compatibleWithUnit(String unit); + + /// Throws a [SassScriptException] unless [this] has [unit] as its only unit + /// (and as a numerator). + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. void assertUnit(String unit, [String? name]) { if (hasUnit(unit)) return; throw _exception('Expected $this to have unit "$unit".', name); } + /// Throws a [SassScriptException] unless [this] has no units. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. void assertNoUnits([String? name]) { if (!hasUnits) return; throw _exception('Expected $this to have no units.', name); } - SassNumber coerceToMatch(ext.SassNumber other, + /// Returns a copy of this number, converted to the same units as [other]. + /// + /// Unlike [convertToMatch], this does *not* throw an error if this number is + /// unitless and [other] is not, or vice versa. Instead, it treats all + /// unitless numbers as convertible to and from all units without changing the + /// value. + /// + /// Note that [coerceValueToMatch] is generally more efficient if the value is + /// going to be accessed directly. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + SassNumber coerceToMatch(SassNumber other, [String? name, String? otherName]) => SassNumber.withUnits(coerceValueToMatch(other, name, otherName), numeratorUnits: other.numeratorUnits, denominatorUnits: other.denominatorUnits); - num coerceValueToMatch(ext.SassNumber other, - [String? name, String? otherName]) => + /// Returns [value], converted to the same units as [other]. + /// + /// Unlike [convertValueToMatch], this does *not* throw an error if this + /// number is unitless and [other] is not, or vice versa. Instead, it treats + /// all unitless numbers as convertible to and from all units without changing + /// the value. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + num coerceValueToMatch(SassNumber other, [String? name, String? otherName]) => _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, coerceUnitless: true, name: name, other: other, otherName: otherName); - SassNumber convertToMatch(ext.SassNumber other, + /// Returns a copy of this number, converted to the same units as [other]. + /// + /// Note that [convertValueToMatch] is generally more efficient if the value + /// is going to be accessed directly. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units, or if either number is unitless but the other is + /// not. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + SassNumber convertToMatch(SassNumber other, [String? name, String? otherName]) => SassNumber.withUnits(convertValueToMatch(other, name, otherName), numeratorUnits: other.numeratorUnits, denominatorUnits: other.denominatorUnits); - num convertValueToMatch(ext.SassNumber other, + /// Returns [value], converted to the same units as [other]. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [other]'s units, or if either number is unitless but the other is + /// not. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`) and [otherName] is the argument name for [other]. These + /// are used for error reporting. + num convertValueToMatch(SassNumber other, [String? name, String? otherName]) => _coerceOrConvertValue(other.numeratorUnits, other.denominatorUnits, coerceUnitless: false, @@ -270,19 +392,51 @@ abstract class SassNumber extends Value implements ext.SassNumber { other: other, otherName: otherName); + /// Returns a copy of this number, converted to the units represented by + /// [newNumerators] and [newDenominators]. + /// + /// This does *not* throw an error if this number is unitless and + /// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead, + /// it treats all unitless numbers as convertible to and from all units + /// without changing the value. + /// + /// Note that [coerceValue] is generally more efficient if the value is going + /// to be accessed directly. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [newNumerators] and [newDenominators]. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. SassNumber coerce(List newNumerators, List newDenominators, [String? name]) => SassNumber.withUnits(coerceValue(newNumerators, newDenominators, name), numeratorUnits: newNumerators, denominatorUnits: newDenominators); + /// Returns [value], converted to the units represented by [newNumerators] and + /// [newDenominators]. + /// + /// This does *not* throw an error if this number is unitless and + /// [newNumerators]/[newDenominators] are not empty, or vice versa. Instead, + /// it treats all unitless numbers as convertible to and from all units + /// without changing the value. + /// + /// Throws a [SassScriptException] if this number's units aren't compatible + /// with [newNumerators] and [newDenominators]. + /// + /// If this came from a function argument, [name] is the argument name + /// (without the `$`). It's used for error reporting. num coerceValue(List newNumerators, List newDenominators, [String? name]) => _coerceOrConvertValue(newNumerators, newDenominators, coerceUnitless: true, name: name); + /// A shorthand for [coerceValue] with only one numerator unit. num coerceValueToUnit(String unit, [String? name]) => coerceValue([unit], [], name); + /// This has been renamed [coerceValue] for consistency with [coerceToMatch], + /// [coerceValueToMatch], [convertToMatch], and [convertValueToMatch]. @deprecated num valueInUnits(List newNumerators, List newDenominators, [String? name]) => @@ -302,7 +456,7 @@ abstract class SassNumber extends Value implements ext.SassNumber { List newNumerators, List newDenominators, {required bool coerceUnitless, String? name, - ext.SassNumber? other, + SassNumber? other, String? otherName}) { assert( other == null || @@ -384,6 +538,9 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// /// Two numbers can be compared if they have compatible units, or if either /// number has no units. + /// + /// @nodoc + @internal bool isComparableTo(SassNumber other) { if (!hasUnits || !other.hasUnits) return true; try { @@ -394,6 +551,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { } } + /// @nodoc + @internal SassBoolean greaterThan(Value other) { if (other is SassNumber) { return SassBoolean(_coerceUnits(other, fuzzyGreaterThan)); @@ -401,6 +560,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this > $other".'); } + /// @nodoc + @internal SassBoolean greaterThanOrEquals(Value other) { if (other is SassNumber) { return SassBoolean(_coerceUnits(other, fuzzyGreaterThanOrEquals)); @@ -408,6 +569,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this >= $other".'); } + /// @nodoc + @internal SassBoolean lessThan(Value other) { if (other is SassNumber) { return SassBoolean(_coerceUnits(other, fuzzyLessThan)); @@ -415,6 +578,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this < $other".'); } + /// @nodoc + @internal SassBoolean lessThanOrEquals(Value other) { if (other is SassNumber) { return SassBoolean(_coerceUnits(other, fuzzyLessThanOrEquals)); @@ -422,6 +587,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this <= $other".'); } + /// @nodoc + @internal Value modulo(Value other) { if (other is SassNumber) { return withValue(_coerceUnits(other, moduloLikeSass)); @@ -431,6 +598,9 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// Return [num1] modulo [num2], using Sass's modulo semantics, which it /// inherited from Ruby and which differ from Dart's. + /// + /// @nodoc + @internal num moduloLikeSass(num num1, num num2) { if (num2 > 0) return num1 % num2; if (num2 == 0) return double.nan; @@ -441,6 +611,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { return result == 0 ? 0 : result + num2; } + /// @nodoc + @internal Value plus(Value other) { if (other is SassNumber) { return withValue(_coerceUnits(other, (num1, num2) => num1 + num2)); @@ -449,6 +621,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this + $other".'); } + /// @nodoc + @internal Value minus(Value other) { if (other is SassNumber) { return withValue(_coerceUnits(other, (num1, num2) => num1 - num2)); @@ -457,6 +631,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this - $other".'); } + /// @nodoc + @internal Value times(Value other) { if (other is SassNumber) { if (!other.hasUnits) return withValue(value * other.value); @@ -466,6 +642,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { throw SassScriptException('Undefined operation "$this * $other".'); } + /// @nodoc + @internal Value dividedBy(Value other) { if (other is SassNumber) { if (!other.hasUnits) return withValue(value / other.value); @@ -475,12 +653,16 @@ abstract class SassNumber extends Value implements ext.SassNumber { return super.dividedBy(other); } + /// @nodoc + @internal Value unaryPlus() => this; /// Converts [other]'s value to be compatible with this number's, and calls /// [operation] with the resulting numbers. /// /// Throws a [SassScriptException] if the two numbers' units are incompatible. + /// + /// @nodoc @protected T _coerceUnits(SassNumber other, T operation(num num1, num num2)) { try { @@ -497,6 +679,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// Returns a new number that's equivalent to `value /// this.numeratorUnits/this.denominatorUnits * 1 /// otherNumerators/otherDenominators`. + /// + /// @nodoc @protected SassNumber multiplyUnits( num value, List otherNumerators, List otherDenominators) { @@ -565,6 +749,8 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// Returns the number of [unit1]s per [unit2]. /// /// Equivalently, `1unit2 * conversionFactor(unit1, unit2) = 1unit1`. + /// + /// @nodoc @protected num? conversionFactor(String unit1, String unit2) { if (unit1 == unit2) return 1; @@ -646,6 +832,9 @@ abstract class SassNumber extends Value implements ext.SassNumber { /// /// That is, if `X unit1 == Y unit2`, `X * canonicalMultiplierForUnit(unit1) /// == Y * canonicalMultiplierForUnit(unit2)`. + /// + /// @nodoc + @protected num canonicalMultiplierForUnit(String unit) { var innerMap = _conversions[unit]; return innerMap == null ? 1 : 1 / innerMap.values.first; diff --git a/lib/src/value/number/single_unit.dart b/lib/src/value/number/single_unit.dart index e58517627..581986c52 100644 --- a/lib/src/value/number/single_unit.dart +++ b/lib/src/value/number/single_unit.dart @@ -11,7 +11,6 @@ import '../../util/number.dart'; import '../../utils.dart'; import '../../util/nullable.dart'; import '../../value.dart'; -import '../external/value.dart' as ext; import '../number.dart'; /// A specialized subclass of [SassNumber] for numbers that have exactly one @@ -39,21 +38,20 @@ class SingleUnitSassNumber extends SassNumber { bool compatibleWithUnit(String unit) => conversionFactor(_unit, unit) != null; - SassNumber coerceToMatch(ext.SassNumber other, + SassNumber coerceToMatch(SassNumber other, [String? name, String? otherName]) => convertToMatch(other, name, otherName); - num coerceValueToMatch(ext.SassNumber other, - [String? name, String? otherName]) => + num coerceValueToMatch(SassNumber other, [String? name, String? otherName]) => convertValueToMatch(other, name, otherName); - SassNumber convertToMatch(ext.SassNumber other, + SassNumber convertToMatch(SassNumber other, [String? name, String? otherName]) => (other is SingleUnitSassNumber ? _coerceToUnit(other._unit) : null) ?? // Call this to generate a consistent error message. super.convertToMatch(other, name, otherName); - num convertValueToMatch(ext.SassNumber other, + num convertValueToMatch(SassNumber other, [String? name, String? otherName]) => (other is SingleUnitSassNumber ? _coerceValueToUnit(other._unit) diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index b826a69de..65c8ed7f8 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -7,7 +7,6 @@ import 'package:tuple/tuple.dart'; import '../../util/number.dart'; import '../../value.dart'; -import '../external/value.dart' as ext; import '../number.dart'; /// A specialized subclass of [SassNumber] for numbers that have no units. @@ -31,22 +30,21 @@ class UnitlessSassNumber extends SassNumber { bool compatibleWithUnit(String unit) => true; - SassNumber coerceToMatch(ext.SassNumber other, + SassNumber coerceToMatch(SassNumber other, [String? name, String? otherName]) => - (other as SassNumber).withValue(value); + other.withValue(value); - num coerceValueToMatch(ext.SassNumber other, - [String? name, String? otherName]) => + num coerceValueToMatch(SassNumber other, [String? name, String? otherName]) => value; - SassNumber convertToMatch(ext.SassNumber other, + SassNumber convertToMatch(SassNumber other, [String? name, String? otherName]) => other.hasUnits // Call this to generate a consistent error message. ? super.convertToMatch(other, name, otherName) : this; - num convertValueToMatch(ext.SassNumber other, + num convertValueToMatch(SassNumber other, [String? name, String? otherName]) => other.hasUnits // Call this to generate a consistent error message. diff --git a/lib/src/value/string.dart b/lib/src/value/string.dart index 1a5d4ba85..616fe94c6 100644 --- a/lib/src/value/string.dart +++ b/lib/src/value/string.dart @@ -3,13 +3,13 @@ // https://opensource.org/licenses/MIT. import 'package:charcode/charcode.dart'; +import 'package:meta/meta.dart'; import '../exception.dart'; import '../util/character.dart'; import '../utils.dart'; import '../value.dart'; import '../visitor/interface/value.dart'; -import 'external/value.dart' as ext; /// A quoted empty string, returned by [SassString.empty]. final _emptyQuoted = SassString("", quotes: true); @@ -17,13 +17,44 @@ final _emptyQuoted = SassString("", quotes: true); /// An unquoted empty string, returned by [SassString.empty]. final _emptyUnquoted = SassString("", quotes: false); -class SassString extends Value implements ext.SassString { +/// A SassScript string. +/// +/// Strings can either be quoted or unquoted. Unquoted strings are usually CSS +/// identifiers, but they may contain any text. +@sealed +class SassString extends Value { + /// The contents of the string. + /// + /// For quoted strings, this is the semantic content—any escape sequences that + /// were been written in the source text are resolved to their Unicode values. + /// For unquoted strings, though, escape sequences are preserved as literal + /// backslashes. + /// + /// This difference allows us to distinguish between identifiers with escapes, + /// such as `url\u28 http://example.com\u29`, and unquoted strings that + /// contain characters that aren't valid in identifiers, such as + /// `url(http://example.com)`. Unfortunately, it also means that we don't + /// consider `foo` and `f\6F\6F` the same string. final String text; + /// Whether this string has quotes. final bool hasQuotes; + /// Sass's notion of the length of this string. + /// + /// Sass treats strings as a series of Unicode code points while Dart treats + /// them as a series of UTF-16 code units. For example, the character U+1F60A, + /// Smiling Face With Smiling Eyes, is a single Unicode code point but is + /// represented in UTF-16 as two code units (`0xD83D` and `0xDE0A`). So in + /// Dart, `"a😊b".length` returns `4`, whereas in Sass `str-length("a😊b")` + /// returns `3`. + /// + /// This returns the same value as `text.runes.length`, but it's more + /// efficient. late final int sassLength = text.runes.length; + /// @nodoc + @internal bool get isSpecialNumber { if (hasQuotes) return false; if (text.length < "min(_)".length) return false; @@ -67,6 +98,8 @@ class SassString extends Value implements ext.SassString { } } + /// @nodoc + @internal bool get isVar { if (hasQuotes) return false; if (text.length < "var(--_)".length) return false; @@ -77,18 +110,54 @@ class SassString extends Value implements ext.SassString { text.codeUnitAt(3) == $lparen; } + /// @nodoc + @internal bool get isBlank => !hasQuotes && text.isEmpty; + /// Creates an empty string. factory SassString.empty({bool quotes = true}) => quotes ? _emptyQuoted : _emptyUnquoted; + /// Creates a string with the given [text]. SassString(this.text, {bool quotes = true}) : hasQuotes = quotes; - int sassIndexToStringIndex(ext.Value sassIndex, [String? name]) => + /// Converts [sassIndex] into a Dart-style index into [text]. + /// + /// Sass indexes are one-based, while Dart indexes are zero-based. Sass + /// indexes may also be negative in order to index from the end of the string. + /// + /// In addition, Sass indices refer to Unicode code points while Dart string + /// indices refer to UTF-16 code units. For example, the character U+1F60A, + /// Smiling Face With Smiling Eyes, is a single Unicode code point but is + /// represented in UTF-16 as two code units (`0xD83D` and `0xDE0A`). So in + /// Dart, `"a😊b".codeUnitAt(1)` returns `0xD83D`, whereas in Sass + /// `str-slice("a😊b", 1, 1)` returns `"😊"`. + /// + /// This function converts Sass's code point indexes to Dart's code unit + /// indexes. This means it's O(n) in the length of [text]. See also + /// [sassIndexToRuneIndex], which is O(1) and returns an index into the + /// string's code points (accessible via `text.runes`). + /// + /// Throws a [SassScriptException] if [sassIndex] isn't a number, if that + /// number isn't an integer, or if that integer isn't a valid index for this + /// string. If [sassIndex] came from a function argument, [name] is the + /// argument name (without the `$`). It's used for error reporting. + int sassIndexToStringIndex(Value sassIndex, [String? name]) => codepointIndexToCodeUnitIndex( text, sassIndexToRuneIndex(sassIndex, name)); - int sassIndexToRuneIndex(ext.Value sassIndex, [String? name]) { + /// Converts [sassIndex] into a Dart-style index into [text]`.runes`. + /// + /// Sass indexes are one-based, while Dart indexes are zero-based. Sass + /// indexes may also be negative in order to index from the end of the string. + /// + /// See also [sassIndexToStringIndex], which an index into [text] directly. + /// + /// Throws a [SassScriptException] if [sassIndex] isn't a number, if that + /// number isn't an integer, or if that integer isn't a valid index for this + /// string. If [sassIndex] came from a function argument, [name] is the + /// argument name (without the `$`). It's used for error reporting. + int sassIndexToRuneIndex(Value sassIndex, [String? name]) { var index = sassIndex.assertNumber(name).assertInt(name); if (index == 0) throw _exception("String index may not be 0.", name); if (index.abs() > sassLength) { @@ -100,10 +169,14 @@ class SassString extends Value implements ext.SassString { return index < 0 ? sassLength + index : index - 1; } + /// @nodoc + @internal T accept(ValueVisitor visitor) => visitor.visitString(this); SassString assertString([String? name]) => this; + /// @nodoc + @internal Value plus(Value other) { if (other is SassString) { return SassString(text + other.text, quotes: hasQuotes); diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index c55898885..4b3d73409 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2194,7 +2194,7 @@ class _EvaluateVisitor return _withoutSlash(await result.accept(this), _expressionNode(result)); } - Future visitNullExpression(NullExpression node) async => sassNull; + Future visitNullExpression(NullExpression node) async => sassNull; Future visitNumberExpression(NumberExpression node) async => SassNumber(node.value, node.unit); diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index e7a3e27a4..342962352 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: b36c6c618b222a025ed67c1df8afb66e037a132b +// Checksum: b2321a00031707d2df699e6888a334deba39995d // // ignore_for_file: unused_import @@ -2183,7 +2183,7 @@ class _EvaluateVisitor return _withoutSlash(result.accept(this), _expressionNode(result)); } - SassNull visitNullExpression(NullExpression node) => sassNull; + Value visitNullExpression(NullExpression node) => sassNull; SassNumber visitNumberExpression(NumberExpression node) => SassNumber(node.value, node.unit); diff --git a/lib/src/visitor/interface/value.dart b/lib/src/visitor/interface/value.dart index e06eafc65..1e8756e2c 100644 --- a/lib/src/visitor/interface/value.dart +++ b/lib/src/visitor/interface/value.dart @@ -13,7 +13,7 @@ abstract class ValueVisitor { T visitFunction(SassFunction value); T visitList(SassList value); T visitMap(SassMap value); - T visitNull(SassNull value); + T visitNull(); T visitNumber(SassNumber value); T visitString(SassString value); } diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index fb7e35240..371040495 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -645,7 +645,7 @@ class _SerializeVisitor if (needsParens) _buffer.writeCharCode($rparen); } - void visitNull(SassNull value) { + void visitNull() { if (_inspect) _buffer.write("null"); }