#
symbol at the
+ * start of the comment, followed by the beginning of each new line.
+ */
+ HASH,
+ /**
+ * The C style line comment, indicated by placing a //
at the
+ * start of the comment, followed by the beginning of each new line.
+ */
+ LINE,
+ /**
+ * A block style comment, indicated by placing a /*
at the
+ * start of the comment, followed by a * /
(with no spaces) at
+ * the very end of the comment.
+ */
+ BLOCK
+}
diff --git a/src/main/org/hjson/CommentType.java b/src/main/org/hjson/CommentType.java
new file mode 100644
index 0000000..de6d53f
--- /dev/null
+++ b/src/main/org/hjson/CommentType.java
@@ -0,0 +1,26 @@
+package org.hjson;
+
+/**
+ * This class represents the specific type of comment to be used in conjunction with a {@link JsonValue}.
+ * Comments can be placed by calling {@link JsonValue#setComment(CommentType, CommentStyle, String)}
+ * or another such variant.
+ */
+public enum CommentType {
+ /**
+ * Indicates that this comment precedes the value that it is paired with, to be placed one line
+ * before the value. In the case of parent or root objects, this type indicates that the comment
+ * is a header inside of the json file.
+ */
+ BOL,
+ /**
+ * Indicates that this comment follows the value that it is paired with, to be placed at the end
+ * of the line. In the case of parent or root objects, this type indicates that the comment is a
+ * footer inside of the json file.
+ */
+ EOL,
+ /**
+ * Indicates that this comment falls anywhere else in association with this value. This usually
+ * implies that the value is inside of an empty object or array.
+ */
+ INTERIOR
+}
diff --git a/src/main/org/hjson/HjsonDsf.java b/src/main/org/hjson/HjsonDsf.java
index 9c704ba..1885f6b 100644
--- a/src/main/org/hjson/HjsonDsf.java
+++ b/src/main/org/hjson/HjsonDsf.java
@@ -21,8 +21,6 @@
******************************************************************************/
package org.hjson;
-import java.util.*;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
@@ -44,11 +42,6 @@ private HjsonDsf() {}
* @return DSF provider
*/
public static IHjsonDsfProvider hex(boolean stringify) { return new DsfHex(stringify); }
- // /**
- // * Returns a date DSF provider.
- // * @return DSF provider
- // */
- // public static IHjsonDsfProvider date() { return new DsfDate(); }
static boolean isInvalidDsfChar(char c)
{
diff --git a/src/main/org/hjson/HjsonOptions.java b/src/main/org/hjson/HjsonOptions.java
index b4e10a5..a1e74a3 100644
--- a/src/main/org/hjson/HjsonOptions.java
+++ b/src/main/org/hjson/HjsonOptions.java
@@ -27,11 +27,28 @@
public class HjsonOptions {
private IHjsonDsfProvider[] dsf;
+ private boolean outputComments;
+ private boolean outputEmptyLines;
private boolean legacyRoot;
+ private boolean bracesSameLine;
+ private boolean allowCondense;
+ private boolean allowMultiVal;
+ private boolean emitRootBraces;
+ private String space, commentSpace;
+ private String newLine;
public HjsonOptions() {
dsf=new IHjsonDsfProvider[0];
legacyRoot=true;
+ bracesSameLine=false;
+ allowCondense=true;
+ allowMultiVal=true;
+ emitRootBraces=false;
+ space=" ";
+ commentSpace="";
+ outputComments=false;
+ outputEmptyLines=false;
+ newLine=JsonValue.eol;
}
/**
@@ -39,45 +56,250 @@ public HjsonOptions() {
*
* @return providers.
*/
- public IHjsonDsfProvider[] getDsfProviders() { return dsf.clone(); }
+ public IHjsonDsfProvider[] getDsfProviders() {
+ return dsf.clone();
+ }
/**
* Sets the DSF providers.
*
* @param value value
+ * @return this, to enable chaining
*/
- public void setDsfProviders(IHjsonDsfProvider[] value) { dsf=value.clone(); }
+ public HjsonOptions setDsfProviders(IHjsonDsfProvider... value) {
+ dsf = value.clone();
+ return this;
+ }
/**
* Detects whether objects without root braces are supported.
*
* @return true
if this feature is enabled.
*/
- public boolean getParseLegacyRoot() { return legacyRoot; }
+ public boolean getParseLegacyRoot() {
+ return legacyRoot;
+ }
/**
* Sets whether root braces should be emitted.
*
* @param value value
+ * @return this, to enable chaining
*/
- public void setParseLegacyRoot(boolean value) { legacyRoot=value; }
+ public HjsonOptions setParseLegacyRoot(boolean value) {
+ legacyRoot = value;
+ return this;
+ }
/**
* Detects whether root braces should be emitted.
*
- * @deprecated will always return true.
* @return true
if this feature is enabled.
*/
- @Deprecated
- public boolean getEmitRootBraces() { return true; }
+ public boolean getEmitRootBraces() {
+ return emitRootBraces;
+ }
/**
* Sets whether root braces should be emitted.
*
- * @deprecated root braces are always emitted.
* @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setEmitRootBraces(boolean value) {
+ emitRootBraces = value;
+ return this;
+ }
+
+ /**
+ * Detects whether braces and brackets should be placed on new lines.
+ *
+ * @return whether braces and brackets follow the KR / Java syntax.
+ */
+ public boolean bracesSameLine() { return bracesSameLine; }
+
+ /**
+ * Sets whether braces and brackets should be placed on new lines.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setBracesSameLine(boolean value) {
+ bracesSameLine = value;
+ return this;
+ }
+
+ /**
+ * Detects whether more than one value is ever allowed on a single line.
+ *
+ * @return true
if more than one value is allowed.
+ */
+ public boolean getAllowMultiVal() {
+ return allowMultiVal;
+ }
+
+ /**
+ * Sets whether more than one value is ever allowed to be placed on a single line.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setAllowMultiVal(boolean value) {
+ allowMultiVal = value;
+ return this;
+ }
+
+ /**
+ * Detects whether objects an arrays are allowed to be displayed on a single line.
+ *
+ * @return true
if objects and arrays can be displayed on a single line.
+ */
+ public boolean getAllowCondense() {
+ return allowCondense;
+ }
+
+ /**
+ * Sets whether objects and arrays can be displayed on a single line.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setAllowCondense(boolean value) {
+ allowCondense = value;
+ return this;
+ }
+
+ /**
+ * Gets the characters to be placed per-level on each new line.
+ *
+ * @return the number of spaces.
+ */
+ public String getSpace() { return space; }
+
+ /**
+ * Sets the characters to be placed per-level on each new line.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setSpace(String value) {
+ space = value;
+ return this;
+ }
+
+ /**
+ * Sets the number of spaces to be placed per-level on each new line.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setSpace(int value) {
+ space = numSpaces(value);
+ return this;
+ }
+
+ /**
+ * Gets the characters to be placed before comments on new lines.
+ *
+ * @return the number of spaces
+ */
+ public String getCommentSpace() {
+ return commentSpace;
+ }
+
+ /**
+ * Sets the characters to be placed before comments on new lines.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setCommentSpace(String value) {
+ commentSpace = value;
+ return this;
+ }
+
+ /**
+ * Gets the new line character(s) to be output by the writer.
+ *
+ * @return the new line character(s)
+ */
+ public String getNewLine() {
+ return this.newLine;
+ }
+
+ /**
+ * Sets the new line character(s) to be output by the writer.
+ *
+ * @param nl The new line characters to be used
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setNewLine(String nl) {
+ if ("\n".equals(nl) || "\r\n".equals(nl)) newLine = nl;
+ return this;
+ }
+
+ /**
+ * Sets the number of spaces to be placed before comments on new lines.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setCommentSpace(int value) {
+ commentSpace = numSpaces(value);
+ return this;
+ }
+
+ /**
+ * Generates a String object based on the input number of spaces.
+ *
+ * @param value value
+ * @return a string containing the input number of spaces.
+ */
+ private String numSpaces(int value) {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < value; i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Sets whether comments are enabled on the output.
+ *
+ * @param value value
+ * @return this, to enable chaining
*/
- @Deprecated
- public void setEmitRootBraces(boolean value) { }
+ public HjsonOptions setOutputComments(boolean value) {
+ outputComments = value;
+ return this;
+ }
+ /**
+ * Gets whether comments are enabled on the output.
+ *
+ * @return whether comments are enabled
+ */
+ public boolean getOutputComments() {
+ return outputComments;
+ }
+
+ /**
+ * Sets whether comments are enabled on the output.
+ *
+ * @param value value
+ * @return this, to enable chaining
+ */
+ public HjsonOptions setOutputEmptyLines(boolean value) {
+ outputEmptyLines = value;
+ return this;
+ }
+
+ /**
+ * Gets whether comments are enabled on the output.
+ *
+ * @return whether comments are enabled
+ */
+ public boolean getOutputEmptyLines() {
+ return outputEmptyLines;
+ }
}
diff --git a/src/main/org/hjson/HjsonParser.java b/src/main/org/hjson/HjsonParser.java
index fec6336..d13ee06 100644
--- a/src/main/org/hjson/HjsonParser.java
+++ b/src/main/org/hjson/HjsonParser.java
@@ -1,4 +1,5 @@
-/*******************************************************************************
+/*
+ ******************************************************************************
* Copyright (c) 2013, 2015 EclipseSource.
* Copyright (c) 2015-2017 Christian Zangl
*
@@ -24,11 +25,9 @@
import java.io.*;
+// Todo: Rewrite
class HjsonParser {
- private static final int MIN_BUFFER_SIZE=10;
- private static final int DEFAULT_BUFFER_SIZE=1024;
-
private final String buffer;
private Reader reader;
private int index;
@@ -37,9 +36,8 @@ class HjsonParser {
private int current;
private StringBuilder captureBuffer, peek;
private boolean capture;
- private boolean legacyRoot;
-
- private IHjsonDsfProvider[] dsfProviders;
+ private final boolean legacyRoot;
+ private final IHjsonDsfProvider[] dsfProviders;
HjsonParser(String string, HjsonOptions options) {
buffer=string;
@@ -57,7 +55,7 @@ class HjsonParser {
this(readToEnd(reader), options);
}
- static String readToEnd(Reader reader) throws IOException {
+ private static String readToEnd(Reader reader) throws IOException {
// read everything into a buffer
int n;
char[] part=new char[8*1024];
@@ -66,21 +64,27 @@ static String readToEnd(Reader reader) throws IOException {
return sb.toString();
}
- void reset() {
+ private void reset() {
index=lineOffset=current=0;
line=1;
peek=new StringBuilder();
- reader=new StringReader(buffer);
+ // Don't lose the final character.
+ reader=new StringReader(buffer + ' ');
capture=false;
captureBuffer=null;
}
JsonValue parse() throws IOException {
- // Braces for the root object are optional
-
read();
- skipWhiteSpace();
+ String header=readBetweenVals();
+ JsonValue value = tryParse();
+ value.setFullComment(CommentType.BOL, header);
+ return value;
+ }
+
+ private JsonValue tryParse() throws IOException {
+ // Braces for the root object are optional
if (legacyRoot) {
switch (current) {
case '[':
@@ -89,15 +93,18 @@ JsonValue parse() throws IOException {
default:
try {
// assume we have a root object without braces
- return checkTrailing(readObject(true));
+ return checkTrailing(readObject(false));
} catch (Exception exception) {
// test if we are dealing with a single JSON value instead (true/false/null/num/"")
reset();
read();
- skipWhiteSpace();
- try { return checkTrailing(readValue()); }
- catch (Exception exception2) { }
- throw exception; // throw original error
+ readBetweenVals();
+ try {
+ return checkTrailing(readValue());
+ } catch (Exception exception2) {
+ // throw original error
+ throw exception;
+ }
}
}
} else {
@@ -105,8 +112,8 @@ JsonValue parse() throws IOException {
}
}
- JsonValue checkTrailing(JsonValue v) throws ParseException, IOException {
- skipWhiteSpace();
+ private JsonValue checkTrailing(JsonValue v) throws ParseException, IOException {
+ v.setFullComment(CommentType.EOL, readBetweenVals());
if (!isEndOfText()) throw error("Extra characters in input: "+current);
return v;
}
@@ -116,7 +123,7 @@ private JsonValue readValue() throws IOException {
case '\'':
case '"': return readString();
case '[': return readArray();
- case '{': return readObject(false);
+ case '{': return readObject(true);
default: return readTfnns();
}
}
@@ -133,18 +140,18 @@ private JsonValue readTfnns() throws IOException {
read();
boolean isEol=current<0 || current=='\r' || current=='\n';
if (isEol || current==',' ||
- current=='}' || current==']' ||
- current=='#' ||
- current=='/' && (peek()=='/' || peek()=='*')
- ) {
+ current=='}' || current==']' ||
+ current=='#' ||
+ current=='/' && (peek()=='/' || peek()=='*')
+ ) {
switch (first) {
case 'f':
case 'n':
case 't':
String svalue=value.toString().trim();
- if (svalue.equals("false")) return JsonValue.FALSE;
- else if (svalue.equals("null")) return JsonValue.NULL;
- else if (svalue.equals("true")) return JsonValue.TRUE;
+ if (svalue.equals("false")) return JsonLiteral.jsonFalse();
+ else if (svalue.equals("null")) return JsonLiteral.jsonNull();
+ else if (svalue.equals("true")) return JsonLiteral.jsonTrue();
break;
default:
if (first=='-' || first>='0' && first<='9') {
@@ -161,46 +168,129 @@ private JsonValue readTfnns() throws IOException {
}
}
+ private JsonObject readObject(boolean expectCloser) throws IOException {
+ // Skip the opening brace.
+ if (expectCloser) read();
+ JsonObject object=new JsonObject();
+ boolean compact=isContainerCompact(expectCloser);
+ ContainerData data=new ContainerData(compact);
+
+ while (true) {
+ // Comment above / before name.
+ String bol=readBetweenVals();
+
+ if (checkEndOfContainer("object", '}', expectCloser)) {
+ // Because we reached the end, we learned
+ // that this was an interior comment.
+ object.setFullComment(CommentType.INTERIOR, bol);
+ break;
+ }
+ // Name comes next.
+ String name=readName();
+
+ // Colon and potential surrounding spaces.
+ // Comments will be fully ignored, here.
+ readBetweenVals();
+ if (!readIf(':')) {
+ throw expected("':'");
+ }
+ readBetweenVals();
+
+ // The value itself.
+ JsonValue value=readValue();
+
+ finishContainerElement(data, value);
+ // Set comments and add.
+ value.setFullComment(CommentType.BOL, bol);
+ object.add(name, value);
+ }
+ return data.into(object);
+ }
+
private JsonArray readArray() throws IOException {
- read();
+ read(); // Clear the opening bracket.
JsonArray array=new JsonArray();
- skipWhiteSpace();
- if (readIf(']')) {
- return array;
- }
+ boolean compact=isContainerCompact(true);
+ ContainerData data=new ContainerData(compact);
+
while (true) {
- skipWhiteSpace();
- array.add(readValue());
- skipWhiteSpace();
- if (readIf(',')) skipWhiteSpace(); // , is optional
- if (readIf(']')) break;
- else if (isEndOfText()) throw error("End of input while parsing an array (did you forget a closing ']'?)");
+ // Any comments above / before value.
+ String bol=readBetweenVals();
+
+ if (checkEndOfContainer("array", ']', true)) {
+ // Because we reached the end, we learned
+ // that this was an interior comment.
+ array.setFullComment(CommentType.INTERIOR, bol);
+ break;
+ }
+ // The value must be next.
+ JsonValue value=readValue();
+ value.setFullComment(CommentType.BOL, bol);
+
+ finishContainerElement(data, value);
+ // Successfully parsed a value.
+ array.add(value);
}
- return array;
+ return data.into(array);
}
- private JsonObject readObject(boolean objectWithoutBraces) throws IOException {
- if (!objectWithoutBraces) read();
- JsonObject object=new JsonObject();
- skipWhiteSpace();
- while (true) {
- if (objectWithoutBraces) {
- if (isEndOfText()) break;
- } else {
- if (isEndOfText()) throw error("End of input while parsing an object (did you forget a closing '}'?)");
- if (readIf('}')) break;
+ private void finishContainerElement(ContainerData data, JsonValue value) throws IOException {
+ int delimiter, numCommas=0;
+ while ((delimiter=readNextDelimiter())==',') {
+ skipToNL();
+ numCommas++;
+ }
+ // We should now be at the eol.
+ if (delimiter=='\n') { // Reached eol.
+ data.nl(); // Update the data to reflect this.
+ // Check for a comma on the next line.
+ // Also skip empty lines.
+ while ((delimiter=readNextDelimiter())>0) {
+ if (delimiter==',') {
+ data.overrideCondensed(); // Can no longer be treated as condensed.
+ numCommas++;
+ }
}
- String name=readName();
- skipWhiteSpace();
- if (!readIf(':')) {
- throw expected("':'");
+ } else { // Did not reach eol.
+ // There was something else on this line.
+ // See if it was an EOL #.
+ String eol=readBetweenVals(true);
+ if (!eol.isEmpty()) { // This is an EOL #.
+ value.setFullComment(CommentType.EOL, eol);
+ } else { // There's another value on this line.
+ data.incrLineLength();
}
- skipWhiteSpace();
- object.add(name, readValue());
- skipWhiteSpace();
- if (readIf(',')) skipWhiteSpace(); // , is optional
}
- return object;
+ if (numCommas>1) throw error("Extra comma detected. Unclear element");
+ }
+
+ private boolean checkEndOfContainer(String type, char closer, boolean expectCloser) throws IOException {
+ if (isEndOfText()) {
+ if (expectCloser) throw error("End of input while parsing an "+type+". Did you forget a closing'"+closer+"'?");
+ return true;
+ }
+ if (expectCloser) return readIf(closer);
+ return false;
+ }
+
+ private int readNextDelimiter() throws IOException {
+ skipToNL();
+ int delimiter=current;
+ if (delimiter=='\n' || delimiter==',') {
+ read();
+ return delimiter;
+ }
+ return -1;
+ }
+
+ private boolean isContainerCompact(boolean expectCloser) throws IOException {
+ skipToNL();
+ readIf('\r');
+ // The object is compact if there is non-whitespace
+ // on the same line. If any further values are placed
+ // on subsequent lines, they will most likely look
+ // better this way, anyway.
+ return current!='\n' && expectCloser;
}
private String readName() throws IOException {
@@ -213,7 +303,7 @@ private String readName() throws IOException {
if (name.length()==0) throw error("Found ':' but no key name (for an empty key name use quotes)");
else if (space>=0 && space!=name.length()) { index=start+space; throw error("Found whitespace in your key name (use quotes to include)"); }
return name.toString();
- } else if (isWhiteSpace(current)) {
+ } else if (isWhitespace(current)) {
if (space<0) space=name.length();
} else if (current<' ') {
throw error("Name is not closed");
@@ -225,7 +315,6 @@ private String readName() throws IOException {
}
private String readMlString() throws IOException {
-
// Parse a multiline string value.
StringBuilder sb=new StringBuilder();
int triple=0;
@@ -235,7 +324,7 @@ private String readMlString() throws IOException {
// skip white/to (newline)
for (; ; ) {
- if (isWhiteSpace(current) && current!='\n') read();
+ if (isWhitespace(current) && current!='\n') read();
else break;
}
if (current=='\n') { read(); skipIndent(indent); }
@@ -273,7 +362,7 @@ else if (current=='\'') {
private void skipIndent(int indent) throws IOException {
while (indent-->0) {
- if (isWhiteSpace(current) && current!='\n') read();
+ if (isWhitespace(current) && current!='\n') read();
else break;
}
}
@@ -283,7 +372,7 @@ private JsonValue readString() throws IOException {
}
private String readStringInternal(boolean allowML) throws IOException {
- // callees make sure that (current=='"' || current=='\'')
+ // callers make sure that (current=='"' || current=='\'')
int exitCh = current;
read();
startCapture();
@@ -349,7 +438,7 @@ private static boolean isDigit(char ch) {
return ch>='0' && ch<='9';
}
- static JsonValue tryParseNumber(StringBuilder value, boolean stopAtNext) throws IOException {
+ static JsonValue tryParseNumber(StringBuilder value, boolean stopAtNext) {
int idx=0, len=value.length();
if (idx@@ -61,17 +58,20 @@ * This class is not supposed to be extended by clients. *
*/ -@SuppressWarnings("serial") // use default serial UID public class JsonArray extends JsonValue implements Iterable