// 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 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 list = []; 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 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 a Map with [YamlNode] keys. Map get nodeMap => nodes.cast(); /// 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()) { 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 YamlNodeExtension on YamlNode { Object get valueOrThrow => value as Object; }