mirror of
https://github.com/danog/dart-sass.git
synced 2024-12-02 17:49:38 +01:00
7b4d26cc5a
Also warn about them.
272 lines
9.2 KiB
Dart
272 lines
9.2 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';
|
|
|
|
/// A simple [LinkedListEntry] that just provides a reference to the value.
|
|
class LinkedListValue<T> extends LinkedListEntry<LinkedListValue<T>> {
|
|
/// The value in the list.
|
|
final T value;
|
|
|
|
LinkedListValue(this.value);
|
|
}
|
|
|
|
/// 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, dynamic/*=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 dynamic/*=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()}) {
|
|
var/*=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;
|
|
}
|
|
}
|
|
|
|
/// 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");
|
|
}
|