Merge pull request #298 from fpringvaldsen/task/improve-indentation

Improve indentation
This commit is contained in:
Havoc Pennington 2015-04-01 21:08:24 -04:00
commit ea78c17381
9 changed files with 306 additions and 30 deletions

View File

@ -6,4 +6,9 @@ final class ConfigNodeArray extends ConfigNodeComplexValue {
ConfigNodeArray(Collection<AbstractConfigNode> children) {
super(children);
}
@Override
protected ConfigNodeArray newNode(Collection<AbstractConfigNode> nodes) {
return new ConfigNodeArray(nodes);
}
}

View File

@ -24,4 +24,29 @@ abstract class ConfigNodeComplexValue extends AbstractConfigNodeValue {
}
return tokens;
}
protected ConfigNodeComplexValue indentText(AbstractConfigNode indentation) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(children);
for (int i = 0; i < childrenCopy.size(); i++) {
AbstractConfigNode child = childrenCopy.get(i);
if (child instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) child).token())) {
childrenCopy.add(i + 1, indentation);
i++;
} else if (child instanceof ConfigNodeField) {
AbstractConfigNode value = ((ConfigNodeField) child).value();
if (value instanceof ConfigNodeComplexValue) {
childrenCopy.set(i, ((ConfigNodeField) child).replaceValue(((ConfigNodeComplexValue) value).indentText(indentation)));
}
} else if (child instanceof ConfigNodeComplexValue) {
childrenCopy.set(i, ((ConfigNodeComplexValue) child).indentText(indentation));
}
}
return newNode(childrenCopy);
}
// This method will just call into the object's constructor, but it's needed
// for use in the indentText() method so we can avoid a gross if/else statement
// checking the type of this
abstract ConfigNodeComplexValue newNode(Collection<AbstractConfigNode> nodes);
}

View File

@ -6,4 +6,9 @@ final class ConfigNodeConcatenation extends ConfigNodeComplexValue {
ConfigNodeConcatenation(Collection<AbstractConfigNode> children) {
super(children);
}
@Override
protected ConfigNodeConcatenation newNode(Collection<AbstractConfigNode> nodes) {
return new ConfigNodeConcatenation(nodes);
}
}

View File

