Skip to content

Commit

Permalink
XML: Properly indent child elements
Browse files Browse the repository at this point in the history
Includes test case
  • Loading branch information
SLaks committed Dec 30, 2013
1 parent 635fbd8 commit 16dbc15
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 3 deletions.
30 changes: 30 additions & 0 deletions Rebracer.Tests/UtilitiesTets/XmlMergerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,36 @@ public void ReorderingNewElementsGetNewLines() {
</C>");
}

[TestMethod]
public void ReplacedChildElementsPreserveIndentation() {
// Note two tabs before each element
var source = @"<C>
<a b=""c"">
<x1>Hi!</x1>
<x2>
<content>Bye!</content>
</x2>
</a>
<d />
</C>";
var container = XElement.Parse(source, LoadOptions.PreserveWhitespace);
var newSource = MergeElements(container,
new XElement("a", new XAttribute("b", "c"),
new XElement("x1", "Hi!"),
new XElement("x2", new XElement("NewContent", "Bye!"))
)
);
container.ToString().Should().Be(@"<C>
<a b=""c"">
<x1>Hi!</x1>
<x2>
<NewContent>Bye!</NewContent>
</x2>
</a>
<d />
</C>");
}

[TestMethod]
public void ReorderingElementsPreservesComments() {
// Note two tabs before each element
Expand Down
51 changes: 48 additions & 3 deletions Rebracer/Utilities/XmlMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static bool MergeElements(this XElement container, IEnumerable<XElement>
if (!changed && !XNode.DeepEquals(o, newItems[newIndex].Value))
changed = true;
o.ReplaceWith(newItems[newIndex].Value);
IndentChildren(newItems[newIndex].Value);
newIndex++;
}

Expand Down Expand Up @@ -92,15 +93,16 @@ public static bool MergeElements(this XElement container, IEnumerable<XElement>
///<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
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.
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) {
///<summary>Gets the XText node containing the whitespace used to indent this element. If there is no preceding whitespace, the following whitespace will be returned, if any.</summary>
private static XText GetPrecedingWhitespace(this XElement element) {
// If we started from an empty parent, there is no known whitespace
if (element == null)
return null;
Expand All @@ -111,5 +113,48 @@ private static XNode GetPrecedingWhitespace(this XElement element) {
return null;
return new XText(sample);
}

///<summary>Indents the children of an inserted element, based on the indentation of the parent element.</summary>
public static void IndentChildren(this XElement parent) {
var parentPrefix = parent.GetPrecedingWhitespace();
if (parentPrefix == null) // No known indent to start from
return;

// Copy the child list before we start mutating.
// Looking at XContainer source code, this isn't
// strictly necessary.
var childElements = parent.Elements().ToList();
if (childElements.Count == 0)
return;
parent.Add(parentPrefix); // Indent the closing tag.

var childPrefix = GetChildPrefix(parent.Depth(), parentPrefix.Value);
if (string.IsNullOrEmpty(childPrefix))
return;

foreach (var child in childElements) {
child.AddBeforeSelf(childPrefix);
IndentChildren(child);
}
}

public static int Depth(this XElement element) {
if (element.Parent == null)
return 0;
return element.Parent.Depth() + 1;
}

private static string GetChildPrefix(int parentDepth, string parentPrefix) {
string newLine = parentPrefix.Substring(0, parentPrefix.TakeWhile(c => c == '\r' || c == '\n').Count());
parentPrefix = parentPrefix.Substring(newLine.Length);

string levelIndent;
if (parentDepth == 0)
return newLine; // Don't know how to indent
else
levelIndent = parentPrefix.Substring(0, parentPrefix.Length / parentDepth);

return newLine + string.Concat(Enumerable.Repeat(levelIndent, parentDepth + 1));
}
}
}

0 comments on commit 16dbc15

Please sign in to comment.