Skip to content

Commit

Permalink
XML: Preserve existing comments, in order
Browse files Browse the repository at this point in the history
When sorting, keep each comment
together with its following element
  • Loading branch information
SLaks committed Dec 26, 2013
1 parent 63dd9b2 commit 635fbd8
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 12 deletions.
25 changes: 25 additions & 0 deletions Rebracer.Tests/UtilitiesTets/XmlMergerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,31 @@ public void ReorderingNewElementsGetNewLines() {
<e />
</C>");
}

[TestMethod]
public void ReorderingElementsPreservesComments() {
// Note two tabs before each element
var source = @"<C>
<!-- Comment on D -->
<d />
<!-- Comment on B -->
<b />
<!-- Comment on end -->
</C>";
var container = XElement.Parse(source, LoadOptions.PreserveWhitespace);
var newSource = MergeElements(container, new XElement("a"), new XElement("c"), new XElement("e"), new XElement("f"));
container.ToString().Should().Be(@"<C>
<a />
<!-- Comment on B -->
<b />
<c />
<!-- Comment on D -->
<d />
<e />
<f />
<!-- Comment on end -->
</C>");
}
[TestMethod]
public void ReplacedElementsPreserveWhitespace() {
// Note two tabs before each element
Expand Down
42 changes: 30 additions & 12 deletions Rebracer/Utilities/XmlMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,14 @@ public static bool MergeElements(this XElement container, IEnumerable<XElement>
while (newIndex < newItems.Count && StringComparer.Ordinal.Compare(newItems[newIndex].Key, thisKey) < 0) {
changed = true;
XElement newNode = newItems[newIndex].Value;
o.AddBeforeSelf(newNode);
// Insert the requisite whitespace between the two nodes
newNode.AddAfterSelf(newNode.GetPrecedingWhitespace());

// Insert the new element before any comments or
// whitespace that precede this element, and add
// the requisite separator whitespace before it.
var precedingTrivia = o.GetPrecedingTrivia().FirstOrDefault();

(precedingTrivia ?? o).AddBeforeSelf(newNode);
newNode.AddBeforeSelf(newNode.GetPrecedingWhitespace());
newIndex++;
}

Expand All @@ -51,14 +56,14 @@ public static bool MergeElements(this XElement container, IEnumerable<XElement>
}

// If the container is not already sorted, sort it,
// then try again. Preserve any whitespace between
// elements, as well as any trailing whitespace for
// the parent's closing tag.
// then try again. Preserve whitespace and comments
// between existing elements, as well as before the
// parent's closing tag.
if (StringComparer.Ordinal.Compare(thisKey, lastKey) < 0) {
container.ReplaceNodes(
oldItems.OrderBy(nameSelector)
.SelectMany(elem => new XNode[] { elem.PreviousNode as XText, elem }),
container.LastNode as XText
.SelectMany(elem => new object[] { elem.GetPrecedingTrivia(), elem }),
container.Elements().Last().NodesAfterSelf()
);
MergeElements(container, newElements, nameSelector);
// Because we sorted the existing elements, we certainly changed something
Expand All @@ -71,15 +76,28 @@ container.LastNode as XText
changed = true;

// Add any new items that go after the last item.
// Add these nodes before any trailing whitespace
var inserter = container.LastNode is XText
? container.LastNode.AddBeforeSelf : new NodeInserter(container.Add);
// Add these nodes immediately following the last
// element, before trailing whitespace & comments
var lastElement = container.Elements().LastOrDefault();
// Use AddBeforeSelf() to preserve ordering.
var inserter = lastElement != null && lastElement.NextNode != null
? lastElement.NextNode.AddBeforeSelf : new NodeInserter(container.Add);

var separatingWhitespace = container.Elements().LastOrDefault().GetPrecedingWhitespace();
var separatingWhitespace = lastElement.GetPrecedingWhitespace();
for (; newIndex < newItems.Count; newIndex++)
inserter(separatingWhitespace, newItems[newIndex].Value);
return changed;
}

///<summary>Gets all whitespace and comment nodes before the specified element, until its preceding element.</summary>
static IEnumerable<XNode> GetPrecedingTrivia(this XElement element) {
var lastElem = element.ElementsBeforeSelf().LastOrDefault();
if (lastElem == null) // If it's the first element, take all preceding nodes
return element.NodesBeforeSelf();
else // Otherwise, take all nodes after the prior element.
return lastElem.NodesAfterSelf().TakeWhile(n => n != element);
}

///<summary>A method to insert nodes into a LINQ to XML document.</summary>
delegate void NodeInserter(params object[] content);
private static XNode GetPrecedingWhitespace(this XElement element) {
Expand Down

0 comments on commit 635fbd8

Please sign in to comment.