Split Value and its subtypes into public and private interfaces (#210)

This commit is contained in:
Natalie Weizenbaum 2018-01-13 01:30:42 -08:00 committed by GitHub
parent 8e57a0635d
commit 1e09cec5aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 525 additions and 262 deletions

View File

@ -12,7 +12,8 @@ import 'src/sync_package_resolver.dart';
export 'src/callable.dart' show Callable, AsyncCallable;
export 'src/importer.dart';
export 'src/value.dart' hide SassNull;
export 'src/value.dart' show ListSeparator;
export 'src/value/external/value.dart';
/// Loads the Sass file at [path], compiles it to CSS, and returns the result.
///

View File

@ -13,9 +13,9 @@ class ColorExpression implements Expression {
/// The value of this color.
final SassColor value;
final FileSpan span;
FileSpan get span => value.originalSpan;
ColorExpression(this.value, this.span);
ColorExpression(this.value);
T accept<T>(ExpressionVisitor<T> visitor) =>
visitor.visitColorExpression(this);

View File

@ -5,6 +5,7 @@
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';
@ -87,5 +88,7 @@ abstract class Callable extends AsyncCallable {
/// which provides access to keyword arguments using
/// [SassArgumentList.keywords].
factory Callable(String name, String arguments,
Value callback(List<Value> arguments)) = BuiltInCallable;
ext.Value callback(List<ext.Value> arguments)) =>
new BuiltInCallable(
name, arguments, (arguments) => callback(arguments) as Value);
}

View File

@ -4,7 +4,10 @@
import 'dart:async';
import 'package:async/async.dart';
import '../value.dart';
import '../value/external/value.dart' as ext;
import 'async_built_in.dart';
/// An interface functions and mixins that can be invoked from Sass by passing
@ -27,5 +30,10 @@ abstract class AsyncCallable {
///
/// See [new Callable] for more details.
factory AsyncCallable(String name, String arguments,
FutureOr<Value> callback(List<Value> arguments)) = AsyncBuiltInCallable;
FutureOr<ext.Value> callback(List<ext.Value> arguments)) =>
new AsyncBuiltInCallable(name, arguments, (arguments) {
var result = callback(arguments);
if (result is ext.Value) return result as Value;
return DelegatingFuture.typed(result as Future);
});
}

View File

