#region Copyright & License Information /* * Copyright 2007-2017 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; using OpenRA.FileSystem; namespace OpenRA { using MiniYamlNodes = List; public static class MiniYamlExts { public static void WriteToFile(this MiniYamlNodes y, string filename) { File.WriteAllLines(filename, y.ToLines(true).Select(x => x.TrimEnd()).ToArray()); } public static string WriteToString(this MiniYamlNodes y) { return y.ToLines(true).Select(x => x.TrimEnd()).JoinWith("\n"); } public static IEnumerable ToLines(this MiniYamlNodes y, bool lowest) { foreach (var kv in y) { foreach (var line in kv.Value.ToLines(kv.Key)) yield return line; if (lowest) yield return ""; } } } public class MiniYamlNode { public struct SourceLocation { public string Filename; public int Line; public override string ToString() { return "{0}:{1}".F(Filename, Line); } } public SourceLocation Location; public string Key; public MiniYaml Value; public MiniYamlNode(string k, MiniYaml v) { Key = k; Value = v; } public MiniYamlNode(string k, MiniYaml v, SourceLocation loc) : this(k, v) { Location = loc; } public MiniYamlNode(string k, string v) : this(k, v, null) { } public MiniYamlNode(string k, string v, List n) : this(k, new MiniYaml(v, n)) { } public MiniYamlNode(string k, string v, List n, SourceLocation loc) : this(k, new MiniYaml(v, n), loc) { } public override string ToString() { return "{{YamlNode: {0} @ {1}}}".F(Key, Location); } public MiniYamlNode Clone() { return new MiniYamlNode(Key, Value.Clone()); } } public class MiniYaml { const int SpacesPerLevel = 4; static readonly Func StringIdentity = s => s; static readonly Func MiniYamlIdentity = my => my; public string Value; public List Nodes; public MiniYaml Clone() { return new MiniYaml(Value, Nodes.Select(n => n.Clone()).ToList()); } public Dictionary ToDictionary() { return ToDictionary(MiniYamlIdentity); } public Dictionary ToDictionary(Func elementSelector) { return ToDictionary(StringIdentity, elementSelector); } public Dictionary ToDictionary( Func keySelector, Func elementSelector) { var ret = new Dictionary(); foreach (var y in Nodes) { var key = keySelector(y.Key); var element = elementSelector(y.Value); try { ret.Add(key, element); } catch (ArgumentException ex) { throw new InvalidDataException("Duplicate key '{0}' in {1}".F(y.Key, y.Location), ex); } } return ret; } public MiniYaml(string value) : this(value, null) { } public MiniYaml(string value, List nodes) { Value = value; Nodes = nodes ?? new List(); } public static List NodesOrEmpty(MiniYaml y, string s) { var nd = y.ToDictionary(); return nd.ContainsKey(s) ? nd[s].Nodes : new List(); } static List FromLines(IEnumerable lines, string filename) { var levels = new List>(); levels.Add(new List()); var lineNo = 0; foreach (var ll in lines) { var line = ll; ++lineNo; var commentIndex = line.IndexOf('#'); if (commentIndex != -1) line = line.Substring(0, commentIndex).TrimEnd(' ', '\t'); if (line.Length == 0) continue; var charPosition = 0; var level = 0; var spaces = 0; var textStart = false; var currChar = line[charPosition]; while (!(currChar == '\n' || currChar == '\r') && charPosition < line.Length && !textStart) { currChar = line[charPosition]; switch (currChar) { case ' ': spaces++; if (spaces >= SpacesPerLevel) { spaces = 0; level++; } charPosition++; break; case '\t': level++; charPosition++; break; default: textStart = true; break; } } var realText = line.Substring(charPosition); if (realText.Length == 0) continue; var location = new MiniYamlNode.SourceLocation { Filename = filename, Line = lineNo }; if (levels.Count <= level) throw new YamlException("Bad indent in miniyaml at {0}".F(location)); while (levels.Count > level + 1) levels.RemoveAt(levels.Count - 1); var d = new List(); var rhs = SplitAtColon(ref realText); levels[level].Add(new MiniYamlNode(realText, rhs, d, location)); levels.Add(d); } return levels[0]; } static string SplitAtColon(ref string realText) { var colon = realText.IndexOf(':'); if (colon == -1) return null; var ret = realText.Substring(colon + 1).Trim(); if (ret.Length == 0) ret = null; realText = realText.Substring(0, colon).Trim(); return ret; } public static Dictionary DictFromFile(string path) { return FromFile(path).ToDictionary(x => x.Key, x => x.Value); } public static Dictionary DictFromStream(Stream stream, string fileName = "") { return FromStream(stream, fileName).ToDictionary(x => x.Key, x => x.Value); } public static List FromFile(string path) { return FromLines(File.ReadAllLines(path), path); } public static List FromStream(Stream s, string fileName = "") { using (var reader = new StreamReader(s)) return FromString(reader.ReadToEnd(), fileName); } public static List FromString(string text, string fileName = "") { return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None), fileName); } public static List Merge(IEnumerable> sources) { if (!sources.Any()) return new List(); var tree = sources.Where(s => s != null).Aggregate(MergePartial) .ToDictionary(n => n.Key, n => n.Value); var resolved = new Dictionary(); foreach (var kv in tree) { var inherited = new Dictionary(); inherited.Add(kv.Key, new MiniYamlNode.SourceLocation()); var children = ResolveInherits(kv.Key, kv.Value, tree, inherited); resolved.Add(kv.Key, new MiniYaml(kv.Value.Value, children)); } // Resolve any top-level removals (e.g. removing whole actor blocks) var nodes = new MiniYaml("", resolved.Select(kv => new MiniYamlNode(kv.Key, kv.Value)).ToList()); return ResolveInherits("", nodes, tree, new Dictionary()); } static void MergeIntoResolved(MiniYamlNode overrideNode, List existingNodes, Dictionary tree, Dictionary inherited) { var existingNode = existingNodes.FirstOrDefault(n => n.Key == overrideNode.Key); if (existingNode != null) { existingNode.Value = MiniYaml.MergePartial(existingNode.Value, overrideNode.Value); existingNode.Value.Nodes = ResolveInherits(existingNode.Key, existingNode.Value, tree, inherited); } else existingNodes.Add(overrideNode.Clone()); } static List ResolveInherits(string key, MiniYaml node, Dictionary tree, Dictionary inherited) { var resolved = new List(); // Inheritance is tracked from parent->child, but not from child->parentsiblings. inherited = new Dictionary(inherited); foreach (var n in node.Nodes) { if (n.Key == "Inherits" || n.Key.StartsWith("Inherits@", StringComparison.Ordinal)) { MiniYaml parent; if (!tree.TryGetValue(n.Value.Value, out parent)) throw new YamlException( "{0}: Parent type `{1}` not found".F(n.Location, n.Value.Value)); if (inherited.ContainsKey(n.Value.Value)) throw new YamlException("{0}: Parent type `{1}` was already inherited by this yaml tree at {2} (note: may be from a derived tree)" .F(n.Location, n.Value.Value, inherited[n.Value.Value])); inherited.Add(n.Value.Value, n.Location); foreach (var r in ResolveInherits(n.Key, parent, tree, inherited)) MergeIntoResolved(r, resolved, tree, inherited); } else if (n.Key.StartsWith("-", StringComparison.Ordinal)) { var removed = n.Key.Substring(1); if (resolved.RemoveAll(r => r.Key == removed) == 0) throw new YamlException("{0}: There are no elements with key `{1}` to remove".F(n.Location, removed)); } else MergeIntoResolved(n, resolved, tree, inherited); } return resolved; } static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes) { if (existingNodes == null) return overrideNodes; if (overrideNodes == null) return existingNodes; return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes)); } static List MergePartial(List existingNodes, List overrideNodes) { if (existingNodes.Count == 0) return overrideNodes; if (overrideNodes.Count == 0) return existingNodes; var ret = new List(); var existingDict = existingNodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => "{0} (at {1})".F(x.Key, x.Location)); var overrideDict = overrideNodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => "{0} (at {1})".F(x.Key, x.Location)); var allKeys = existingDict.Keys.Union(overrideDict.Keys); foreach (var key in allKeys) { MiniYamlNode existingNode, overrideNode; existingDict.TryGetValue(key, out existingNode); overrideDict.TryGetValue(key, out overrideNode); var loc = overrideNode == null ? default(MiniYamlNode.SourceLocation) : overrideNode.Location; var merged = (existingNode == null || overrideNode == null) ? overrideNode ?? existingNode : new MiniYamlNode(key, MergePartial(existingNode.Value, overrideNode.Value), loc); ret.Add(merged); } return ret; } public IEnumerable ToLines(string name) { yield return name + ": " + Value; if (Nodes != null) foreach (var line in Nodes.ToLines(false)) yield return "\t" + line; } public static List Load(IReadOnlyFileSystem fileSystem, IEnumerable files, MiniYaml mapRules) { if (mapRules != null && mapRules.Value != null) { var mapFiles = FieldLoader.GetValue("value", mapRules.Value); files = files.Append(mapFiles); } var yaml = files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s)); if (mapRules != null && mapRules.Nodes.Any()) yaml = yaml.Append(mapRules.Nodes); return MiniYaml.Merge(yaml); } } [Serializable] public class YamlException : Exception { public YamlException(string s) : base(s) { } } }