@ -1,15 +1,30 @@
package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collection;
final class ConfigNodeInclude extends ConfigNodeComplexValue {
final class ConfigNodeInclude extends AbstractConfigNode {
final private ArrayList<AbstractConfigNode> children;
final private ConfigIncludeKind kind;
ConfigNodeInclude(Collection<AbstractConfigNode> children, ConfigIncludeKind kind) {
super(children);
this.children = new ArrayList<AbstractConfigNode>(children);
this.kind = kind;
}
final public Collection<AbstractConfigNode> children() {
return children;
}
@Override
protected Collection<Token> tokens() {
ArrayList<Token> tokens = new ArrayList();
for (AbstractConfigNode child : children) {
tokens.addAll(child.tokens());
}
return tokens;
}
protected ConfigIncludeKind kind() {
return kind;
}

View File

@ -10,6 +10,11 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
super(children);
}
@Override
protected ConfigNodeObject newNode(Collection<AbstractConfigNode> nodes) {
return new ConfigNodeObject(nodes);
}
public boolean hasValue(Path desiredPath) {
for (AbstractConfigNode node : children) {
if (node instanceof ConfigNodeField) {
@ -69,7 +74,15 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
}
} else if (key.equals(desiredPath)) {
seenNonMatching = true;
childrenCopy.set(i, node.replaceValue(value));
AbstractConfigNodeValue indentedValue;
AbstractConfigNode before = i - 1 > 0 ? childrenCopy.get(i - 1) : null;
if (value instanceof ConfigNodeComplexValue &&
before instanceof ConfigNodeSingleToken &&
Tokens.isIgnoredWhitespace(((ConfigNodeSingleToken) before).token()))
indentedValue = ((ConfigNodeComplexValue) value).indentText(before);
else
indentedValue = value;
childrenCopy.set(i, node.replaceValue(indentedValue));
valueCopy = null;
} else if (desiredPath.startsWith(key)) {
seenNonMatching = true;
@ -105,9 +118,65 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
return node;
}
private Collection<AbstractConfigNode> indentation() {
boolean seenNewLine = false;
ArrayList<AbstractConfigNode> indentation = new ArrayList<AbstractConfigNode>();
for (int i = 0; i < children.size(); i++) {
if (!seenNewLine) {
if (children.get(i) instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) children.get(i)).token())) {
seenNewLine = true;
indentation.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
}
} else {
if (children.get(i) instanceof ConfigNodeSingleToken &&
Tokens.isIgnoredWhitespace(((ConfigNodeSingleToken) children.get(i)).token()) &&
i + 1 < children.size() && (children.get(i+1) instanceof ConfigNodeField ||
children.get(i+1) instanceof ConfigNodeInclude)) {
// Return the indentation of the first setting on its own line
indentation.add(children.get(i));
return indentation;
}
}
}
if (indentation.isEmpty()) {
indentation.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
return indentation;
} else {
// Calculate the indentation of the ending curly-brace to get the indentation of the root object
AbstractConfigNode last = children.get(children.size() - 1);
if (last instanceof ConfigNodeSingleToken && ((ConfigNodeSingleToken) last).token() == Tokens.CLOSE_CURLY) {
AbstractConfigNode beforeLast = children.get(children.size() - 2);
if (beforeLast instanceof ConfigNodeSingleToken &&
Tokens.isIgnoredWhitespace(((ConfigNodeSingleToken) beforeLast).token())) {
String indent = ((ConfigNodeSingleToken) beforeLast).token().tokenText();
indent += " ";
indentation.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, indent)));
return indentation;
}
}
}
// The object has no curly braces and is at the root level, so don't indent
return indentation;
}
protected ConfigNodeObject addValueOnPath(ConfigNodePath desiredPath, AbstractConfigNodeValue value, ConfigSyntax flavor) {
Path path = desiredPath.value();
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList<AbstractConfigNode>(super.children);
ArrayList<AbstractConfigNode> indentation = new ArrayList<AbstractConfigNode>(indentation());
// If the value we're inserting is a complex value, we'll need to indent it for insertion
AbstractConfigNodeValue indentedValue;
if (value instanceof ConfigNodeComplexValue) {
indentedValue = ((ConfigNodeComplexValue) value).indentText(indentation.get(indentation.size() - 1));
} else {
indentedValue = value;
}
boolean sameLine = !(indentation.get(0) instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) indentation.get(0)).token()));
// If the path is of length greater than one, see if the value needs to be added further down
if (path.length() > 1) {
for (int i = super.children.size() - 1; i >= 0; i--) {
if (!(super.children.get(i) instanceof ConfigNodeField)) {
@ -123,43 +192,78 @@ final class ConfigNodeObject extends ConfigNodeComplexValue {
}
}
}
// Otherwise, construct the new setting
boolean startsWithBrace = super.children.get(0) instanceof ConfigNodeSingleToken &&
((ConfigNodeSingleToken) super.children.get(0)).token() == Tokens.OPEN_CURLY;
ArrayList<AbstractConfigNode> newNodes = new ArrayList<AbstractConfigNode>();
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
newNodes.addAll(indentation);
newNodes.add(desiredPath.first());
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newNodes.add(new ConfigNodeSingleToken(Tokens.COLON));
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
if (path.length() == 1) {
newNodes.add(value);
newNodes.add(indentedValue);
} else {
// If the path is of length greater than one add the required new objects along the path
ArrayList<AbstractConfigNode> newObjectNodes = new ArrayList<AbstractConfigNode>();
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.OPEN_CURLY));
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.CLOSE_CURLY));
ConfigNodeObject newObject = new ConfigNodeObject(newObjectNodes);
newNodes.add(newObject.addValueOnPath(desiredPath.subPath(1), value, flavor));
newNodes.add(newObject.addValueOnPath(desiredPath.subPath(1), indentedValue, flavor));
}
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
// Combine these two cases so that we only have to iterate once
if (flavor == ConfigSyntax.JSON || startsWithBrace) {
if (flavor == ConfigSyntax.JSON || startsWithBrace || sameLine) {
for (int i = childrenCopy.size() - 1; i >= 0; i--) {
// Valid JSON requires all key-value pairs except the last one to be succeeded by a comma,
// so we'll need to add a comma when adding a value
if (flavor == ConfigSyntax.JSON && childrenCopy.get(i) instanceof ConfigNodeField) {
// If we are in JSON or are adding a setting on the same line, we need to add a comma to the
// last setting
if ((flavor == ConfigSyntax.JSON || sameLine) && childrenCopy.get(i) instanceof ConfigNodeField) {
if (i+1 >= childrenCopy.size() ||
!(childrenCopy.get(i+1) instanceof ConfigNodeSingleToken
&& ((ConfigNodeSingleToken) childrenCopy.get(i+1)).token() == Tokens.COMMA))
childrenCopy.add(i+1, new ConfigNodeSingleToken(Tokens.COMMA));
break;
}
// Add the value into the copy of the children map, keeping any whitespace/newlines
// before the close curly brace
if (startsWithBrace && childrenCopy.get(i) instanceof ConfigNodeSingleToken &&
((ConfigNodeSingleToken) childrenCopy.get(i)).token == Tokens.CLOSE_CURLY) {
AbstractConfigNode previous = childrenCopy.get(i - 1);
if (previous instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) previous).token())) {
childrenCopy.add(i - 1, new ConfigNodeField(newNodes));
i--;
} else if (previous instanceof ConfigNodeSingleToken &&
Tokens.isIgnoredWhitespace(((ConfigNodeSingleToken) previous).token())) {
AbstractConfigNode beforePrevious = childrenCopy.get(i - 2);
if (sameLine) {
childrenCopy.add(i - 1, new ConfigNodeField(newNodes));
i--;
}
else if (beforePrevious instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) beforePrevious).token())) {
childrenCopy.add(i - 2, new ConfigNodeField(newNodes));
i -= 2;
} else {
childrenCopy.add(i, new ConfigNodeField(newNodes));
}
}
else
childrenCopy.add(i, new ConfigNodeField(newNodes));
}
}
}
if (!startsWithBrace) {
if (childrenCopy.get(childrenCopy.size() - 1) instanceof ConfigNodeSingleToken &&
Tokens.isNewline(((ConfigNodeSingleToken) childrenCopy.get(childrenCopy.size() - 1)).token()))
childrenCopy.add(childrenCopy.size() - 1, new ConfigNodeField(newNodes));
else
childrenCopy.add(new ConfigNodeField(newNodes));
}
return new ConfigNodeObject(childrenCopy);