@ -1704,20 +1704,14 @@ abstract class StylesheetParser extends Parser {
var first = scanner.peekChar();
if (first != null && isDigit(first)) {
var color = _hexColorContents();
var span = scanner.spanFrom(start);
setOriginalSpan(color, span);
return new ColorExpression(color, span);
return new ColorExpression(_hexColorContents(start));
}
var afterHash = scanner.state;
var identifier = _interpolatedIdentifier();
if (_isHexColor(identifier)) {
scanner.state = afterHash;
var color = _hexColorContents();
var span = scanner.spanFrom(start);
setOriginalSpan(color, span);
return new ColorExpression(color, span);
return new ColorExpression(_hexColorContents(start));
}
var buffer = new InterpolationBuffer();
@ -1727,7 +1721,7 @@ abstract class StylesheetParser extends Parser {
}
/// Consumes the contents of a hex color, after the `#`.
SassColor _hexColorContents() {
SassColor _hexColorContents(LineScannerState start) {
var red = _hexDigit();
var green = _hexDigit();
var blue = _hexDigit();
@ -1743,7 +1737,7 @@ abstract class StylesheetParser extends Parser {
blue = (blue << 4) + blue;
}
return new SassColor.rgb(red, green, blue);
return new SassColor.rgb(red, green, blue, 1, scanner.spanFrom(start));
}
/// Returns whether [interpolation] is a plain string that can be parsed as a
@ -2034,9 +2028,8 @@ abstract class StylesheetParser extends Parser {
var color = colorsByName[lower];
if (color != null) {
color = new SassColor.rgb(
color.red, color.green, color.blue, color.alpha);
setOriginalSpan(color, identifier.span);
return new ColorExpression(color, identifier.span);
color.red, color.green, color.blue, color.alpha, identifier.span);
return new ColorExpression(color);
}
}

View File

@ -6,6 +6,7 @@ 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';
@ -16,7 +17,7 @@ import 'visitor/serialize.dart';
export 'value/argument_list.dart';
export 'value/boolean.dart';
export 'value/color.dart' hide setOriginalSpan;
export 'value/color.dart';
export 'value/function.dart';
export 'value/list.dart';
export 'value/map.dart';
@ -24,38 +25,20 @@ export 'value/null.dart';
export 'value/number.dart';
export 'value/string.dart';
/// A SassScript value.
// TODO(nweiz): Just mark members as @internal when sdk#28066 is fixed.
/// The implementation of [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.
abstract class 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 {
bool get isTruthy => true;
ListSeparator get separator => ListSeparator.undecided;
bool get hasBrackets => false;
List<Value> get asList => [this];
/// Whether the value will be represented in CSS as the empty string.
bool get isBlank => false;
/// 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<Value> get asList => [this];
/// Whether this is a value that CSS may treat as a number, such as `calc()`
/// or `var()`.
///
@ -78,48 +61,21 @@ abstract class Value {
/// It's not guaranteed to be stable across versions.
T accept<T>(ValueVisitor<T> visitor);
/// 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);
/// 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);
@ -132,8 +88,6 @@ abstract class Value {
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`). It's used for error reporting.
///
/// **Note:** this function should not be called outside the `sass` package.
SelectorList assertSelector({String name, bool allowParent: false}) {
var string = _selectorString(name);
try {
@ -154,9 +108,6 @@ abstract class Value {
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`). It's used for error reporting.
///
/// **Note:** this function should not be called outside the `sass` package.
/// It's not guaranteed to be stable across versions.
SimpleSelector assertSimpleSelector({String name, bool allowParent: false}) {
var string = _selectorString(name);
try {
@ -177,9 +128,6 @@ abstract class Value {
///
/// If this came from a function argument, [name] is the argument name
/// (without the `$`). It's used for error reporting.
///
/// **Note:** this function should not be called outside the `sass` package.
/// It's not guaranteed to be stable across versions.
CompoundSelector assertCompoundSelector(
{String name, bool allowParent: false}) {
var string = _selectorString(name);
@ -369,11 +317,6 @@ abstract class Value {
/// If [quote] is `false`, quoted strings are emitted without quotes.
String toCssString({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].

View File

@ -4,18 +4,13 @@
import 'dart:collection';
import 'package:collection/collection.dart';
import '../utils.dart';
import '../value.dart';
import 'external/value.dart' as ext;
/// 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.
class SassArgumentList extends SassList {
/// The keyword arguments attached to this argument list.
///
/// The argument names don't include `$`.
class SassArgumentList extends SassList implements ext.SassArgumentList {
Map<String, Value> get keywords {
_wereKeywordsAccessed = true;
return _keywords;

View File

@ -4,6 +4,7 @@
import '../visitor/interface/value.dart';
import '../value.dart';
import 'external/value.dart' as ext;
/// The SassScript `true` value.
const sassTrue = const SassBoolean._(true);
@ -11,17 +12,11 @@ const sassTrue = const SassBoolean._(true);
/// The SassScript `false` value.
const sassFalse = const SassBoolean._(false);
/// A SassScript boolean value.
class SassBoolean extends Value {
/// Whether this value is `true` or `false`.
class SassBoolean extends Value implements ext.SassBoolean {
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);

View File

@ -10,17 +10,9 @@ import '../exception.dart';
import '../util/number.dart';
import '../value.dart';
import '../visitor/interface/value.dart';
import 'external/value.dart' as ext;
/// Sets the span for [SassColor.original] to [span].
///
/// This is a separate function so it can be hidden in most exports.
void setOriginalSpan(SassColor color, SourceSpan span) {
color._originalSpan = span;
}
/// A SassScript color.
class SassColor extends Value {
/// This color's red channel, between `0` and `255`.
class SassColor extends Value implements ext.SassColor {
int get red {
if (_red == null) _hslToRgb();
return _red;
@ -28,7 +20,6 @@ class SassColor extends Value {
int _red;
/// This color's green channel, between `0` and `255`.
int get green {
if (_green == null) _hslToRgb();
return _green;
@ -36,7 +27,6 @@ class SassColor extends Value {
int _green;
/// This color's blue channel, between `0` and `255`.
int get blue {
if (_blue == null) _hslToRgb();
return _blue;
@ -44,7 +34,6 @@ class SassColor extends Value {
int _blue;
/// This color's hue, between `0` and `360`.
num get hue {
if (_hue == null) _rgbToHsl();
return _hue;
@ -52,7 +41,6 @@ class SassColor extends Value {
num _hue;
/// This color's saturation, a percentage between `0` and `100`.
num get saturation {
if (_saturation == null) _rgbToHsl();
return _saturation;
@ -60,7 +48,6 @@ class SassColor extends Value {
num _saturation;
/// This color's lightness, a percentage between `0` and `100`.
num get lightness {
if (_lightness == null) _rgbToHsl();
return _lightness;
@ -68,57 +55,48 @@ class SassColor extends Value {
num _lightness;
/// 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.
String get original => _originalSpan?.text;
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.
SourceSpan _originalSpan;
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])
SassColor.rgb(this._red, this._green, this._blue,
[num alpha, this.originalSpan])
: alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha") {
RangeError.checkValueInInterval(red, 0, 255, "red");
RangeError.checkValueInInterval(green, 0, 255, "green");
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"),
_lightness = fuzzyAssertRange(lightness, 0, 100, "lightness"),
alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha");
alpha = alpha == null ? 1 : fuzzyAssertRange(alpha, 0, 1, "alpha"),
originalSpan = null;
SassColor._(this._red, this._green, this._blue, this._hue, this._saturation,
this._lightness, this.alpha);
this._lightness, this.alpha)
: originalSpan = null;
T accept<T>(ValueVisitor<T> 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}) =>
new 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}) =>
new SassColor.hsl(hue ?? this.hue, saturation ?? this.saturation,
lightness ?? this.lightness, alpha ?? this.alpha);
/// Returns a new copy of this color with the alpha channel set to [alpha].
SassColor changeAlpha(num alpha) => new SassColor._(_red, _green, _blue, _hue,
_saturation, _lightness, fuzzyAssertRange(alpha, 0, 1, "alpha"));

View File

@ -0,0 +1,26 @@
// 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:collection/collection.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.
abstract class SassArgumentList extends SassList {
/// The keyword arguments attached to this argument list.
///
/// The argument names don't include `$`.
Map<String, Value> get keywords;
factory SassArgumentList(Iterable<Value> contents,
Map<String, Value> keywords, ListSeparator separator) =>
new internal.SassArgumentList(DelegatingIterable.typed(contents),
DelegatingMap.typed(keywords), separator);
}

24
lib/src/value/external/boolean.dart vendored Normal file
View File

@ -0,0 +1,24 @@
// 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 '../../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.
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;
}

53
lib/src/value/external/color.dart vendored Normal file
View File

@ -0,0 +1,53 @@
// 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 '../../value.dart' as internal;
import 'value.dart';
/// A SassScript color.
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 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;
/// 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});
/// Returns a new copy of this color with the alpha channel set to [alpha].
SassColor changeAlpha(num alpha);
}

22
lib/src/value/external/function.dart vendored Normal file
View File

@ -0,0 +1,22 @@
// 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 '../../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.
abstract class SassFunction extends Value {
/// The callable that this function invokes.
///
/// Note that this is typed as an [AsyncCallback] 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 [Callback].
AsyncCallable get callable;
factory SassFunction(AsyncCallable callable) = internal.SassFunction;
}

36
lib/src/value/external/list.dart vendored Normal file
View File

@ -0,0 +1,36 @@
// 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:collection/collection.dart';
import '../../value.dart' as internal;
import '../../value.dart' show ListSeparator;
import 'value.dart';
/// A SassScript list.
abstract class SassList extends Value {
// TODO(nweiz): Use persistent data structures rather than copying here. An
// RRB vector should fit our use-cases well.
//
// We may also want to fall back to a plain unmodifiable List for small lists
// (<32 items?).
/// The contents of the list.
List<Value> get contents;
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<Value> contents, ListSeparator separator,
{bool brackets: false}) =>
new internal.SassList(DelegatingIterable.typed(contents), separator,
brackets: brackets);
}

26
lib/src/value/external/map.dart vendored Normal file
View File

@ -0,0 +1,26 @@
// 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:collection/collection.dart';
import '../../value.dart' as internal;
import 'value.dart';
/// A SassScript map.
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<Value, Value> get contents;
/// Returns an empty map.
const factory SassMap.empty() = internal.SassMap.empty;
factory SassMap(Map<Value, Value> contents) =>
new internal.SassMap(DelegatingMap.typed(contents));
}

123
lib/src/value/external/number.dart vendored Normal file
View File

@ -0,0 +1,123 @@
// 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 '../../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.
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<String> get numeratorUnits;
/// This number's denominator units.
List<String> 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,
{Iterable<String> numeratorUnits,
Iterable<String> 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]);
/// Asserts that this is a valid Sass-style index for [list], and returns the
/// Dart-style index.
///
/// A Sass-style index is one-based, and uses negative numbers to count
/// backwards from the end of the list.
///
/// Throws a [SassScriptException] if this isn't an integer or if it isn't a
/// valid index for [list]. If this came from a function argument, [name] is
/// the argument name (without the `$`). It's used for error reporting.
int assertIndexFor(List list, [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);
/// 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 units represented by
/// [newNumerators] and [newDenominators].
///
/// Note that [valueInUnits] 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].
SassNumber coerce(List<String> newNumerators, List<String> newDenominators);
/// Returns [value], converted to the units represented by [newNumerators] and
/// [newDenominators].
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [newNumerators] and [newDenominators].
num valueInUnits(List<String> newNumerators, List<String> newDenominators);
}

39
lib/src/value/external/string.dart vendored Normal file
View File

@ -0,0 +1,39 @@
// 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 '../../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.
abstract class SassString extends Value {
/// The contents of the string.
///
/// For quoted strings, this is the semantic contentany 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;
/// 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;
}

113
lib/src/value/external/value.dart vendored Normal file
View File

@ -0,0 +1,113 @@
// 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 '../../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.
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<Value> get asList;
/// 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]);
/// 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();
}

View File

@ -5,17 +5,9 @@
import '../callable.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
import 'external/value.dart' as internal;
/// A SassScript function reference.
///
/// A function reference captures a function from the local environment so that
/// it may be passed between modules.
class SassFunction extends Value {
/// The callable that this function invokes.
///
/// Note that this is typed as an [AsyncCallback] 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 [Callback].
class SassFunction extends Value implements internal.SassFunction {
final AsyncCallable callable;
SassFunction(this.callable);

View File

@ -5,15 +5,9 @@
import '../utils.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
import 'external/value.dart' as ext;
/// A SassScript list.
class SassList extends Value {
// TODO(nweiz): Use persistent data structures rather than copying here. An
// RRB vector should fit our use-cases well.
//
// We may also want to fall back to a plain unmodifiable List for small lists
// (<32 items?).
/// The contents of the list.
class SassList extends Value implements ext.SassList {
final List<Value> contents;
final ListSeparator separator;
@ -24,7 +18,6 @@ class SassList extends Value {
List<Value> get asList => contents;
/// Returns an empty list with the given [separator] and [brackets].
const SassList.empty({ListSeparator separator, bool brackets: false})
: contents = const [],
separator = separator ?? ListSeparator.undecided,

View File

@ -6,16 +6,9 @@ import 'package:collection/collection.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
import 'external/value.dart' as ext;
/// A SassScript map.
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.
class SassMap extends Value implements ext.SassMap {
final Map<Value, Value> contents;
ListSeparator get separator => ListSeparator.comma;

View File

@ -9,6 +9,7 @@ import '../util/number.dart';
import '../utils.dart';
import '../value.dart';
import '../visitor/interface/value.dart';
import 'external/value.dart' as ext;
/// A nested map containing unit conversion rates.
///
@ -141,66 +142,31 @@ final _conversions = {
// and numbers with only a single numerator unit. These should be opaque to
// users of SassNumber.
/// 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.
class SassNumber extends Value {
/// The number of distinct digits that are emitted when converting a number to
/// CSS.
static const precision = 10;
class SassNumber extends Value implements ext.SassNumber {
static const precision = ext.SassNumber.precision;
/// 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.
final List<String> numeratorUnits;
/// This number's denominator units.
final List<String> denominatorUnits;
/// The slash-separated representation of this number, if it has one.
final String asSlash;
/// 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 => numeratorUnits.isNotEmpty || denominatorUnits.isNotEmpty;
/// 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.
SassNumber(num value, [String unit])
: this.withUnits(value, numeratorUnits: unit == null ? null : [unit]);
/// Creates a number with full [numeratorUnits] and [denominatorUnits].
SassNumber.withUnits(this.value,
{Iterable<String> numeratorUnits, Iterable<String> denominatorUnits})
: numeratorUnits = numeratorUnits == null
@ -228,27 +194,12 @@ class SassNumber extends Value {
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);
}
/// Asserts that this is a valid Sass-style index for [list], and returns the
/// Dart-style index.
///
/// A Sass-style index is one-based, and uses negative numbers to count
/// backwards from the end of the list.
///
/// Throws a [SassScriptException] if this isn't an integer or if it isn't a
/// valid index for [list]. If this came from a function argument, [name] is
/// the argument name (without the `$`). It's used for error reporting.
int assertIndexFor(List list, [String name]) {
var sassIndex = assertInt(name);
if (sassIndex == 0) throw _exception("List index may not be 0.");
@ -260,12 +211,6 @@ class SassNumber extends Value {
return sassIndex < 0 ? list.length + sassIndex : sassIndex - 1;
}
/// 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;
@ -273,48 +218,25 @@ class SassNumber extends Value {
"Expected $this to be within $min$unitString and $max$unitString.");
}
/// Returns whether [this] has [unit] as its only unit (and as a numerator).
bool hasUnit(String unit) =>
numeratorUnits.length == 1 &&
denominatorUnits.isEmpty &&
numeratorUnits.first == 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);
}
/// Returns a copy of this number, converted to the units represented by
/// [newNumerators] and [newDenominators].
///
/// Note that [valueInUnits] 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].
SassNumber coerce(List<String> newNumerators, List<String> newDenominators) =>
new SassNumber.withUnits(valueInUnits(newNumerators, newDenominators),
numeratorUnits: newNumerators, denominatorUnits: newDenominators);
/// Returns [value], converted to the units represented by [newNumerators] and
/// [newDenominators].
///
/// Throws a [SassScriptException] if this number's units aren't compatible
/// with [newNumerators] and [newDenominators].
num valueInUnits(List<String> newNumerators, List<String> newDenominators) {
if ((newNumerators.isEmpty && newDenominators.isEmpty) ||
(numeratorUnits.isEmpty && denominatorUnits.isEmpty) ||

View File

@ -7,6 +7,7 @@ import 'package:charcode/charcode.dart';
import '../util/character.dart';
import '../visitor/interface/value.dart';
import '../value.dart';
import 'external/value.dart' as ext;
/// A quoted empty string, returned by [SassString.empty].
final _emptyQuoted = new SassString("", quotes: true);
@ -14,26 +15,9 @@ final _emptyQuoted = new SassString("", quotes: true);
/// An unquoted empty string, returned by [SassString.empty].
final _emptyUnquoted = new SassString("", quotes: false);
/// A SassScript string.
///
/// Strings can either be quoted or unquoted. Unquoted strings are usually CSS
/// identifiers, but they may contain any text.
class SassString extends Value {
/// The contents of the string.
///
/// For quoted strings, this is the semantic contentany 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.
class SassString extends Value implements ext.SassString {
final String text;
/// Whether this string has quotes.
final bool hasQuotes;
bool get isSpecialNumber {

View File

@ -12,6 +12,7 @@ environment:
dependencies:
args: "^0.13.0"
async: ">=1.10.0 <3.0.0"
charcode: "^1.1.0"
collection: "^1.8.0"
convert: "^2.0.1"