mirror of
https://github.com/danog/dart-sass.git
synced 2024-11-27 04:34:59 +01:00
Add SassString index helpers
We aren't using these in our own functions because, for backwards-compatibility, these functions don't throw errors for invalid indices.
This commit is contained in:
parent
c88912fc4c
commit
123fe55017
@ -45,6 +45,22 @@ export 'callable/user_defined.dart';
|
||||
///
|
||||
/// * When in doubt, lists should default to comma-separated, strings should
|
||||
/// default to quoted, and number should default to unitless.
|
||||
///
|
||||
/// * In Sass, lists and strings use one-based indexing and use negative indices
|
||||
/// to index from the end of value. Functions should follow these conventions.
|
||||
/// The [Value.sassIndexToListIndex] and [SassString.sassIndexToStringIndex]
|
||||
/// methods can be used to do this automatically.
|
||||
///
|
||||
/// * String indexes in Sass 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 `"😊"`. Functions should follow this
|
||||
/// convention. The [SassString.sassIndexToStringIndex] and
|
||||
/// [SassString.sassIndexToRuneIndex] methods can be used to do this
|
||||
/// automatically, and the [SassString.sassLength] getter can be used to
|
||||
/// access a string's length in code points.
|
||||
abstract class Callable extends AsyncCallable {
|
||||
/// Creates a callable with the given [name] and [arguments] that runs
|
||||
/// [callback] when called.
|
||||
|
@ -555,7 +555,7 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
|
||||
|
||||
new BuiltInCallable("str-length", r"$string", (arguments) {
|
||||
var string = arguments[0].assertString("string");
|
||||
return new SassNumber(string.text.runes.length);
|
||||
return new SassNumber(string.sassLength);
|
||||
}),
|
||||
|
||||
new BuiltInCallable("str-insert", r"$string, $insert, $index", (arguments) {
|
||||
@ -572,7 +572,7 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
|
||||
// negative.
|
||||
if (indexInt < 0) indexInt++;
|
||||
|
||||
var codepointIndex = _codepointForIndex(indexInt, string.text.runes.length);
|
||||
var codepointIndex = _codepointForIndex(indexInt, string.sassLength);
|
||||
|
||||
var codeUnitIndex =
|
||||
codepointIndexToCodeUnitIndex(string.text, codepointIndex);
|
||||
@ -600,7 +600,7 @@ final List<BuiltInCallable> coreFunctions = new UnmodifiableListView([
|
||||
start.assertNoUnits("start");
|
||||
end.assertNoUnits("end");
|
||||
|
||||
var lengthInCodepoints = string.text.runes.length;
|
||||
var lengthInCodepoints = string.sassLength;
|
||||
|
||||
// No matter what the start index is, an end index of 0 will produce an
|
||||
// empty string.
|
||||
|
49
lib/src/value/external/string.dart
vendored
49
lib/src/value/external/string.dart
vendored
@ -27,6 +27,19 @@ abstract class SassString extends Value {
|
||||
/// 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`.
|
||||
@ -36,4 +49,40 @@ abstract class SassString extends Value {
|
||||
///
|
||||
/// 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]);
|
||||
}
|
||||
|
@ -4,9 +4,11 @@
|
||||
|
||||
import 'package:charcode/charcode.dart';
|
||||
|
||||
import '../exception.dart';
|
||||
import '../util/character.dart';
|
||||
import '../visitor/interface/value.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].
|
||||
@ -20,6 +22,13 @@ class SassString extends Value implements ext.SassString {
|
||||
|
||||
final bool hasQuotes;
|
||||
|
||||
int get sassLength {
|
||||
_sassLength ??= text.runes.length;
|
||||
return _sassLength;
|
||||
}
|
||||
|
||||
int _sassLength;
|
||||
|
||||
bool get isSpecialNumber {
|
||||
if (hasQuotes) return false;
|
||||
if (text.length < "calc(_)".length) return false;
|
||||
@ -56,6 +65,22 @@ class SassString extends Value implements ext.SassString {
|
||||
|
||||
SassString(this.text, {bool quotes: false}) : hasQuotes = quotes;
|
||||
|
||||
int sassIndexToStringIndex(ext.Value sassIndex, [String name]) =>
|
||||
codepointIndexToCodeUnitIndex(
|
||||
text, sassIndexToRuneIndex(sassIndex, name));
|
||||
|
||||
int sassIndexToRuneIndex(ext.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) {
|
||||
throw _exception(
|
||||
"Invalid index $sassIndex for a string with $sassLength characters.",
|
||||
name);
|
||||
}
|
||||
|
||||
return index < 0 ? sassLength + index : index - 1;
|
||||
}
|
||||
|
||||
T accept<T>(ValueVisitor<T> visitor) => visitor.visitString(this);
|
||||
|
||||
SassString assertString([String name]) => this;
|
||||
@ -71,4 +96,8 @@ class SassString extends Value implements ext.SassString {
|
||||
bool operator ==(other) => other is SassString && text == other.text;
|
||||
|
||||
int get hashCode => text.hashCode;
|
||||
|
||||
/// Throws a [SassScriptException] with the given [message].
|
||||
SassScriptException _exception(String message, [String name]) =>
|
||||
new SassScriptException(name == null ? message : "\$$name: $message");
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import 'package:sass/sass.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
main() {
|
||||
group("an unquoted string", () {
|
||||
group("an unquoted ASCII string", () {
|
||||
SassString value;
|
||||
setUp(() => value = parseValue("foobar") as SassString);
|
||||
|
||||
@ -39,9 +39,91 @@ main() {
|
||||
expect(value.assertMap, throwsSassScriptException);
|
||||
expect(value.assertNumber, throwsSassScriptException);
|
||||
});
|
||||
|
||||
test("sassLength returns the length", () {
|
||||
expect(value.sassLength, equals(6));
|
||||
});
|
||||
|
||||
group("sassIndexToStringIndex()", () {
|
||||
test("converts a positive index to a Dart index", () {
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(1)), equals(0));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(2)), equals(1));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(3)), equals(2));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(4)), equals(3));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(5)), equals(4));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(6)), equals(5));
|
||||
});
|
||||
|
||||
test("converts a negative index to a Dart index", () {
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-1)), equals(5));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-2)), equals(4));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-3)), equals(3));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-4)), equals(2));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-5)), equals(1));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-6)), equals(0));
|
||||
});
|
||||
|
||||
test("rejects a non-number", () {
|
||||
expect(() => value.sassIndexToStringIndex(new SassString("foo")),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
|
||||
test("rejects a non-integer", () {
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(1.1)),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
|
||||
test("rejects invalid indices", () {
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(0)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(7)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(-7)),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
});
|
||||
|
||||
group("sassIndexToRuneIndex()", () {
|
||||
test("converts a positive index to a Dart index", () {
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(1)), equals(0));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(2)), equals(1));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(3)), equals(2));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(4)), equals(3));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(5)), equals(4));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(6)), equals(5));
|
||||
});
|
||||
|
||||
test("converts a negative index to a Dart index", () {
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-1)), equals(5));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-2)), equals(4));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-3)), equals(3));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-4)), equals(2));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-5)), equals(1));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-6)), equals(0));
|
||||
});
|
||||
|
||||
test("rejects a non-number", () {
|
||||
expect(() => value.sassIndexToRuneIndex(new SassString("foo")),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
|
||||
test("rejects a non-integer", () {
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(1.1)),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
|
||||
test("rejects invalid indices", () {
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(0)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(7)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(-7)),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("a quoted string", () {
|
||||
group("a quoted ASCII string", () {
|
||||
SassString value;
|
||||
setUp(() => value = parseValue('"foobar"') as SassString);
|
||||
|
||||
@ -59,6 +141,69 @@ main() {
|
||||
});
|
||||
});
|
||||
|
||||
group("an unquoted Unicde", () {
|
||||
SassString value;
|
||||
setUp(() => value = parseValue("a👭b👬c") as SassString);
|
||||
|
||||
test("sassLength returns the length", () {
|
||||
expect(value.sassLength, equals(5));
|
||||
});
|
||||
|
||||
group("sassIndexToStringIndex()", () {
|
||||
test("converts a positive index to a Dart index", () {
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(1)), equals(0));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(2)), equals(1));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(3)), equals(3));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(4)), equals(4));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(5)), equals(6));
|
||||
});
|
||||
|
||||
test("converts a negative index to a Dart index", () {
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-1)), equals(6));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-2)), equals(4));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-3)), equals(3));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-4)), equals(1));
|
||||
expect(value.sassIndexToStringIndex(new SassNumber(-5)), equals(0));
|
||||
});
|
||||
|
||||
test("rejects invalid indices", () {
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(0)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(6)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToStringIndex(new SassNumber(-6)),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
});
|
||||
|
||||
group("sassIndexToRuneIndex()", () {
|
||||
test("converts a positive index to a Dart index", () {
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(1)), equals(0));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(2)), equals(1));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(3)), equals(2));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(4)), equals(3));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(5)), equals(4));
|
||||
});
|
||||
|
||||
test("converts a negative index to a Dart index", () {
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-1)), equals(4));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-2)), equals(3));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-3)), equals(2));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-4)), equals(1));
|
||||
expect(value.sassIndexToRuneIndex(new SassNumber(-5)), equals(0));
|
||||
});
|
||||
|
||||
test("rejects invalid indices", () {
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(0)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(6)),
|
||||
throwsSassScriptException);
|
||||
expect(() => value.sassIndexToRuneIndex(new SassNumber(-6)),
|
||||
throwsSassScriptException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("new SassString.empty()", () {
|
||||
test("creates an empty unquoted string", () {
|
||||
var string = new SassString.empty();
|
||||
|
Loading…
Reference in New Issue
Block a user