mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-10 14:58:38 +01:00
5a95a72771
This allows us to support cases where an extension's extender is itself extended after it's declared but before it's used.
272 lines
9.1 KiB
Dart
272 lines
9.1 KiB
Dart
// 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 'dart:collection';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:charcode/charcode.dart';
|
|
import 'package:collection/collection.dart';
|
|
import 'package:source_span/source_span.dart';
|
|
|
|
import 'ast/node.dart';
|
|
import 'io.dart';
|
|
import 'util/character.dart';
|
|
|
|
/// Converts [iter] into a sentence, separating each word with [conjunction].
|
|
String toSentence(Iterable iter, [String conjunction]) {
|
|
conjunction ??= "and";
|
|
if (iter.length == 1) return iter.first.toString();
|
|
return iter.take(iter.length - 1).join(", ") + " $conjunction ${iter.last}";
|
|
}
|
|
|
|
/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
|
|
///
|
|
/// By default, this just adds "s" to the end of [name] to get the plural. If
|
|
/// [plural] is passed, that's used instead.
|
|
String pluralize(String name, int number, {String plural}) {
|
|
if (number == 1) return name;
|
|
if (plural != null) return plural;
|
|
return '${name}s';
|
|
}
|
|
|
|
/// Flattens the first level of nested arrays in [iterable].
|
|
///
|
|
/// The return value is ordered first by index in the nested iterable, then by
|
|
/// the index *of* that iterable in [iterable]. For example,
|
|
/// `flattenVertically([["1a", "1b"], ["2a", "2b"]])` returns `["1a", "2a",
|
|
/// "1b", "2b"]`.
|
|
List<T> flattenVertically<T>(Iterable<Iterable<T>> iterable) {
|
|
var queues = iterable.map((inner) => new QueueList.from(inner)).toList();
|
|
if (queues.length == 1) return queues.first;
|
|
|
|
var result = <T>[];
|
|
while (queues.isNotEmpty) {
|
|
queues.removeWhere((queue) {
|
|
result.add(queue.removeFirst());
|
|
return queue.isEmpty;
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Converts [codepointIndex] to a code unit index, relative to [string].
|
|
///
|
|
/// A codepoint index is the index in pure Unicode codepoints; a code unit index
|
|
/// is an index into a UTF-16 string.
|
|
int codepointIndexToCodeUnitIndex(String string, int codepointIndex) {
|
|
var codeUnitIndex = 0;
|
|
for (var i = 0; i < codepointIndex; i++) {
|
|
if (isHighSurrogate(string.codeUnitAt(codeUnitIndex++))) codeUnitIndex++;
|
|
}
|
|
return codeUnitIndex;
|
|
}
|
|
|
|
/// Converts [codeUnitIndex] to a codepoint index, relative to [string].
|
|
///
|
|
/// A codepoint index is the index in pure Unicode codepoints; a code unit index
|
|
/// is an index into a UTF-16 string.
|
|
int codeUnitIndexToCodepointIndex(String string, int codeUnitIndex) {
|
|
var codepointIndex = 0;
|
|
for (var i = 0; i < codeUnitIndex; i++) {
|
|
codepointIndex++;
|
|
if (isHighSurrogate(string.codeUnitAt(i))) i++;
|
|
}
|
|
return codepointIndex;
|
|
}
|
|
|
|
/// Returns whether [list1] and [list2] have the same contents.
|
|
bool listEquals<T>(List<T> list1, List<T> list2) =>
|
|
const ListEquality().equals(list1, list2);
|
|
|
|
/// Returns a hash code for [list] that matches [listEquals].
|
|
int listHash(List list) => const ListEquality().hash(list);
|
|
|
|
/// Returns a source span that covers the spans of both the first and last nodes
|
|
/// in [nodes].
|
|
///
|
|
/// If [nodes] is empty, or if either the first or last node has a `null` span,
|
|
/// returns `null`.
|
|
FileSpan spanForList(List<AstNode> nodes) {
|
|
if (nodes.isEmpty) return null;
|
|
// Spans may be null for dynamically-constructed ASTs.
|
|
if (nodes.first.span == null) return null;
|
|
if (nodes.last.span == null) return null;
|
|
return nodes.first.span.expand(nodes.last.span);
|
|
}
|
|
|
|
/// Returns [name] without a vendor prefix.
|
|
///
|
|
/// If [name] has no vendor prefix, it's returned as-is.
|
|
String unvendor(String name) {
|
|
if (name.length < 2) return name;
|
|
if (name.codeUnitAt(0) != $dash) return name;
|
|
if (name.codeUnitAt(1) == $dash) return name;
|
|
|
|
for (var i = 2; i < name.length; i++) {
|
|
if (name.codeUnitAt(i) == $dash) return name.substring(i + 1);
|
|
}
|
|
return name;
|
|
}
|
|
|
|
/// Returns whether [string1] and [string2] are equal if `-` and `_` are
|
|
/// considered equivalent.
|
|
bool equalsIgnoreSeparator(String string1, String string2) {
|
|
if (identical(string1, string2)) return true;
|
|
if (string1 == null || string2 == null) return false;
|
|
if (string1.length != string2.length) return false;
|
|
for (var i = 0; i < string1.length; i++) {
|
|
var codeUnit1 = string1.codeUnitAt(i);
|
|
var codeUnit2 = string2.codeUnitAt(i);
|
|
if (codeUnit1 == codeUnit2) continue;
|
|
if (codeUnit1 == $dash) {
|
|
if (codeUnit2 != $underscore) return false;
|
|
} else if (codeUnit1 == $underscore) {
|
|
if (codeUnit2 != $dash) return false;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Returns a hash code for [string] that matches [equalsIgnoreSeparator].
|
|
int hashCodeIgnoreSeparator(String string) {
|
|
var hash = 4603;
|
|
for (var i = 0; i < string.length; i++) {
|
|
var codeUnit = string.codeUnitAt(i);
|
|
if (codeUnit == $underscore) codeUnit = $dash;
|
|
hash &= 0x3FFFFFF;
|
|
hash *= 33;
|
|
hash ^= codeUnit;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
/// Returns whether [string1] and [string2] are equal, ignoring ASCII case.
|
|
bool equalsIgnoreCase(String string1, String string2) {
|
|
if (identical(string1, string2)) return true;
|
|
if (string1 == null || string2 == null) return false;
|
|
if (string1.length != string2.length) return false;
|
|
return string1.toUpperCase() == string2.toUpperCase();
|
|
}
|
|
|
|
/// Returns an empty map that uses [equalsIgnoreSeparator] for key equality.
|
|
///
|
|
/// If [source] is passed, copies it into the map.
|
|
Map<String, V> normalizedMap<V>([Map<String, V> source]) {
|
|
var map = new LinkedHashMap<String, V>(
|
|
equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator);
|
|
if (source != null) map.addAll(source);
|
|
return map;
|
|
}
|
|
|
|
/// Returns an empty set that uses [equalsIgnoreSeparator] for equality.
|
|
///
|
|
/// If [source] is passed, copies it into the set.
|
|
Set<String> normalizedSet([Iterable<String> source]) {
|
|
var set = new LinkedHashSet(
|
|
equals: equalsIgnoreSeparator, hashCode: hashCodeIgnoreSeparator);
|
|
if (source != null) set.addAll(source);
|
|
return set;
|
|
}
|
|
|
|
/// Like [mapMap], but returns a map that uses [equalsIgnoreSeparator] for key
|
|
/// equality.
|
|
Map<String, V2> normalizedMapMap<K, V1, V2>(Map<K, V1> map,
|
|
{String key(K key, V1 value), V2 value(K key, V1 value)}) {
|
|
key ??= (mapKey, _) => mapKey as String;
|
|
value ??= (_, mapValue) => mapValue as V2;
|
|
|
|
var result = normalizedMap<V2>();
|
|
map.forEach((mapKey, mapValue) {
|
|
result[key(mapKey, mapValue)] = value(mapKey, mapValue);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/// Returns the longest common subsequence between [list1] and [list2].
|
|
///
|
|
/// If there are more than one equally long common subsequence, returns the one
|
|
/// which starts first in [list1].
|
|
///
|
|
/// If [select] is passed, it's used to check equality between elements in each
|
|
/// list. If it returns `null`, the elements are considered unequal; otherwise,
|
|
/// it should return the element to include in the return value.
|
|
List<T> longestCommonSubsequence<T>(List<T> list1, List<T> list2,
|
|
{T select(T element1, T element2)}) {
|
|
select ??= (element1, element2) => element1 == element2 ? element1 : null;
|
|
|
|
var lengths = new List.generate(
|
|
list1.length + 1, (_) => new List.filled(list2.length + 1, 0),
|
|
growable: false);
|
|
|
|
var selections = new List<List<T>>.generate(
|
|
list1.length, (_) => new List<T>(list2.length),
|
|
growable: false);
|
|
|
|
// TODO(nweiz): Calling [select] here may be expensive. Can we use a memoizing
|
|
// approach to avoid calling it O(n*m) times in most cases?
|
|
for (var i = 0; i < list1.length; i++) {
|
|
for (var j = 0; j < list2.length; j++) {
|
|
var selection = select(list1[i], list2[j]);
|
|
selections[i][j] = selection;
|
|
lengths[i + 1][j + 1] = selection == null
|
|
? math.max(lengths[i + 1][j], lengths[i][j + 1])
|
|
: lengths[i][j] + 1;
|
|
}
|
|
}
|
|
|
|
List<T> backtrack(int i, int j) {
|
|
if (i == -1 || j == -1) return [];
|
|
var selection = selections[i][j];
|
|
if (selection != null) return backtrack(i - 1, j - 1)..add(selection);
|
|
|
|
return lengths[i + 1][j] > lengths[i][j + 1]
|
|
? backtrack(i, j - 1)
|
|
: backtrack(i - 1, j);
|
|
}
|
|
|
|
return backtrack(list1.length - 1, list2.length - 1);
|
|
}
|
|
|
|
/// Removes and returns the first value in [list] that matches [test].
|
|
///
|
|
/// By default, throws a [StateError] if no value matches. If [orElse] is
|
|
/// passed, its return value is used instead.
|
|
T removeFirstWhere<T>(List<T> list, bool test(T value), {T orElse()}) {
|
|
T toRemove;
|
|
for (var element in list) {
|
|
if (!test(element)) continue;
|
|
toRemove = element;
|
|
break;
|
|
}
|
|
|
|
if (toRemove == null) {
|
|
if (orElse != null) return orElse();
|
|
throw new StateError("No such element.");
|
|
} else {
|
|
list.remove(toRemove);
|
|
return toRemove;
|
|
}
|
|
}
|
|
|
|
/// Rotates the element in list from [start] (inclusive) to [end] (exclusive)
|
|
/// one index higher, looping the final element back to [start].
|
|
void rotateSlice(List list, int start, int end) {
|
|
var element = list[end - 1];
|
|
for (var i = start; i < end; i++) {
|
|
var next = list[i];
|
|
list[i] = element;
|
|
element = next;
|
|
}
|
|
}
|
|
|
|
/// Prints a warning to standard error, associated with [span].
|
|
///
|
|
/// If [color] is `true`, this uses terminal colors.
|
|
void warn(String message, FileSpan span, {bool color: false}) {
|
|
var warning = color ? '\u001b[33m\u001b[1mWarning\u001b[0m' : 'WARNING';
|
|
stderr.writeln("$warning on ${span.message("\n$message", color: color)}\n");
|
|
}
|