diff --git a/lib/src/functions.dart b/lib/src/functions.dart index b3be6f91..87f0a5ee 100644 --- a/lib/src/functions.dart +++ b/lib/src/functions.dart @@ -696,17 +696,17 @@ final List coreFunctions = new UnmodifiableListView([ (arguments) => new SassNumber(arguments[0].asList.length)), new BuiltInCallable("nth", r"$list, $n", (arguments) { - var list = arguments[0].asList; - var index = arguments[1].assertNumber("n"); - return list[index.assertIndexFor(list, "n")]; + var list = arguments[0]; + var index = arguments[1]; + return list.asList[list.sassIndexToListIndex(index, "n")]; }), new BuiltInCallable("set-nth", r"$list, $n, $value", (arguments) { - var list = arguments[0].asList; - var index = arguments[1].assertNumber("n"); + var list = arguments[0]; + var index = arguments[1]; var value = arguments[2]; - var newList = list.toList(); - newList[index.assertIndexFor(list, "n")] = value; + var newList = list.asList.toList(); + newList[list.sassIndexToListIndex(index, "n")] = value; return arguments[0].changeListContents(newList); }), diff --git a/lib/src/value.dart b/lib/src/value.dart index 68cdb54e..6df2101b 100644 --- a/lib/src/value.dart +++ b/lib/src/value.dart @@ -2,6 +2,8 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import 'ast/selector.dart'; import 'exception.dart'; import 'value/boolean.dart'; @@ -36,6 +38,13 @@ abstract class Value implements ext.Value { bool get hasBrackets => false; List get asList => [this]; + /// The length of [asList]. + /// + /// This is used to compute [sassIndexToListIndex] without allocating a new + /// list. + @protected + int get lengthAsList => 1; + /// Whether the value will be represented in CSS as the empty string. bool get isBlank => false; @@ -61,6 +70,18 @@ abstract class Value implements ext.Value { /// It's not guaranteed to be stable across versions. T accept(ValueVisitor visitor); + int sassIndexToListIndex(ext.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) { + throw _exception( + "Invalid index $sassIndex for a list with ${lengthAsList} elements.", + name); + } + + return index < 0 ? lengthAsList + index : index - 1; + } + SassBoolean assertBoolean([String name]) => throw _exception("$this is not a boolean.", name); diff --git a/lib/src/value/external/number.dart b/lib/src/value/external/number.dart index a9919f59..0b376326 100644 --- a/lib/src/value/external/number.dart +++ b/lib/src/value/external/number.dart @@ -69,17 +69,6 @@ abstract class SassNumber extends Value { /// 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 diff --git a/lib/src/value/external/value.dart b/lib/src/value/external/value.dart index 0a73ccc2..418a7d40 100644 --- a/lib/src/value/external/value.dart +++ b/lib/src/value/external/value.dart @@ -58,6 +58,18 @@ abstract class Value { /// and all other values count as single-value lists. List get asList; + /// 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 diff --git a/lib/src/value/list.dart b/lib/src/value/list.dart index eaee3db2..7af6efb0 100644 --- a/lib/src/value/list.dart +++ b/lib/src/value/list.dart @@ -18,6 +18,8 @@ class SassList extends Value implements ext.SassList { List get asList => contents; + int get lengthAsList => contents.length; + const SassList.empty({ListSeparator separator, bool brackets: false}) : contents = const [], separator = separator ?? ListSeparator.undecided, diff --git a/lib/src/value/map.dart b/lib/src/value/map.dart index ba5aee28..c3d1683c 100644 --- a/lib/src/value/map.dart +++ b/lib/src/value/map.dart @@ -21,6 +21,8 @@ class SassMap extends Value implements ext.SassMap { return result; } + int get lengthAsList => contents.length; + /// Returns an empty map. const SassMap.empty() : contents = const {}; diff --git a/lib/src/value/number.dart b/lib/src/value/number.dart index 1833691b..0af49404 100644 --- a/lib/src/value/number.dart +++ b/lib/src/value/number.dart @@ -200,17 +200,6 @@ class SassNumber extends Value implements ext.SassNumber { throw _exception("$this is not an int.", name); } - int assertIndexFor(List list, [String name]) { - var sassIndex = assertInt(name); - if (sassIndex == 0) throw _exception("List index may not be 0."); - if (sassIndex.abs() > list.length) { - throw _exception( - "Invalid index $this for a list with ${list.length} elements."); - } - - return sassIndex < 0 ? list.length + sassIndex : sassIndex - 1; - } - num valueInRange(num min, num max, [String name]) { var result = fuzzyCheckRange(value, min, max); if (result != null) return result; diff --git a/test/dart_api/value/list_test.dart b/test/dart_api/value/list_test.dart index 4302bc3f..a0451992 100644 --- a/test/dart_api/value/list_test.dart +++ b/test/dart_api/value/list_test.dart @@ -62,6 +62,39 @@ main() { ListSeparator.comma)))); }); + group("sassIndexToListIndex()", () { + test("converts a positive index to a Dart index", () { + expect(value.sassIndexToListIndex(new SassNumber(1)), equals(0)); + expect(value.sassIndexToListIndex(new SassNumber(2)), equals(1)); + expect(value.sassIndexToListIndex(new SassNumber(3)), equals(2)); + }); + + test("converts a negative index to a Dart index", () { + expect(value.sassIndexToListIndex(new SassNumber(-1)), equals(2)); + expect(value.sassIndexToListIndex(new SassNumber(-2)), equals(1)); + expect(value.sassIndexToListIndex(new SassNumber(-3)), equals(0)); + }); + + test("rejects a non-number", () { + expect(() => value.sassIndexToListIndex(new SassString("foo")), + throwsSassScriptException); + }); + + test("rejects a non-integer", () { + expect(() => value.sassIndexToListIndex(new SassNumber(1.1)), + throwsSassScriptException); + }); + + test("rejects invalid indices", () { + expect(() => value.sassIndexToListIndex(new SassNumber(0)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(4)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(-4)), + throwsSassScriptException); + }); + }); + test("isn't any other type", () { expect(value.assertBoolean, throwsSassScriptException); expect(value.assertColor, throwsSassScriptException); @@ -133,6 +166,15 @@ main() { expect(value.assertNumber, throwsSassScriptException); expect(value.assertString, throwsSassScriptException); }); + + test("sassIndexToListIndex() rejects invalid indices", () { + expect(() => value.sassIndexToListIndex(new SassNumber(0)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(1)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(-1)), + throwsSassScriptException); + }); }); group("a scalar value", () { @@ -152,6 +194,25 @@ main() { expect(list, hasLength(1)); expect(list.first, same(value)); }); + + group("sassIndexToListIndex()", () { + test("converts a positive index to a Dart index", () { + expect(value.sassIndexToListIndex(new SassNumber(1)), equals(0)); + }); + + test("converts a negative index to a Dart index", () { + expect(value.sassIndexToListIndex(new SassNumber(-1)), equals(0)); + }); + + test("rejects invalid indices", () { + expect(() => value.sassIndexToListIndex(new SassNumber(0)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(2)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(-2)), + throwsSassScriptException); + }); + }); }); group("new SassList.empty()", () { diff --git a/test/dart_api/value/map_test.dart b/test/dart_api/value/map_test.dart index 76e9da41..4c0f2b4c 100644 --- a/test/dart_api/value/map_test.dart +++ b/test/dart_api/value/map_test.dart @@ -39,6 +39,27 @@ main() { ])); }); + group("sassIndexToListIndex()", () { + test("converts a positive index to a Dart index", () { + expect(value.sassIndexToListIndex(new SassNumber(1)), equals(0)); + expect(value.sassIndexToListIndex(new SassNumber(2)), equals(1)); + }); + + test("converts a negative index to a Dart index", () { + expect(value.sassIndexToListIndex(new SassNumber(-1)), equals(1)); + expect(value.sassIndexToListIndex(new SassNumber(-2)), equals(0)); + }); + + test("rejects invalid indices", () { + expect(() => value.sassIndexToListIndex(new SassNumber(0)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(3)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(-3)), + throwsSassScriptException); + }); + }); + test("equals the same map", () { expect( value, @@ -128,6 +149,15 @@ main() { test("equals an empty list", () { expect(value, equalsWithHash(new SassList.empty())); }); + + test("sassIndexToListIndex() rejects invalid indices", () { + expect(() => value.sassIndexToListIndex(new SassNumber(0)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(1)), + throwsSassScriptException); + expect(() => value.sassIndexToListIndex(new SassNumber(-1)), + throwsSassScriptException); + }); }); test("new SassMap.empty() creates an empty map with default metadata", () {