| // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'package:source_span/source_span.dart'; | |
| import 'package:yaml/src/event.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| bool _contains(YamlList l1, YamlNode n2) { | |
| for (YamlNode n1 in l1.nodes) { | |
| if (n1.value == n2.value) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /// Merges two maps (of yaml) with simple override semantics, suitable for | |
| /// merging two maps where one map defines default values that are added to | |
| /// (and possibly overridden) by an overriding map. | |
| class Merger { | |
| /// Merges a default [o1] with an overriding object [o2]. | |
| /// | |
| /// * lists are merged (without duplicates). | |
| /// * lists of scalar values can be promoted to simple maps when merged with | |
| /// maps of strings to booleans (e.g., ['opt1', 'opt2'] becomes | |
| /// {'opt1': true, 'opt2': true}. | |
| /// * maps are merged recursively. | |
| /// * if map values cannot be merged, the overriding value is taken. | |
| /// | |
| YamlNode merge(YamlNode o1, YamlNode o2) { | |
| // Handle promotion first. | |
| YamlMap listToMap(YamlList list) { | |
| // We use the default linked hash map so the ordering is the same every time. | |
| Map<YamlNode, YamlNode> map = {}; | |
| ScalarEvent event = ScalarEvent( | |
| o1.span as FileSpan, | |
| 'true', | |
| ScalarStyle.PLAIN, | |
| ); | |
| for (var element in list.nodes) { | |
| map[element] = YamlScalar.internal(true, event); | |
| } | |
| return YamlMap.internal(map, o1.span, CollectionStyle.BLOCK); | |
| } | |
| if (_isListOfString(o1) && _isMapToBools(o2)) { | |
| o1 = listToMap(o1 as YamlList); | |
| } else if (_isMapToBools(o1) && _isListOfString(o2)) { | |
| o2 = listToMap(o2 as YamlList); | |
| } | |
| if (o1 is YamlMap && o2 is YamlMap) { | |
| return mergeMap(o1, o2); | |
| } | |
| if (o1 is YamlList && o2 is YamlList) { | |
| return mergeList(o1, o2); | |
| } | |
| // Default to override, unless the overriding value is `null`. | |
| if (o2 is YamlScalar && o2.value == null) { | |
| return o1; | |
| } | |
| return o2; | |
| } | |
| /// Merge lists, avoiding duplicates. | |
| YamlList mergeList(YamlList l1, YamlList l2) { | |
| List<YamlNode> list = <YamlNode>[]; | |
| list.addAll(l1.nodes); | |
| for (YamlNode n2 in l2.nodes) { | |
| if (!_contains(l1, n2)) { | |
| list.add(n2); | |
| } | |
| } | |
| return YamlList.internal(list, l1.span, CollectionStyle.BLOCK); | |
| } | |
| /// Merge maps (recursively). | |
| YamlMap mergeMap(YamlMap m1, YamlMap m2) { | |
| // We use the default linked hash map so the ordering is the same every time. | |
| Map<YamlNode, YamlNode> merged = {}; | |
| m1.nodeMap.forEach((k, v) { | |
| merged[k] = v; | |
| }); | |
| m2.nodeMap.forEach((k, v) { | |
| var value = k.value; | |
| var mergedKey = | |
| merged.keys.firstWhere((key) => key.value == value, orElse: () => k) | |
| as YamlScalar; | |
| var o1 = merged[mergedKey]; | |
| if (o1 != null) { | |
| merged[mergedKey] = merge(o1, v); | |
| } else { | |
| merged[mergedKey] = v; | |
| } | |
| }); | |
| return YamlMap.internal(merged, m1.span, CollectionStyle.BLOCK); | |
| } | |
| static bool _isListOfString(Object? o) => | |
| o is YamlList && | |
| o.nodes.every((e) => e is YamlScalar && e.value is String); | |
| static bool _isMapToBools(Object? o) => | |
| o is YamlMap && | |
| o.nodes.values.every((v) => v is YamlScalar && v.value is bool); | |
| } | |
| extension YamlMapExtensions on YamlMap { | |
| /// Return [nodes] as entries with [YamlNode] keys and values. | |
| Iterable<({YamlNode keyNode, YamlNode valueNode})> get nodeEntries sync* { | |
| for (var MapEntry(:key, :value) in nodeMap.entries) { | |
| yield (keyNode: key, valueNode: value); | |
| } | |
| } | |
| /// Return [nodes] as a Map with [YamlNode] keys. | |
| Map<YamlNode, YamlNode> get nodeMap => nodes.cast<YamlNode, YamlNode>(); | |
| /// Return the [YamlNode] associated with the given [key], or `null` if there | |
| /// is no matching key. | |
| YamlNode? getKey(String key) { | |
| for (var k in nodes.keys.cast<YamlNode>()) { | |
| if (k is YamlScalar && k.value == key) { | |
| return k; | |
| } | |
| } | |
| return null; | |
| } | |
| /// Return the [YamlNode] representing the key that corresponds to the value | |
| /// represented by the [value] node. | |
| YamlNode? keyAtValue(YamlNode value) { | |
| for (var entry in nodes.entries) { | |
| if (entry.value == value) { | |
| return entry.key as YamlNode?; | |
| } | |
| } | |
| return null; | |
| } | |
| /// Return the value associated with the key whose value matches the given | |
| /// [key], or `null` if there is no matching key. | |
| YamlNode? valueAt(String key) { | |
| for (var keyNode in nodes.keys) { | |
| if (keyNode is YamlScalar && keyNode.value == key) { | |
| return nodes[keyNode]; | |
| } | |
| } | |
| return null; | |
| } | |
| } | |
| extension YamlScalarExtension on YamlScalar { | |
| /// Returns the value as a bool, or `null` if it cannot be converted. | |
| bool? toBool() { | |
| switch (value) { | |
| case bool value: | |
| return value; | |
| case String value: | |
| var map = const {'true': true, 'false': false}; | |
| return map[value.toLowerCase()]; | |
| } | |
| return null; | |
| } | |
| /// Returns the value in lower case, or `null` if it is not a string. | |
| String? toLowerCase() { | |
| Object? value = this.value; | |
| return value is String ? value.toLowerCase() : null; | |
| } | |
| /// Returns the value in upper case, or `null` if it is not a string. | |
| String? toUpperCase() { | |
| Object? value = this.value; | |
| return value is String ? value.toUpperCase() : null; | |
| } | |
| } |