This changeset is motivated by a simple concept - get rid of the MiniYaml.Clone and MiniYamlNode.Clone methods to avoid deep copying yaml trees during merging. MiniYaml becoming immutable allows the merge function to reuse existing yaml trees rather than cloning them, saving on memory and improving merge performance. On initial loading the YAML for all maps is processed, so this provides a small reduction in initial loading time. The rest of the changeset is dealing with the change in the exposed API surface. Some With* helper methods are introduced to allow creating new YAML from existing YAML. Areas of code that generated small amounts of YAML are able to transition directly to the immutable model without too much ceremony. Some use cases are far less ergonomic even with these helper methods and so a MiniYamlBuilder is introduced to retain mutable creation functionality. This allows those areas to continue to use the old mutable structures. The main users are the update rules and linting capabilities.
680 lines
21 KiB
C#
680 lines
21 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* 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.Collections.Immutable;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using OpenRA.FileSystem;
|
|
|
|
namespace OpenRA
|
|
{
|
|
public static class MiniYamlExts
|
|
{
|
|
public static void WriteToFile(this IEnumerable<MiniYamlNode> y, string filename)
|
|
{
|
|
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
|
|
}
|
|
|
|
public static string WriteToString(this IEnumerable<MiniYamlNode> y)
|
|
{
|
|
// Remove all trailing newlines and restore the final EOF newline
|
|
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
|
|
}
|
|
|
|
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNode> y)
|
|
{
|
|
foreach (var kv in y)
|
|
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
|
|
yield return line;
|
|
}
|
|
|
|
public static void WriteToFile(this IEnumerable<MiniYamlNodeBuilder> y, string filename)
|
|
{
|
|
File.WriteAllLines(filename, y.ToLines().Select(x => x.TrimEnd()).ToArray());
|
|
}
|
|
|
|
public static string WriteToString(this IEnumerable<MiniYamlNodeBuilder> y)
|
|
{
|
|
// Remove all trailing newlines and restore the final EOF newline
|
|
return y.ToLines().JoinWith("\n").TrimEnd('\n') + "\n";
|
|
}
|
|
|
|
public static IEnumerable<string> ToLines(this IEnumerable<MiniYamlNodeBuilder> y)
|
|
{
|
|
foreach (var kv in y)
|
|
foreach (var line in kv.Value.ToLines(kv.Key, kv.Comment))
|
|
yield return line;
|
|
}
|
|
}
|
|
|
|
public sealed class MiniYamlNode
|
|
{
|
|
public readonly struct SourceLocation
|
|
{
|
|
public readonly string Filename;
|
|
public readonly int Line;
|
|
|
|
public SourceLocation(string filename, int line)
|
|
{
|
|
Filename = filename;
|
|
Line = line;
|
|
}
|
|
|
|
public override string ToString() { return $"{Filename}:{Line}"; }
|
|
}
|
|
|
|
public readonly SourceLocation Location;
|
|
public readonly string Key;
|
|
public readonly MiniYaml Value;
|
|
public readonly string Comment;
|
|
|
|
public MiniYamlNode WithValue(MiniYaml value)
|
|
{
|
|
if (Value == value)
|
|
return this;
|
|
return new MiniYamlNode(Key, value, Comment, Location);
|
|
}
|
|
|
|
public MiniYamlNode(string k, MiniYaml v, string c = null)
|
|
{
|
|
Key = k;
|
|
Value = v;
|
|
Comment = c;
|
|
}
|
|
|
|
public MiniYamlNode(string k, MiniYaml v, string c, SourceLocation loc)
|
|
: this(k, v, c)
|
|
{
|
|
Location = loc;
|
|
}
|
|
|
|
public MiniYamlNode(string k, string v, string c = null)
|
|
: this(k, new MiniYaml(v, Enumerable.Empty<MiniYamlNode>()), c) { }
|
|
|
|
public MiniYamlNode(string k, string v, IEnumerable<MiniYamlNode> n)
|
|
: this(k, new MiniYaml(v, n), null) { }
|
|
|
|
public override string ToString()
|
|
{
|
|
return $"{{YamlNode: {Key} @ {Location}}}";
|
|
}
|
|
}
|
|
|
|
public sealed class MiniYaml
|
|
{
|
|
const int SpacesPerLevel = 4;
|
|
static readonly Func<string, string> StringIdentity = s => s;
|
|
static readonly Func<MiniYaml, MiniYaml> MiniYamlIdentity = my => my;
|
|
|
|
public readonly string Value;
|
|
public readonly ImmutableArray<MiniYamlNode> Nodes;
|
|
|
|
public MiniYaml WithValue(string value)
|
|
{
|
|
if (Value == value)
|
|
return this;
|
|
return new MiniYaml(value, Nodes);
|
|
}
|
|
|
|
public MiniYaml WithNodes(IEnumerable<MiniYamlNode> nodes)
|
|
{
|
|
if (nodes is ImmutableArray<MiniYamlNode> n && Nodes == n)
|
|
return this;
|
|
return new MiniYaml(Value, nodes);
|
|
}
|
|
|
|
public MiniYaml WithNodesAppended(IEnumerable<MiniYamlNode> nodes)
|
|
{
|
|
var newNodes = Nodes.AddRange(nodes);
|
|
if (Nodes == newNodes)
|
|
return this;
|
|
return new MiniYaml(Value, newNodes);
|
|
}
|
|
|
|
public Dictionary<string, MiniYaml> ToDictionary()
|
|
{
|
|
return ToDictionary(MiniYamlIdentity);
|
|
}
|
|
|
|
public Dictionary<string, TElement> ToDictionary<TElement>(Func<MiniYaml, TElement> elementSelector)
|
|
{
|
|
return ToDictionary(StringIdentity, elementSelector);
|
|
}
|
|
|
|
public Dictionary<TKey, TElement> ToDictionary<TKey, TElement>(
|
|
Func<string, TKey> keySelector, Func<MiniYaml, TElement> elementSelector)
|
|
{
|
|
var ret = new Dictionary<TKey, TElement>(Nodes.Length);
|
|
foreach (var y in Nodes)
|
|
{
|
|
var key = keySelector(y.Key);
|
|
var element = elementSelector(y.Value);
|
|
if (!ret.TryAdd(key, element))
|
|
throw new InvalidDataException($"Duplicate key '{y.Key}' in {y.Location}");
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
public MiniYaml(string value)
|
|
: this(value, Enumerable.Empty<MiniYamlNode>()) { }
|
|
|
|
public MiniYaml(string value, IEnumerable<MiniYamlNode> nodes)
|
|
{
|
|
Value = value;
|
|
Nodes = ImmutableArray.CreateRange(nodes);
|
|
}
|
|
|
|
public static ImmutableArray<MiniYamlNode> NodesOrEmpty(MiniYaml y, string s)
|
|
{
|
|
return y.Nodes.FirstOrDefault(n => n.Key == s)?.Value.Nodes ?? ImmutableArray<MiniYamlNode>.Empty;
|
|
}
|
|
|
|
static List<MiniYamlNode> FromLines(IEnumerable<ReadOnlyMemory<char>> lines, string filename, bool discardCommentsAndWhitespace, Dictionary<string, string> stringPool)
|
|
{
|
|
stringPool ??= new Dictionary<string, string>();
|
|
|
|
var result = new List<List<MiniYamlNode>>
|
|
{
|
|
new List<MiniYamlNode>()
|
|
};
|
|
var parsedLines = new List<(int Level, string Key, string Value, string Comment, MiniYamlNode.SourceLocation Location)>();
|
|
|
|
var lineNo = 0;
|
|
foreach (var ll in lines)
|
|
{
|
|
var line = ll.Span;
|
|
++lineNo;
|
|
|
|
var keyStart = 0;
|
|
var level = 0;
|
|
var spaces = 0;
|
|
var textStart = false;
|
|
|
|
ReadOnlySpan<char> key = default;
|
|
ReadOnlySpan<char> value = default;
|
|
ReadOnlySpan<char> comment = default;
|
|
var location = new MiniYamlNode.SourceLocation(filename, lineNo);
|
|
|
|
if (line.Length > 0)
|
|
{
|
|
var currChar = line[keyStart];
|
|
|
|
while (!(currChar == '\n' || currChar == '\r') && keyStart < line.Length && !textStart)
|
|
{
|
|
currChar = line[keyStart];
|
|
switch (currChar)
|
|
{
|
|
case ' ':
|
|
spaces++;
|
|
if (spaces >= SpacesPerLevel)
|
|
{
|
|
spaces = 0;
|
|
level++;
|
|
}
|
|
|
|
keyStart++;
|
|
break;
|
|
case '\t':
|
|
level++;
|
|
keyStart++;
|
|
break;
|
|
default:
|
|
textStart = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (parsedLines.Count > 0 && parsedLines[^1].Level < level - 1)
|
|
throw new YamlException($"Bad indent in miniyaml at {location}");
|
|
|
|
// Extract key, value, comment from line as `<key>: <value>#<comment>`
|
|
// The # character is allowed in the value if escaped (\#).
|
|
// Leading and trailing whitespace is always trimmed from keys.
|
|
// Leading and trailing whitespace is trimmed from values unless they
|
|
// are marked with leading or trailing backslashes
|
|
var keyLength = line.Length - keyStart;
|
|
var valueStart = -1;
|
|
var valueLength = 0;
|
|
var commentStart = -1;
|
|
for (var i = 0; i < line.Length; i++)
|
|
{
|
|
if (valueStart < 0 && line[i] == ':')
|
|
{
|
|
valueStart = i + 1;
|
|
keyLength = i - keyStart;
|
|
valueLength = line.Length - i - 1;
|
|
}
|
|
|
|
if (commentStart < 0 && line[i] == '#' && (i == 0 || line[i - 1] != '\\'))
|
|
{
|
|
commentStart = i + 1;
|
|
if (commentStart <= keyLength)
|
|
keyLength = i - keyStart;
|
|
else
|
|
valueLength = i - valueStart;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (keyLength > 0)
|
|
key = line.Slice(keyStart, keyLength).Trim();
|
|
|
|
if (valueStart >= 0)
|
|
{
|
|
var trimmed = line.Slice(valueStart, valueLength).Trim();
|
|
if (trimmed.Length > 0)
|
|
value = trimmed;
|
|
}
|
|
|
|
if (commentStart >= 0 && !discardCommentsAndWhitespace)
|
|
comment = line[commentStart..];
|
|
|
|
if (value.Length > 1)
|
|
{
|
|
// Remove leading/trailing whitespace guards
|
|
var trimLeading = value[0] == '\\' && (value[1] == ' ' || value[1] == '\t') ? 1 : 0;
|
|
var trimTrailing = value[^1] == '\\' && (value[^2] == ' ' || value[^2] == '\t') ? 1 : 0;
|
|
if (trimLeading + trimTrailing > 0)
|
|
value = value.Slice(trimLeading, value.Length - trimLeading - trimTrailing);
|
|
|
|
// Remove escape characters from #
|
|
if (value.Contains("\\#", StringComparison.Ordinal))
|
|
value = value.ToString().Replace("\\#", "#");
|
|
}
|
|
}
|
|
|
|
if (!key.IsEmpty || !discardCommentsAndWhitespace)
|
|
{
|
|
while (parsedLines.Count > 0 && parsedLines[^1].Level > level)
|
|
BuildCompletedSubNode(level);
|
|
|
|
var keyString = key.IsEmpty ? null : key.ToString();
|
|
var valueString = value.IsEmpty ? null : value.ToString();
|
|
|
|
// Note: We need to support empty comments here to ensure that empty comments
|
|
// (i.e. a lone # at the end of a line) can be correctly re-serialized
|
|
var commentString = comment == default ? null : comment.ToString();
|
|
|
|
keyString = keyString == null ? null : stringPool.GetOrAdd(keyString, keyString);
|
|
valueString = valueString == null ? null : stringPool.GetOrAdd(valueString, valueString);
|
|
commentString = commentString == null ? null : stringPool.GetOrAdd(commentString, commentString);
|
|
|
|
parsedLines.Add((level, keyString, valueString, commentString, location));
|
|
}
|
|
}
|
|
|
|
if (parsedLines.Count > 0)
|
|
BuildCompletedSubNode(0);
|
|
|
|
return result[0];
|
|
|
|
void BuildCompletedSubNode(int level)
|
|
{
|
|
var lastLevel = parsedLines[^1].Level;
|
|
while (lastLevel >= result.Count)
|
|
result.Add(new List<MiniYamlNode>());
|
|
|
|
while (parsedLines.Count > 0 && parsedLines[^1].Level >= level)
|
|
{
|
|
var parent = parsedLines[^1];
|
|
var startOfRange = parsedLines.Count - 1;
|
|
while (startOfRange > 0 && parsedLines[startOfRange - 1].Level == parent.Level)
|
|
startOfRange--;
|
|
|
|
for (var i = startOfRange; i < parsedLines.Count - 1; i++)
|
|
{
|
|
var sibling = parsedLines[i];
|
|
result[parent.Level].Add(
|
|
new MiniYamlNode(sibling.Key, new MiniYaml(sibling.Value), sibling.Comment, sibling.Location));
|
|
}
|
|
|
|
var childNodes = parent.Level + 1 < result.Count ? result[parent.Level + 1] : null;
|
|
result[parent.Level].Add(new MiniYamlNode(
|
|
parent.Key,
|
|
new MiniYaml(parent.Value, childNodes ?? Enumerable.Empty<MiniYamlNode>()),
|
|
parent.Comment,
|
|
parent.Location));
|
|
childNodes?.Clear();
|
|
|
|
parsedLines.RemoveRange(startOfRange, parsedLines.Count - startOfRange);
|
|
}
|
|
}
|
|
}
|
|
|
|
public static List<MiniYamlNode> FromFile(string path, bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
|
{
|
|
return FromStream(File.OpenRead(path), path, discardCommentsAndWhitespace, stringPool);
|
|
}
|
|
|
|
public static List<MiniYamlNode> FromStream(Stream s, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
|
{
|
|
return FromLines(s.ReadAllLinesAsMemory(), fileName, discardCommentsAndWhitespace, stringPool);
|
|
}
|
|
|
|
public static List<MiniYamlNode> FromString(string text, string fileName = "<no filename available>", bool discardCommentsAndWhitespace = true, Dictionary<string, string> stringPool = null)
|
|
{
|
|
return FromLines(text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).Select(s => s.AsMemory()), fileName, discardCommentsAndWhitespace, stringPool);
|
|
}
|
|
|
|
public static List<MiniYamlNode> Merge(IEnumerable<IReadOnlyCollection<MiniYamlNode>> sources)
|
|
{
|
|
var sourcesList = sources.ToList();
|
|
if (sourcesList.Count == 0)
|
|
return new List<MiniYamlNode>();
|
|
|
|
var tree = sourcesList
|
|
.Where(s => s != null)
|
|
.Select(MergeSelfPartial)
|
|
.Aggregate(MergePartial)
|
|
.Where(n => n.Key != null)
|
|
.ToDictionary(n => n.Key, n => n.Value);
|
|
|
|
var resolved = new Dictionary<string, MiniYaml>(tree.Count);
|
|
foreach (var kv in tree)
|
|
{
|
|
// Inheritance is tracked from parent->child, but not from child->parentsiblings.
|
|
var inherited = ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty.Add(kv.Key, default);
|
|
var children = ResolveInherits(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)));
|
|
return ResolveInherits(nodes, tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation>.Empty);
|
|
}
|
|
|
|
static void MergeIntoResolved(MiniYamlNode overrideNode, List<MiniYamlNode> existingNodes, HashSet<string> existingNodeKeys,
|
|
Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
|
|
{
|
|
if (existingNodeKeys.Add(overrideNode.Key))
|
|
{
|
|
existingNodes.Add(overrideNode);
|
|
return;
|
|
}
|
|
|
|
var existingNodeIndex = IndexOfKey(existingNodes, overrideNode.Key);
|
|
var existingNode = existingNodes[existingNodeIndex];
|
|
var value = MergePartial(existingNode.Value, overrideNode.Value);
|
|
var nodes = ResolveInherits(value, tree, inherited);
|
|
if (!value.Nodes.SequenceEqual(nodes))
|
|
value = value.WithNodes(nodes);
|
|
existingNodes[existingNodeIndex] = existingNode.WithValue(value);
|
|
}
|
|
|
|
static List<MiniYamlNode> ResolveInherits(MiniYaml node, Dictionary<string, MiniYaml> tree, ImmutableDictionary<string, MiniYamlNode.SourceLocation> inherited)
|
|
{
|
|
var resolved = new List<MiniYamlNode>(node.Nodes.Length);
|
|
var resolvedKeys = new HashSet<string>(node.Nodes.Length);
|
|
|
|
foreach (var n in node.Nodes)
|
|
{
|
|
if (n.Key == "Inherits" || n.Key.StartsWith("Inherits@", StringComparison.Ordinal))
|
|
{
|
|
if (!tree.TryGetValue(n.Value.Value, out var parent))
|
|
throw new YamlException(
|
|
$"{n.Location}: Parent type `{n.Value.Value}` not found");
|
|
|
|
try
|
|
{
|
|
inherited = inherited.Add(n.Value.Value, n.Location);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
throw new YamlException($"{n.Location}: Parent type `{n.Value.Value}` was already inherited by this yaml tree at {inherited[n.Value.Value]} (note: may be from a derived tree)");
|
|
}
|
|
|
|
foreach (var r in ResolveInherits(parent, tree, inherited))
|
|
MergeIntoResolved(r, resolved, resolvedKeys, tree, inherited);
|
|
}
|
|
else if (n.Key.StartsWith("-", StringComparison.Ordinal))
|
|
{
|
|
var removed = n.Key[1..];
|
|
if (resolved.RemoveAll(r => r.Key == removed) == 0)
|
|
throw new YamlException($"{n.Location}: There are no elements with key `{removed}` to remove");
|
|
resolvedKeys.Remove(removed);
|
|
}
|
|
else
|
|
MergeIntoResolved(n, resolved, resolvedKeys, tree, inherited);
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges any duplicate keys that are defined within the same set of nodes.
|
|
/// Does not resolve inheritance or node removals.
|
|
/// </summary>
|
|
static IReadOnlyCollection<MiniYamlNode> MergeSelfPartial(IReadOnlyCollection<MiniYamlNode> existingNodes)
|
|
{
|
|
var keys = new HashSet<string>(existingNodes.Count);
|
|
var ret = new List<MiniYamlNode>(existingNodes.Count);
|
|
foreach (var n in existingNodes)
|
|
{
|
|
if (keys.Add(n.Key))
|
|
ret.Add(n);
|
|
else
|
|
{
|
|
// Node with the same key has already been added: merge new node over the existing one
|
|
var originalIndex = IndexOfKey(ret, n.Key);
|
|
var original = ret[originalIndex];
|
|
ret[originalIndex] = original.WithValue(MergePartial(original.Value, n.Value));
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static MiniYaml MergePartial(MiniYaml existingNodes, MiniYaml overrideNodes)
|
|
{
|
|
existingNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
|
|
overrideNodes?.Nodes.ToDictionaryWithConflictLog(x => x.Key, "MiniYaml.Merge", null, x => $"{x.Key} (at {x.Location})");
|
|
|
|
if (existingNodes == null)
|
|
return overrideNodes;
|
|
|
|
if (overrideNodes == null)
|
|
return existingNodes;
|
|
|
|
return new MiniYaml(overrideNodes.Value ?? existingNodes.Value, MergePartial(existingNodes.Nodes, overrideNodes.Nodes));
|
|
}
|
|
|
|
static IReadOnlyCollection<MiniYamlNode> MergePartial(IReadOnlyCollection<MiniYamlNode> existingNodes, IReadOnlyCollection<MiniYamlNode> overrideNodes)
|
|
{
|
|
if (existingNodes.Count == 0)
|
|
return overrideNodes;
|
|
|
|
if (overrideNodes.Count == 0)
|
|
return existingNodes;
|
|
|
|
var ret = new List<MiniYamlNode>(existingNodes.Count + overrideNodes.Count);
|
|
var plainKeys = new HashSet<string>(existingNodes.Count + overrideNodes.Count);
|
|
|
|
foreach (var node in existingNodes)
|
|
MergeNode(node);
|
|
foreach (var node in overrideNodes)
|
|
MergeNode(node);
|
|
|
|
void MergeNode(MiniYamlNode node)
|
|
{
|
|
// Append Removal nodes to the result.
|
|
// Therefore: we know the remainder of the method deals with a plain node.
|
|
if (node.Key.StartsWith("-", StringComparison.Ordinal))
|
|
{
|
|
ret.Add(node);
|
|
return;
|
|
}
|
|
|
|
// If no previous node with this key is present, it is new and can just be appended.
|
|
if (plainKeys.Add(node.Key))
|
|
{
|
|
ret.Add(node);
|
|
return;
|
|
}
|
|
|
|
// A Removal node is closer than the previous node.
|
|
// We should not merge the new node, as the data being merged will jump before the Removal.
|
|
// Instead, append it so the previous node is applied, then removed, then the new node is applied.
|
|
var previousNodeIndex = LastIndexOfKey(ret, node.Key);
|
|
var previousRemovalNodeIndex = LastIndexOfKey(ret, $"-{node.Key}");
|
|
if (previousRemovalNodeIndex != -1 && previousRemovalNodeIndex > previousNodeIndex)
|
|
{
|
|
ret.Add(node);
|
|
return;
|
|
}
|
|
|
|
// A previous node is present with no intervening Removal.
|
|
// We should merge the new one into it, in place.
|
|
ret[previousNodeIndex] = node.WithValue(MergePartial(ret[previousNodeIndex].Value, node.Value));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int IndexOfKey(List<MiniYamlNode> nodes, string key)
|
|
{
|
|
// PERF: Avoid LINQ.
|
|
for (var i = 0; i < nodes.Count; i++)
|
|
if (nodes[i].Key == key)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
static int LastIndexOfKey(List<MiniYamlNode> nodes, string key)
|
|
{
|
|
// PERF: Avoid LINQ.
|
|
for (var i = nodes.Count - 1; i >= 0; i--)
|
|
if (nodes[i].Key == key)
|
|
return i;
|
|
return -1;
|
|
}
|
|
|
|
public IEnumerable<string> ToLines(string key, string comment = null)
|
|
{
|
|
var hasKey = !string.IsNullOrEmpty(key);
|
|
var hasValue = !string.IsNullOrEmpty(Value);
|
|
var hasComment = comment != null;
|
|
yield return (hasKey ? key + ":" : "")
|
|
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
|
|
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
|
|
|
|
if (Nodes != null)
|
|
foreach (var line in Nodes.ToLines())
|
|
yield return "\t" + line;
|
|
}
|
|
|
|
public static List<MiniYamlNode> Load(IReadOnlyFileSystem fileSystem, IEnumerable<string> files, MiniYaml mapRules)
|
|
{
|
|
if (mapRules != null && mapRules.Value != null)
|
|
{
|
|
var mapFiles = FieldLoader.GetValue<string[]>("value", mapRules.Value);
|
|
files = files.Append(mapFiles);
|
|
}
|
|
|
|
IEnumerable<IReadOnlyCollection<MiniYamlNode>> yaml = files.Select(s => FromStream(fileSystem.Open(s), s));
|
|
if (mapRules != null && mapRules.Nodes.Length > 0)
|
|
yaml = yaml.Append(mapRules.Nodes);
|
|
|
|
return Merge(yaml);
|
|
}
|
|
}
|
|
|
|
public sealed class MiniYamlNodeBuilder
|
|
{
|
|
public MiniYamlNode.SourceLocation Location;
|
|
public string Key;
|
|
public MiniYamlBuilder Value;
|
|
public string Comment;
|
|
|
|
public MiniYamlNodeBuilder(MiniYamlNode node)
|
|
{
|
|
Location = node.Location;
|
|
Key = node.Key;
|
|
Value = new MiniYamlBuilder(node.Value);
|
|
Comment = node.Comment;
|
|
}
|
|
|
|
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c = null)
|
|
{
|
|
Key = k;
|
|
Value = v;
|
|
Comment = c;
|
|
}
|
|
|
|
public MiniYamlNodeBuilder(string k, MiniYamlBuilder v, string c, MiniYamlNode.SourceLocation loc)
|
|
: this(k, v, c)
|
|
{
|
|
Location = loc;
|
|
}
|
|
|
|
public MiniYamlNodeBuilder(string k, string v, string c = null)
|
|
: this(k, new MiniYamlBuilder(v, null), c) { }
|
|
|
|
public MiniYamlNodeBuilder(string k, string v, List<MiniYamlNode> n)
|
|
: this(k, new MiniYamlBuilder(v, n), null) { }
|
|
|
|
public MiniYamlNode Build()
|
|
{
|
|
return new MiniYamlNode(Key, Value.Build(), Comment, Location);
|
|
}
|
|
}
|
|
|
|
public sealed class MiniYamlBuilder
|
|
{
|
|
public string Value;
|
|
public List<MiniYamlNodeBuilder> Nodes;
|
|
|
|
public MiniYamlBuilder(MiniYaml yaml)
|
|
{
|
|
Value = yaml.Value;
|
|
Nodes = yaml.Nodes.Select(n => new MiniYamlNodeBuilder(n)).ToList();
|
|
}
|
|
|
|
public MiniYamlBuilder(string value)
|
|
: this(value, null) { }
|
|
|
|
public MiniYamlBuilder(string value, List<MiniYamlNode> nodes)
|
|
{
|
|
Value = value;
|
|
Nodes = nodes == null ? new List<MiniYamlNodeBuilder>() : nodes.Select(x => new MiniYamlNodeBuilder(x)).ToList();
|
|
}
|
|
|
|
public MiniYaml Build()
|
|
{
|
|
return new MiniYaml(Value, Nodes.Select(n => n.Build()));
|
|
}
|
|
|
|
public IEnumerable<string> ToLines(string key, string comment = null)
|
|
{
|
|
var hasKey = !string.IsNullOrEmpty(key);
|
|
var hasValue = !string.IsNullOrEmpty(Value);
|
|
var hasComment = comment != null;
|
|
yield return (hasKey ? key + ":" : "")
|
|
+ (hasValue ? " " + Value.Replace("#", "\\#") : "")
|
|
+ (hasComment ? (hasKey || hasValue ? " " : "") + "#" + comment : "");
|
|
|
|
if (Nodes != null)
|
|
foreach (var line in Nodes.ToLines())
|
|
yield return "\t" + line;
|
|
}
|
|
}
|
|
|
|
[Serializable]
|
|
public class YamlException : Exception
|
|
{
|
|
public YamlException(string s)
|
|
: base(s) { }
|
|
}
|
|
}
|