blob: 5550c7509851c69d87c1a7a9c94b749959085eef [file] [log] [blame]
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
import 'editor.dart';
/// Determines if [string] is dangerous by checking if parsing the plain string can
/// return a result different from [string].
///
/// This function is also capable of detecting if non-printable characters are in
/// [string].
bool isDangerousString(String string) {
try {
if (loadYamlNode(string).value != string) {
return true;
}
/// [string] should also not contain the `[`, `]`, `,`, `{` and `}` indicator characters.
return string.contains(RegExp(r'\{|\[|\]|\}|,'));
} catch (e) {
/// This catch statement catches [ArgumentError] in `loadYamlNode` when
/// a string can be interpreted as a URI tag, but catches for other
/// [YamlException]s
return true;
}
}
/// Asserts that [value] is a valid scalar according to YAML.
///
/// A valid scalar is a number, String, boolean, or null.
void assertValidScalar(Object? value) {
if (value is num || value is String || value is bool || value == null) {
return;
}
throw ArgumentError.value(value, 'value', 'Not a valid scalar type!');
}
/// Checks if [node] is a [YamlNode] with block styling.
///
/// [ScalarStyle.ANY] and [CollectionStyle.ANY] are considered to be block styling
/// by default for maximum flexibility.
bool isBlockNode(YamlNode node) {
if (node is YamlScalar) {
if (node.style == ScalarStyle.LITERAL ||
node.style == ScalarStyle.FOLDED ||
node.style == ScalarStyle.ANY) {
return true;
}
}
if (node is YamlList &&
(node.style == CollectionStyle.BLOCK ||
node.style == CollectionStyle.ANY)) return true;
if (node is YamlMap &&
(node.style == CollectionStyle.BLOCK ||
node.style == CollectionStyle.ANY)) return true;
return false;
}
/// Returns the content sensitive ending offset of [yamlNode] (i.e. where the last
/// meaningful content happens)
int getContentSensitiveEnd(YamlNode yamlNode) {
if (yamlNode is YamlList) {
if (yamlNode.style == CollectionStyle.FLOW) {
return yamlNode.span.end.offset;
} else {
return getContentSensitiveEnd(yamlNode.nodes.last);
}
} else if (yamlNode is YamlMap) {
if (yamlNode.style == CollectionStyle.FLOW) {
return yamlNode.span.end.offset;
} else {
return getContentSensitiveEnd(yamlNode.nodes.values.last);
}
}
return yamlNode.span.end.offset;
}
/// Checks if the item is a Map or a List
bool isCollection(Object item) => item is Map || item is List;
/// Checks if [index] is [int], >=0, < [length]
bool isValidIndex(Object? index, int length) {
return index is int && index >= 0 && index < length;
}
/// Checks if the item is empty, if it is a List or a Map.
///
/// Returns `false` if [item] is not a List or Map.
bool isEmpty(Object item) {
if (item is Map) return item.isEmpty;
if (item is List) return item.isEmpty;
return false;
}
/// Creates a [SourceSpan] from [sourceUrl] with no meaningful location
/// information.
///
/// Mainly used with [wrapAsYamlNode] to allow for a reasonable
/// implementation of [SourceSpan.message].
SourceSpan shellSpan(Object? sourceUrl) {
final shellSourceLocation = SourceLocation(0, sourceUrl: sourceUrl);
return SourceSpanBase(shellSourceLocation, shellSourceLocation, '');
}
/// Returns if [value] is a [YamlList] or [YamlMap] with [CollectionStyle.FLOW].
bool isFlowYamlCollectionNode(Object value) {
if (value is YamlList || value is YamlMap) {
return (value as dynamic).style == CollectionStyle.FLOW;
}
return false;
}
/// Determines the index where [newKey] will be inserted if the keys in [map] are in
/// alphabetical order when converted to strings.
///
/// Returns the length of [map] if the keys in [map] are not in alphabetical order.
int getMapInsertionIndex(YamlMap map, Object newKey) {
final keys = map.nodes.keys.map((k) => k.toString()).toList();
for (var i = 1; i < keys.length; i++) {
if (keys[i].compareTo(keys[i - 1]) < 0) {
return map.length;
}
}
final insertionIndex =
keys.indexWhere((key) => key.compareTo(newKey as String) > 0);
if (insertionIndex != -1) return insertionIndex;
return map.length;
}
/// Returns the [style] property of [target], if it is a [YamlNode]. Otherwise return null.
Object? getStyle(Object target) {
if (target is YamlNode) {
return (target as dynamic).style;
}
return null;
}
/// Returns the detected indentation step used in [yaml], or
/// defaults to a value of `2` if no indentation step can be detected.
///
/// Indentation step is determined by the difference in indentation of the
/// first block-styled yaml collection in the second level as compared to the
/// top-level elements. In the case where there are multiple possible
/// candidates, we choose the candidate closest to the start of [yaml].
int getIndentation(YamlEditor editor) {
final node = editor.parseAt([]);
Iterable<YamlNode>? children;
var indentation = 2;
if (node is YamlMap && node.style == CollectionStyle.BLOCK) {
children = node.nodes.values;
} else if (node is YamlList && node.style == CollectionStyle.BLOCK) {
children = node.nodes;
}
if (children != null) {
for (final child in children) {
var indent = 0;
if (child is YamlList) {
indent = getListIndentation(editor.toString(), child);
} else if (child is YamlMap) {
indent = getMapIndentation(editor.toString(), child);
}
if (indent != 0) indentation = indent;
}
}
return indentation;
}
/// Gets the indentation level of [list]. This is 0 if it is a flow list,
/// but returns the number of spaces before the hyphen of elements for
/// block lists.
///
/// Throws [UnsupportedError] if an empty block map is passed in.
int getListIndentation(String yaml, YamlList list) {
if (list.style == CollectionStyle.FLOW) return 0;
/// An empty block map doesn't really exist.
if (list.isEmpty) {
throw UnsupportedError('Unable to get indentation for empty block list');
}
final lastSpanOffset = list.nodes.last.span.start.offset;
final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset - 1);
final lastHyphen = yaml.lastIndexOf('-', lastSpanOffset - 1);
if (lastNewLine == -1) return lastHyphen;
return lastHyphen - lastNewLine - 1;
}
/// Gets the indentation level of [map]. This is 0 if it is a flow map,
/// but returns the number of spaces before the keys for block maps.
int getMapIndentation(String yaml, YamlMap map) {
if (map.style == CollectionStyle.FLOW) return 0;
/// An empty block map doesn't really exist.
if (map.isEmpty) {
throw UnsupportedError('Unable to get indentation for empty block map');
}
/// Use the number of spaces between the last key and the newline as
/// indentation.
final lastKey = map.nodes.keys.last as YamlNode;
final lastSpanOffset = lastKey.span.start.offset;
final lastNewLine = yaml.lastIndexOf('\n', lastSpanOffset);
final lastQuestionMark = yaml.lastIndexOf('?', lastSpanOffset);
if (lastQuestionMark == -1) {
if (lastNewLine == -1) return lastSpanOffset;
return lastSpanOffset - lastNewLine - 1;
}
/// If there is a question mark, it might be a complex key. Check if it
/// is on the same line as the key node to verify.
if (lastNewLine == -1) return lastQuestionMark;
if (lastQuestionMark > lastNewLine) {
return lastQuestionMark - lastNewLine - 1;
}
return lastSpanOffset - lastNewLine - 1;
}
/// Returns the detected line ending used in [yaml], more specifically, whether
/// [yaml] appears to use Windows `\r\n` or Unix `\n` line endings.
///
/// The heuristic used is to count all `\n` in the text and if stricly more
/// than half of them are preceded by `\r` we report that windows line endings
/// are used.
String getLineEnding(String yaml) {
var index = -1;
var unixNewlines = 0;
var windowsNewlines = 0;
while ((index = yaml.indexOf('\n', index + 1)) != -1) {
if (index != 0 && yaml[index - 1] == '\r') {
windowsNewlines++;
} else {
unixNewlines++;
}
}
return windowsNewlines > unixNewlines ? '\r\n' : '\n';
}