View File

@ -15,6 +15,11 @@ final class ConfigNodeRoot extends ConfigNodeComplexValue {
this.origin = origin;
}
@Override
protected ConfigNodeRoot newNode(Collection<AbstractConfigNode> nodes) {
throw new ConfigException.BugOrBroken("Tried to indent the root object");
}
protected ConfigNodeComplexValue value() {
for (AbstractConfigNode node : children) {
if (node instanceof ConfigNodeComplexValue) {

View File

@ -270,7 +270,6 @@ final class ConfigParser {
valueNode = ((ConfigNodeField) node).value();
// comments from the key token go to the value token
//newValue = parseValue(valueToken.prepend(keyToken.comments));
newValue = parseValue(valueNode, comments);
if (((ConfigNodeField) node).separator() == Tokens.PLUS_EQUALS) {

View File

@ -148,14 +148,14 @@ class ConfigDocumentTest extends TestUtils {
origText = "{a.b.c: d}"
configDoc = ConfigDocumentFactory.parseString(origText)
assertEquals("{\na : 2\n}", configDoc.setValue("a", "2").render())
assertEquals("{ a : 2}", configDoc.setValue("a", "2").render())
}
@Test
def configDocumentSetNewValueBraceRoot {
val origText = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n}"
val finalTextConf = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n\n\"e\" : \"f\"\n}"
val finalTextJson = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\",\n\n\"e\" : \"f\"\n}"
val finalTextConf = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n\t\"e\" : \"f\"\n}"
val finalTextJson = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\",\n\t\"e\" : \"f\"\n}"
configDocumentReplaceConfTest(origText, finalTextConf, "\"f\"", "\"e\"")
configDocumentReplaceJsonTest(origText, finalTextJson, "\"f\"", "\"e\"")
}
@ -163,21 +163,21 @@ class ConfigDocumentTest extends TestUtils {
@Test
def configDocumentSetNewValueNoBraces {
val origText = "\"a\":\"b\",\n\"c\":\"d\"\n"
val finalText = "\"a\":\"b\",\n\"c\":\"d\"\n\n\"e\" : \"f\"\n"
val finalText = "\"a\":\"b\",\n\"c\":\"d\"\n\"e\" : \"f\"\n"
configDocumentReplaceConfTest(origText, finalText, "\"f\"", "\"e\"")
}
@Test
def configDocumentSetNewValueMultiLevelConf {
val origText = "a:b\nc:d"
val finalText = "a:b\nc:d\ne : {\nf : {\ng : 12\n}\n}\n"
val finalText = "a:b\nc:d\ne : { f : { g : 12 } }"
configDocumentReplaceConfTest(origText, finalText, "12", "e.f.g")
}
@Test
def configDocumentSetNewValueMultiLevelJson {
val origText = "{\"a\":\"b\",\n\"c\":\"d\"}"
val finalText = "{\"a\":\"b\",\n\"c\":\"d\",\n\"e\" : {\n\"f\" : {\n\"g\" : 12\n}\n}\n}"
val finalText = "{\"a\":\"b\",\n\"c\":\"d\",\n\"e\" : { \"f\" : { \"g\" : 12 } }}"
configDocumentReplaceJsonTest(origText, finalText, "12", "e.f.g")
}
@ -325,4 +325,122 @@ class ConfigDocumentTest extends TestUtils {
assertEquals(configDocumentFile.render(), configDocument.render())
}
@Test
def configDocumentIndentationSingleLineObject {
// Proper insertion for single-line objects
var origText = "a { b: c }"
var configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b: c, d : e }", configDocument.setValue("a.d", "e").render())
origText = "a { b: c }, d: e"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b: c }, d: e, f : g", configDocument.setValue("f", "g").render())
origText = "a { b: c }, d: e,"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b: c }, d: e, f : g", configDocument.setValue("f", "g").render())
assertEquals("a { b: c }, d: e, f : { g : { h : i } }", configDocument.setValue("f.g.h", "i").render())
origText = "{a { b: c }, d: e}"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("{a { b: c }, d: e, f : g}", configDocument.setValue("f", "g").render())
assertEquals("{a { b: c }, d: e, f : { g : { h : i } }}", configDocument.setValue("f.g.h", "i").render())
}
@Test
def configDocumentIndentationMultiLineObject {
var origText = "a {\n b: c\n}"
var configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b: c\n e : f\n}", configDocument.setValue("a.e", "f").render())
assertEquals("a {\n b: c\n d : { e : { f : g } }\n}", configDocument.setValue("a.d.e.f", "g").render())
origText = "a {\n b: c\n}\n"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b: c\n}\nd : e\n", configDocument.setValue("d", "e").render())
assertEquals("a {\n b: c\n}\nd : { e : { f : g } }\n", configDocument.setValue("d.e.f", "g").render())
}
@Test
def configDocumentIndentationNested {
var origText = "a { b { c { d: e } } }"
var configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b { c { d: e, f : g } } }", configDocument.setValue("a.b.c.f", "g").render())
origText = "a {\n b {\n c {\n d: e\n }\n }\n}"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b {\n c {\n d: e\n f : g\n }\n }\n}", configDocument.setValue("a.b.c.f", "g").render())
}
@Test
def configDocumentIndentationEmptyObject {
var origText = "a { }"
var configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b : c }", configDocument.setValue("a.b", "c").render())
origText = "a {\n b {\n }\n}"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b {\n c : d\n }\n}", configDocument.setValue("a.b.c", "d").render())
}
@Test
def configDocumentIndentationMultiLineValue {
val origText = "a {\n b {\n c {\n d: e\n }\n }\n}"
val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b {\n c {\n d: e\n f : {\n g: h\n i: j\n k: {\n l: m\n }\n }\n }\n }\n}",
configDocument.setValue("a.b.c.f", "{\n g: h\n i: j\n k: {\n l: m\n }\n}").render())
assertEquals("a {\n b {\n c {\n d: e\n f : 12 13 [1,\n 2,\n 3,\n {\n a:b\n }]\n }\n }\n}",
configDocument.setValue("a.b.c.f", "12 13 [1,\n2,\n3,\n{\n a:b\n}]").render())
}
@Test
def configDocumentIndentationMultiLineValueSingleLineObject {
// Weird indentation occurs when adding a multi-line value to a single-line object
val origText = "a { b { } }"
val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b { c : {\n c:d\n } } }", configDocument.setValue("a.b.c", "{\n c:d\n}").render())
}
@Test
def configDocumentIndentationSingleLineObjectContainingMultiLineValue {
val origText = "a { b {\n c: d\n} }"
val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a { b {\n c: d\n}, e : f }", configDocument.setValue("a.e", "f").render())
}
@Test
def configDocumentIndentationReplacingWithMultiLineValue {
var origText = "a {\n b {\n c : 22\n }\n}"
var configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b {\n c : {\n d:e\n }\n }\n}", configDocument.setValue("a.b.c", "{\n d:e\n}").render())
origText = "a {\n b {\n f : 10\n c : 22\n }\n}"
configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b {\n f : 10\n c : {\n d:e\n }\n }\n}", configDocument.setValue("a.b.c", "{\n d:e\n}").render())
}
@Test
def configDocumentIndentationValueWithIncludeTest {
val origText = "a {\n b {\n c : 22\n }\n}"
val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a {\n b {\n c : 22\n d : {\n include \"foo\"\n e:f\n }\n }\n}",
configDocument.setValue("a.b.d", "{\n include \"foo\"\n e:f\n}").render())
}
@Test
def configDocumentIndentationBadedOnIncludeNode {
val origText = "a : b\n include \"foo\"\n"
val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals("a : b\n include \"foo\"\n c : d\n", configDocument.setValue("c", "d").render())
}
}

View File

@ -61,7 +61,7 @@ class ConfigNodeTest extends TestUtils {
val node = configNodeObject(List(nodeKeyValuePair(configNodeKey("bar"), nodeInt(15))))
assertEquals("bar : 15", node.render())
val newNode = node.setValueOnPath("foo", value)
val finalText = "bar : 15\nfoo : " + value.render() + "\n"
val finalText = "bar : 15, foo : " + value.render()
assertEquals(finalText, newNode.render())
}
@ -195,24 +195,24 @@ class ConfigNodeTest extends TestUtils {
// Test that all features of node replacement in a map work in a complex map containing nested maps
val origText = "foo : bar\nbaz : {\n\t\"abc.def\" : 123\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" +
"def : \"this is a string\"\n\t\tghi : ${\"a.b\"}\n\t}\n}\nbaz.abc.ghi : 52\nbaz.abc.ghi : 53\n}"
val lowestLevelMap = configNodeObject(List(nodeOpenBrace, nodeLine(6),
nodeKeyValuePair(nodeWhitespace("\t\t"), configNodeKey("def"), configNodeSimpleValue(tokenString("this is a string")), nodeLine(7)),
nodeKeyValuePair(nodeWhitespace("\t\t"), configNodeKey("ghi"), configNodeSimpleValue(tokenKeySubstitution("a.b")), nodeLine(8)),
val lowestLevelMap = configNodeObject(List(nodeOpenBrace, nodeLine(6), nodeWhitespace("\t\t"),
nodeKeyValuePair(configNodeKey("def"), configNodeSimpleValue(tokenString("this is a string"))), nodeLine(7),
nodeWhitespace("\t\t"), nodeKeyValuePair(configNodeKey("ghi"), configNodeSimpleValue(tokenKeySubstitution("a.b"))), nodeLine(8),
nodeWhitespace("\t"), nodeCloseBrace))
val higherLevelMap = configNodeObject(List(nodeOpenBrace, nodeLine(2),
nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey("\"abc.def\""), configNodeSimpleValue(tokenInt(123)), nodeLine(3)),
nodeWhitespace("\t"), nodeKeyValuePair(configNodeKey("\"abc.def\""), configNodeSimpleValue(tokenInt(123))), nodeLine(3),
nodeWhitespace("\t"), configNodeBasic(tokenCommentDoubleSlash("This is a comment about the below setting")),
nodeLine(4), nodeLine(5),
nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey("abc"), lowestLevelMap, nodeLine(9)), nodeWhitespace(""),
nodeWhitespace("\t"), nodeKeyValuePair(configNodeKey("abc"), lowestLevelMap), nodeLine(9), nodeWhitespace(""),
nodeCloseBrace))
val origNode = configNodeObject(List(nodeKeyValuePair(configNodeKey("foo"), configNodeSimpleValue(tokenUnquoted("bar")), nodeLine(1)),
nodeKeyValuePair(configNodeKey("baz"), higherLevelMap, nodeLine(10)),
nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(52)), nodeLine(11)),
nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(53)), nodeLine(12)),
nodeKeyValuePair(configNodeKey("baz"), higherLevelMap), nodeLine(10),
nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(52))), nodeLine(11),
nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(53))), nodeLine(12),
nodeCloseBrace))
assertEquals(origText, origNode.render())
val finalText = "foo : bar\nbaz : {\n\t\"abc.def\" : true\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" +
"def : false\n\n\"this.does.not.exist@@@+$#\" : {\nend : doesnotexist\n}\n}\n}\nbaz.abc.ghi : randomunquotedString\n}"
"def : false\n\t\t\n\t\t\"this.does.not.exist@@@+$#\" : { end : doesnotexist }\n\t}\n}\n\nbaz.abc.ghi : randomunquotedString\n}"
//Can replace settings in nested maps
// Paths with quotes in the name are treated as a single Path, rather than multiple sub-paths