In fact, I have impl it some weeks ago.
But for some reason, I can't create large PR directly.
So I paste the patches, and you can apply it locally to review my impl.
All patches's base commit is d220224342fac20d9636fe37b6379afe2faef53e
It contains new UTs, and pass all UTs.
Here are 3 patches totally.
And if you need , i can write a degisn doc for you.
Index: src/main/java/de/marhali/json5/stream/Json5Lexer.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main/java/de/marhali/json5/stream/Json5Lexer.java b/src/main/java/de/marhali/json5/stream/Json5Lexer.java
--- a/src/main/java/de/marhali/json5/stream/Json5Lexer.java (revision d220224342fac20d9636fe37b6379afe2faef53e)
+++ b/src/main/java/de/marhali/json5/stream/Json5Lexer.java (date 1757065878329)
@@ -25,15 +25,25 @@
package de.marhali.json5.stream;
-import de.marhali.json5.*;
+import de.marhali.json5.Json5Array;
+import de.marhali.json5.Json5Boolean;
+import de.marhali.json5.Json5Element;
+import de.marhali.json5.Json5Hexadecimal;
+import de.marhali.json5.Json5Null;
+import de.marhali.json5.Json5Number;
+import de.marhali.json5.Json5Object;
+import de.marhali.json5.Json5Options;
+import de.marhali.json5.Json5String;
import de.marhali.json5.exception.Json5Exception;
import java.io.BufferedReader;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.util.Arrays;
import java.util.Objects;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
/**
* This is a lexer to convert the provided data into tokens according to the json5 specification.
@@ -44,26 +54,21 @@
* @author SyntaxError404
* @see <a href="https://spec.json5.org/">Json5 Standard</a>.
*/
+@SuppressWarnings("HardcodedLineSeparator")
public class Json5Lexer {
-
- private static final Pattern PATTERN_BOOLEAN = Pattern.compile(
- "true|false"
- );
+ private static final Pattern PATTERN_BOOLEAN = Pattern.compile("true|false");
private static final Pattern PATTERN_NUMBER_FLOAT = Pattern.compile(
- "[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?"
- );
- private static final Pattern PATTERN_NUMBER_INTEGER = Pattern.compile(
- "[+-]?(0|[1-9]\\d*)"
- );
- private static final Pattern PATTERN_NUMBER_HEX = Pattern.compile(
- "[+-]?0[xX][0-9a-fA-F]+"
- );
- private static final Pattern PATTERN_NUMBER_SPECIAL = Pattern.compile(
- "[+-]?(Infinity|NaN)"
- );
+ "[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?");
+
+ private static final Pattern PATTERN_NUMBER_INTEGER = Pattern.compile("[+-]?(0|[1-9]\\d*)");
+
+ private static final Pattern PATTERN_NUMBER_HEX = Pattern.compile("[+-]?0[xX][0-9a-fA-F]+");
+
+ private static final Pattern PATTERN_NUMBER_SPECIAL = Pattern.compile("[+-]?(Infinity|NaN)");
private final Reader reader;
+
private final Json5Options options;
/**
@@ -80,10 +85,12 @@
* the absolute position in the string
*/
private long index;
+
/**
* the relative position in the line
*/
private long character;
+
/**
* the line number
*/
@@ -93,15 +100,19 @@
* the previous character
*/
private char previous;
+
/**
* the current character
*/
private char current;
+ private StringBuilder lastComment;
+
/**
* Constructs a new lexer from a specific {@link Reader}.
* <p><b>Note:</b> The reader must be closed after operation ({@link Reader#close()})!</p>
- * @param reader a reader.
+ *
+ * @param reader a reader.
* @param options the options for lexing.
*/
public Json5Lexer(Reader reader, Json5Options options) {
@@ -117,13 +128,22 @@
previous = 0;
current = 0;
+ lastComment = null;
}
- private boolean more() {
- if (back || eof)
- return back && !eof;
-
- return peek() > 0;
+ /**
+ * Returns the last comment that was read and clears it.
+ * The parser will call this before parsing a new element.
+ *
+ * @return The captured comment content, or null if no comment was found.
+ */
+ public String consumeComment() {
+ if (this.lastComment == null) {
+ return null;
+ }
+ String comment = this.lastComment.toString();
+ this.lastComment = null;
+ return comment;
}
/**
@@ -133,9 +153,179 @@
back = true;
}
- private char peek() {
- if (eof)
- return 0;
+ /**
+ * Reads until encountering a character that is not a whitespace according to the
+ * <a href="https://spec.json5.org/#white-space">JSON5 Specification</a>
+ *
+ * @return a non-whitespace character, or {@code 0} if the end of the stream has been reached
+ */
+ public char nextClean() {
+ while (true) {
+ if (!more()) {
+ if (index == -1) { // Empty stream
+ return 0;
+ }
+ throw syntaxError("Unexpected end of data");
+ }
+
+ char n = next();
+
+ if (n == '/') {
+ char p = peek();
+
+ if (p == '*') {
+ // 跳过peek的字符
+ next();
+ nextMultiLineComment();
+ } else if (p == '/') {
+ // 跳过peek的字符
+ next();
+ // 再跳过//后紧邻的第一个空格
+ if (peek() == ' ') next();
+ nextSingleLineComment();
+ } else { return n; }
+ } else if (!isWhitespace(n)) { return n; }
+ }
+ }
+
+ /**
+ * Reads a member name from the source according to the
+ * <a href="https://spec.json5.org/#prod-JSON5MemberName">JSON5 Specification</a>
+ *
+ * @return an member name
+ */
+ public String nextMemberName() {
+ StringBuilder result = new StringBuilder();
+
+ char prev;
+ char n = next();
+
+ if (n == '"' || n == '\'') { return nextString(n); }
+
+ back();
+ n = 0;
+
+ while (true) {
+ if (!more()) { throw syntaxError("Unexpected end of data"); }
+
+ boolean part = result.length() > 0;
+
+ prev = n;
+ n = next();
+
+ if (n == '\\') { // unicode escape sequence
+ n = next();
+
+ if (n != 'u') { throw syntaxError("Illegal escape sequence '\\" + n + "' in key"); }
+
+ n = unicodeEscape(true, part);
+ } else if (!isMemberNameChar(n, part)) {
+ back();
+ break;
+ }
+
+ checkSurrogate(prev, n);
+
+ result.append(n);
+ }
+
+ if (result.length() == 0) { throw syntaxError("Empty key"); }
+
+ return result.toString();
+ }
+
+ /**
+ * Reads a value from the source according to the
+ * <a href="https://spec.json5.org/#prod-JSON5Value">JSON5 Specification</a>
+ *
+ * @return an member name
+ */
+ public Json5Element nextValue() {
+ char n = nextClean();
+
+ switch (n) {
+ case '"':
+ case '\'':
+ String string = nextString(n);
+ return new Json5String(string);
+ case '{':
+ back();
+ return Json5Parser.parseObject(this);
+ case '[':
+ back();
+ return Json5Parser.parseArray(this);
+ }
+
+ back();
+
+ String string = nextCleanTo(",]}");
+
+ if (string.equals("null")) { return Json5Null.INSTANCE; }
+
+ if (PATTERN_BOOLEAN.matcher(string).matches()) { return new Json5Boolean(string.equals("true")); }
+
+ if (PATTERN_NUMBER_INTEGER.matcher(string).matches()) {
+ BigInteger bigint = new BigInteger(string);
+ return new Json5Number(bigint);
+ }
+
+ if (PATTERN_NUMBER_FLOAT.matcher(string).matches()) { return new Json5Number(new BigDecimal(string)); }
+
+ if (PATTERN_NUMBER_SPECIAL.matcher(string).matches()) {
+ String special;
+
+ int factor;
+ double d = 0;
+
+ switch (string.charAt(0)) { // +, -, or 0
+ case '+':
+ special = string.substring(1); // +
+ factor = 1;
+ break;
+
+ case '-':
+ special = string.substring(1); // -
+ factor = -1;
+ break;
+
+ default:
+ special = string;
+ factor = 1;
+ break;
+ }
+
+ switch (special) {
+ case "NaN":
+ d = Double.NaN;
+ break;
+ case "Infinity":
+ d = Double.POSITIVE_INFINITY;
+ break;
+ }
+
+ return new Json5Number(factor * d);
+ }
+
+ if (PATTERN_NUMBER_HEX.matcher(string).matches()) {
+ return new Json5Hexadecimal(string);
+ }
+
+ throw new Json5Exception("Illegal value '" + string + "'");
+ }
+
+ @Override
+ public String toString() {
+ return " at index " + index + " [character " + character + " in line " + line + "]";
+ }
+
+ private boolean more() {
+ if (back || eof) { return back && !eof; }
+
+ return peek() > 0;
+ }
+
+ private char peek() {
+ if (eof) { return 0; }
int c;
@@ -179,7 +369,7 @@
if (isLineTerminator(current) && (current != '\n' || (current == '\n' && previous != '\r'))) {
line++;
character = 0;
- } else character++;
+ } else { character++; }
return current;
}
@@ -213,8 +403,7 @@
return true;
default:
// Unicode category "Zs" (space separators)
- if (Character.getType(c) == Character.SPACE_SEPARATOR)
- return true;
+ if (Character.getType(c) == Character.SPACE_SEPARATOR) { return true; }
return false;
}
@@ -226,54 +415,68 @@
}
private void nextMultiLineComment() {
- while (true) {
- char n = next();
-
- if (n == '*' && peek() == '/') {
- next();
- return;
- }
- }
- }
+ if (!options.isReadComments()) {
+ while (true) {
+ char n = next();
+ if (n == 0) {
+ throw syntaxError("Unterminated multi-line comment");
+ }
+ if (n == '*' && peek() == '/') {
+ next();
+ return;
+ }
+ }
+ }
+ if (this.lastComment == null) {
+ this.lastComment = new StringBuilder();
+ } else if (!this.lastComment.isEmpty()) {
+ this.lastComment.append("\n");
+ }
+
+ this.lastComment.append("/*");
+ while (true) {
+ char n = next();
+ if (n == 0) {
+ throw syntaxError("Unterminated multi-line comment");
+ }
+ this.lastComment.append(n);
+ if (n == '*' && peek() == '/') {
+ this.lastComment.append(next());
+ break;
+ }
+ }
+
+ String comment = this.lastComment.toString();
+ String[] lines = comment.split("\r?\n");
+ String normalizedComment = Arrays.stream(lines).skip(1).map(String::strip).map(l -> " " + l).collect(
+ Collectors.joining("\n"));
+ normalizedComment = lines[0] +"\n" + normalizedComment;
+ this.lastComment = new StringBuilder(normalizedComment);
+ }
+
private void nextSingleLineComment() {
- while (true) {
- char n = next();
-
- if (isLineTerminator(n) || n == 0)
- return;
- }
- }
+ if (!options.isReadComments()) {
+ while (true) {
+ char n = next();
+ if (isLineTerminator(n) || n == 0) {
+ return;
+ }
+ }
+ }
- /**
- * Reads until encountering a character that is not a whitespace according to the
- * <a href="https://spec.json5.org/#white-space">JSON5 Specification</a>
- *
- * @return a non-whitespace character, or {@code 0} if the end of the stream has been reached
- */
- public char nextClean() {
+ if (this.lastComment == null) {
+ this.lastComment = new StringBuilder();
+ } else if (!this.lastComment.isEmpty()) {
+ this.lastComment.append("\n");
+ }
+
while (true) {
- if (!more()) {
- if(index == -1) { // Empty stream
- return 0;
- }
- throw syntaxError("Unexpected end of data");
- }
-
char n = next();
-
- if (n == '/') {
- char p = peek();
-
- if (p == '*') {
- next();
- nextMultiLineComment();
- } else if (p == '/') {
- next();
- nextSingleLineComment();
- } else return n;
- } else if (!isWhitespace(n))
- return n;
+ if (isLineTerminator(n) || n == 0) {
+ return;
+ }
+ this.lastComment.append(n);
}
}
@@ -281,8 +484,7 @@
StringBuilder result = new StringBuilder();
while (true) {
- if (!more())
- throw syntaxError("Unexpected end of data");
+ if (!more()) { throw syntaxError("Unexpected end of data"); }
char n = nextClean();
@@ -298,14 +500,11 @@
}
private int dehex(char c) {
- if (c >= '0' && c <= '9')
- return c - '0';
+ if (c >= '0' && c <= '9') { return c - '0'; }
- if (c >= 'a' && c <= 'f')
- return c - 'a' + 0xA;
+ if (c >= 'a' && c <= 'f') { return c - 'a' + 0xA; }
- if (c >= 'A' && c <= 'F')
- return c - 'A' + 0xA;
+ if (c >= 'A' && c <= 'F') { return c - 'A' + 0xA; }
return -1;
}
@@ -322,30 +521,26 @@
int hex = dehex(n);
- if (hex == -1)
- throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in " + where);
+ if (hex == -1) { throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in " + where); }
codepoint |= hex << ((3 - i) << 2);
}
- if (member && !isMemberNameChar((char) codepoint, part))
+ if (member && !isMemberNameChar((char) codepoint, part)) {
throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in key");
+ }
return (char) codepoint;
}
private void checkSurrogate(char hi, char lo) {
- if (options.isAllowInvalidSurrogates())
- return;
+ if (options.isAllowInvalidSurrogates()) { return; }
- if (!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo))
- return;
+ if (!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo)) { return; }
- if (!Character.isSurrogatePair(hi, lo))
- throw syntaxError(String.format(
- "Invalid surrogate pair: U+%04X and U+%04X",
- hi, lo
- ));
+ if (!Character.isSurrogatePair(hi, lo)) {
+ throw syntaxError(String.format("Invalid surrogate pair: U+%04X and U+%04X", hi, lo));
+ }
}
// https://spec.json5.org/#prod-JSON5String
@@ -359,89 +554,88 @@
char prev;
while (true) {
- if (!more())
- throw syntaxError("Unexpected end of data");
+ if (!more()) { throw syntaxError("Unexpected end of data"); }
prev = n;
n = next();
- if (n == quote)
- break;
+ if (n == quote) { break; }
- if (isLineTerminator(n) && n != 0x2028 && n != 0x2029)
+ if (isLineTerminator(n) && n != 0x2028 && n != 0x2029) {
throw syntaxError("Unescaped line terminator in string");
+ }
if (n == '\\') {
n = next();
if (isLineTerminator(n)) {
- if (n == '\r' && peek() == '\n')
- next();
+ if (n == '\r' && peek() == '\n') { next(); }
// escaped line terminator/ line continuation
continue;
- } else switch (n) {
- case '\'':
- case '"':
- case '\\':
- result.append(n);
- continue;
- case 'b':
- result.append('\b');
- continue;
- case 'f':
- result.append('\f');
- continue;
- case 'n':
- result.append('\n');
- continue;
- case 'r':
- result.append('\r');
- continue;
- case 't':
- result.append('\t');
- continue;
- case 'v': // Vertical Tab
- result.append((char) 0x0B);
- continue;
+ } else {
+ switch (n) {
+ case '\'':
+ case '"':
+ case '\\':
+ result.append(n);
+ continue;
+ case 'b':
+ result.append('\b');
+ continue;
+ case 'f':
+ result.append('\f');
+ continue;
+ case 'n':
+ result.append('\n');
+ continue;
+ case 'r':
+ result.append('\r');
+ continue;
+ case 't':
+ result.append('\t');
+ continue;
+ case 'v': // Vertical Tab
+ result.append((char) 0x0B);
+ continue;
- case '0': // NUL
- char p = peek();
+ case '0': // NUL
+ char p = peek();
- if (isDecimalDigit(p))
- throw syntaxError("Illegal escape sequence '\\0" + p + "'");
+ if (isDecimalDigit(p)) { throw syntaxError("Illegal escape sequence '\0" + p + "'"); }
- result.append((char) 0);
- continue;
+ result.append((char) 0);
+ continue;
- case 'x': // Hex escape sequence
- value = "";
- codepoint = 0;
+ case 'x': // Hex escape sequence
+ value = "";
+ codepoint = 0;
- for (int i = 0; i < 2; ++i) {
- n = next();
- value += n;
+ for (int i = 0; i < 2; ++i) {
+ n = next();
+ value += n;
- int hex = dehex(n);
+ int hex = dehex(n);
- if (hex == -1)
- throw syntaxError("Illegal hex escape sequence '\\x" + value + "' in string");
+ if (hex == -1) {
+ throw syntaxError("Illegal hex escape sequence '\\x" + value + "' in string");
+ }
- codepoint |= hex << ((1 - i) << 2);
- }
+ codepoint |= hex << ((1 - i) << 2);
+ }
- n = (char) codepoint;
- break;
+ n = (char) codepoint;
+ break;
- case 'u': // Unicode escape sequence
- n = unicodeEscape(false, false);
- break;
+ case 'u': // Unicode escape sequence
+ n = unicodeEscape(false, false);
+ break;
- default:
- if (isDecimalDigit(n))
- throw syntaxError("Illegal escape sequence '\\" + n + "'");
+ default:
+ if (isDecimalDigit(n)) { throw syntaxError("Illegal escape sequence '\\" + n + "'"); }
- break;
+ break;
+ }
}
}
@@ -454,8 +648,7 @@
}
private boolean isMemberNameChar(char n, boolean part) {
- if (n == '$' || n == '_' || n == 0x200C || n == 0x200D)
- return true;
+ if (n == '$' || n == '_' || n == 0x200C || n == 0x200D) { return true; }
int type = Character.getType(n);
@@ -472,151 +665,18 @@
case Character.COMBINING_SPACING_MARK:
case Character.DECIMAL_DIGIT_NUMBER:
case Character.CONNECTOR_PUNCTUATION:
- if (part)
- return true;
+ if (part) { return true; }
break;
}
return false;
}
- /**
- * Reads a member name from the source according to the
- * <a href="https://spec.json5.org/#prod-JSON5MemberName">JSON5 Specification</a>
- *
- * @return an member name
- */
- public String nextMemberName() {
- StringBuilder result = new StringBuilder();
-
- char prev;
- char n = next();
-
- if (n == '"' || n == '\'')
- return nextString(n);
-
- back();
- n = 0;
-
- while (true) {
- if (!more())
- throw syntaxError("Unexpected end of data");
-
- boolean part = result.length() > 0;
-
- prev = n;
- n = next();
-
- if (n == '\\') { // unicode escape sequence
- n = next();
-
- if (n != 'u')
- throw syntaxError("Illegal escape sequence '\\" + n + "' in key");
-
- n = unicodeEscape(true, part);
- } else if (!isMemberNameChar(n, part)) {
- back();
- break;
- }
-
- checkSurrogate(prev, n);
-
- result.append(n);
- }
-
- if (result.length() == 0)
- throw syntaxError("Empty key");
-
- return result.toString();
- }
-
- /**
- * Reads a value from the source according to the
- * <a href="https://spec.json5.org/#prod-JSON5Value">JSON5 Specification</a>
- *
- * @return an member name
- */
- public Json5Element nextValue() {
- char n = nextClean();
-
- switch (n) {
- case '"':
- case '\'':
- String string = nextString(n);
- return new Json5String(string);
- case '{':
- back();
- return Json5Parser.parseObject(this);
- case '[':
- back();
- return Json5Parser.parseArray(this);
- }
-
- back();
-
- String string = nextCleanTo(",]}");
-
- if (string.equals("null"))
- return Json5Null.INSTANCE;
-
- if (PATTERN_BOOLEAN.matcher(string).matches())
- return new Json5Boolean(string.equals("true"));
-
- if (PATTERN_NUMBER_INTEGER.matcher(string).matches()) {
- BigInteger bigint = new BigInteger(string);
- return new Json5Number(bigint);
- }
-
- if (PATTERN_NUMBER_FLOAT.matcher(string).matches())
- return new Json5Number(new BigDecimal(string));
-
- if (PATTERN_NUMBER_SPECIAL.matcher(string).matches()) {
- String special;
-
- int factor;
- double d = 0;
-
- switch (string.charAt(0)) { // +, -, or 0
- case '+':
- special = string.substring(1); // +
- factor = 1;
- break;
-
- case '-':
- special = string.substring(1); // -
- factor = -1;
- break;
-
- default:
- special = string;
- factor = 1;
- break;
- }
-
- switch (special) {
- case "NaN":
- d = Double.NaN;
- break;
- case "Infinity":
- d = Double.POSITIVE_INFINITY;
- break;
- }
-
- return new Json5Number(factor * d);
- }
-
- if (PATTERN_NUMBER_HEX.matcher(string).matches()) {
- return new Json5Hexadecimal(string);
- }
-
- throw new Json5Exception("Illegal value '" + string + "'");
- }
-
/**
* Constructs a new JSONException with a detail message and a causing exception
*
* @param message the detail message
- * @param cause the causing exception
+ * @param cause the causing exception
* @return a JSONException
*/
protected Json5Exception syntaxError(String message, Throwable cause) {
@@ -632,9 +692,4 @@
protected Json5Exception syntaxError(String message) {
return new Json5Exception(message + this);
}
-
- @Override
- public String toString() {
- return " at index " + index + " [character " + character + " in line " + line + "]";
- }
}
Index: src/main/java/de/marhali/json5/stream/Json5Parser.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main/java/de/marhali/json5/stream/Json5Parser.java b/src/main/java/de/marhali/json5/stream/Json5Parser.java
--- a/src/main/java/de/marhali/json5/stream/Json5Parser.java (revision d220224342fac20d9636fe37b6379afe2faef53e)
+++ b/src/main/java/de/marhali/json5/stream/Json5Parser.java (date 1757065878199)
@@ -51,18 +51,31 @@
public static Json5Element parse(Json5Lexer lexer) {
Objects.requireNonNull(lexer);
- switch (lexer.nextClean()) {
+ char c = lexer.nextClean();
+ String comment = lexer.consumeComment();
+
+ Json5Element element;
+
+ switch (c) {
case '{':
lexer.back();
- return parseObject(lexer);
+ element = parseObject(lexer);
+ break;
case '[':
lexer.back();
- return parseArray(lexer);
+ element = parseArray(lexer);
+ break;
case 0:
return null;
default:
throw lexer.syntaxError("Unknown or unexpected control character");
}
+
+ if (comment != null) {
+ element.setComment(comment);
+ }
+
+ return element;
}
/**
@@ -86,10 +99,14 @@
while (true) {
control = lexer.nextClean();
+ String comment = lexer.consumeComment();
switch (control) {
case 0:
throw lexer.syntaxError("A json object must end with '}'");
case '}':
+ if (comment != null) {
+ object.setComment(comment);
+ }
return object;
default:
lexer.back();
@@ -104,7 +121,12 @@
throw lexer.syntaxError("Expected ':' after a key, got '" + control + "' instead");
}
- object.add(key, lexer.nextValue());
+ Json5Element value = lexer.nextValue();
+ object.add(key, value);
+ if (comment != null) {
+ object.setComment(key, comment);
+ value.setComment(comment);
+ }
control = lexer.nextClean();
if(control == '}') {
@@ -136,16 +158,24 @@
while(true) {
control = lexer.nextClean();
+ String comment = lexer.consumeComment();
switch (control) {
case 0:
throw lexer.syntaxError("A json array must end with ']'");
case ']':
+ if (comment != null) {
+ array.setComment(comment);
+ }
return array;
default:
lexer.back();
}
- array.add(lexer.nextValue());
+ Json5Element value = lexer.nextValue();
+ if (comment != null) {
+ value.setComment(comment);
+ }
+ array.add(value);
control = lexer.nextClean();
if(control == ']') {
Index: src/main/java/de/marhali/json5/stream/Json5Writer.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main/java/de/marhali/json5/stream/Json5Writer.java b/src/main/java/de/marhali/json5/stream/Json5Writer.java
--- a/src/main/java/de/marhali/json5/stream/Json5Writer.java (revision d220224342fac20d9636fe37b6379afe2faef53e)
+++ b/src/main/java/de/marhali/json5/stream/Json5Writer.java (date 1757065878331)
@@ -3,6 +3,7 @@
*
* Copyright (C) 2021 SyntaxError404
* Copyright (C) 2022 Marcel Haßlinger
+ * Copyright (C) 2024 Ultreon Team
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +26,12 @@
package de.marhali.json5.stream;
-import de.marhali.json5.*;
+import de.marhali.json5.Json5Array;
+import de.marhali.json5.Json5Element;
+import de.marhali.json5.Json5Object;
+import de.marhali.json5.Json5Options;
+import de.marhali.json5.Json5Primitive;
+import de.marhali.json5.Json5String;
import java.io.IOException;
import java.io.Writer;
@@ -39,14 +45,16 @@
* @author Marcel Haßlinger
* @author SyntaxError404
*/
+@SuppressWarnings("HardcodedLineSeparator")
public final class Json5Writer {
-
private final Json5Options options;
+
private final Writer writer;
/**
* Creates a new instance that writes a JSON5-encoded stream to {@code writer}.
* <p><b>Note:</b> The writer must be closed after operation ({@link Writer#close()})!</p>
+ *
* @param options Parsing and serialization options
* @param writer Output stream. For best performance, use a {@link java.io.BufferedWriter}
*/
@@ -58,44 +66,50 @@
/**
* Encodes and writes the provided {@link Json5Element} into json5 according to the specification
* and the configured options. The element can be any json5 element. All child trees will be included.
+ *
* @param element Element to encode
* @throws IOException If an I/O error occurs
* @see #Json5Writer(Json5Options, Writer) Configuration options
- * @see #write(Json5Element, String)
+ * @see #write(Json5Element, String, boolean)
*/
public void write(Json5Element element) throws IOException {
- write(element, "");
+ write(element, "", true);
}
/**
* Encodes and writes the provided {@link Json5Element} into json5 according to the specification
* and the configured options. The element can be any json5 element. All child trees will be included.
+ *
* @param element Element to encode
* @param indent Indent to apply (for nested elements)
* @throws IOException If an I/O error occurs
* @see #Json5Writer(Json5Options, Writer) Configuration options
* @see #write(Json5Element) without indent
*/
- public void write(Json5Element element, String indent) throws IOException {
+ public void write(Json5Element element, String indent, boolean writeComment) throws IOException {
Objects.requireNonNull(element);
Objects.requireNonNull(indent);
-
- if(element.isJson5Null()) {
+ if (writeComment && options.isWriteComments() && element.hasComment()) {
+ String comment = element.getComment();
+ writeElementComment(comment, indent);
+ }
+ if (element.isJson5Null()) {
writeNull();
- } else if(element.isJson5Object()) {
+ } else if (element.isJson5Object()) {
writeObject(element.getAsJson5Object(), indent);
- } else if(element.isJson5Array()) {
+ } else if (element.isJson5Array()) {
writeArray(element.getAsJson5Array(), indent);
- } else if(element.isJson5Primitive()) {
+ } else if (element.isJson5Primitive()) {
writePrimitive(element.getAsJson5Primitive());
} else {
- throw new UnsupportedOperationException("Unknown json element with type class "
- + element.getClass().getName());
+ throw new UnsupportedOperationException(
+ "Unknown json element with type class " + element.getClass().getName());
}
}
/**
* Writes the equivalent of a {@link de.marhali.json5.Json5Null}({@code null}) value.
+ *
* @throws IOException If an I/O error occurs.
*/
public void writeNull() throws IOException {
@@ -104,13 +118,14 @@
/**
* Writes the provided primitive to the stream and encodes it if necessary.
+ *
* @param primitive Primitive value.
* @throws IOException If an I/O error occurs.
*/
public void writePrimitive(Json5Primitive primitive) throws IOException {
Objects.requireNonNull(primitive);
- if(primitive instanceof Json5String) {
+ if (primitive instanceof Json5String) {
writer.append(quote(primitive.getAsString()));
} else {
writer.append(primitive.getAsString());
@@ -119,6 +134,7 @@
/**
* Writes the provided {@link Json5Object} to the stream.
+ *
* @param object Object to encode
* @param indent Indent to apply (for nested elements)
* @throws IOException If an I/O error occurs.
@@ -133,27 +149,38 @@
writer.write("{");
int index = 0;
- for(Map.Entry<String, Json5Element> entry : object.entrySet()) {
- if(options.getIndentFactor() > 0) {
+ for (Map.Entry<String, Json5Element> entry : object.entrySet()) {
+ if (options.getIndentFactor() > 0) {
writer.append('\n').append(childIndent);
}
- writer.append(quote(entry.getKey())).append(":");
+ Json5Element element = entry.getValue();
+ if (options.isWriteComments() && element.hasComment()) {
+ writeElementComment(element.getComment(), childIndent);
+ }
+
+ //<editor-fold desc="Modified by Ultreon (added support for quoteless)">
+ if (options.isQuoteless() && entry.getKey().matches("^[a-zA-Z_][a-zA-Z0-9_]*[a-zA-Z_]$")) {
+ writer.append(entry.getKey()).append(":");
+ } else {
+ writer.append(quote(entry.getKey())).append(":");
+ }
+ //</editor-fold>
- if(options.getIndentFactor() > 0) {
+ if (options.getIndentFactor() > 0) {
writer.append(' ');
}
- write(entry.getValue(), childIndent);
+ write(element, childIndent, false);
- if(options.isTrailingComma() || index < object.size() - 1) {
+ if (options.isTrailingComma() || index < object.size() - 1) {
writer.append(',');
}
index++;
}
- if(options.getIndentFactor() > 0 && object.size() > 0) {
+ if (options.getIndentFactor() > 0 && object.size() > 0) {
writer.append('\n').append(indent);
}
@@ -162,6 +189,7 @@
/**
* Writes the provided {@link Json5Array} to the stream.
+ *
* @param array Array to encode
* @param indent Indent to apply (for nested elements)
* @throws IOException If an I/O error occurs.
@@ -175,21 +203,21 @@
writer.write('[');
- for(int i = 0; i < array.size(); i++) {
+ for (int i = 0; i < array.size(); i++) {
Json5Element currentElement = array.get(i);
- if(options.getIndentFactor() > 0) {
+ if (options.getIndentFactor() > 0) {
writer.append('\n').append(childIndent);
}
- write(currentElement, childIndent);
+ write(currentElement, childIndent, options.isWriteComments());
- if(options.isTrailingComma() || i < array.size() - 1) {
+ if (options.isTrailingComma() || i < array.size() - 1) {
writer.append(',');
}
}
- if(options.getIndentFactor() > 0 && !array.isEmpty()) {
+ if (options.getIndentFactor() > 0 && !array.isEmpty()) {
writer.append('\n').append(indent);
}
@@ -198,27 +226,28 @@
/**
* Quotes the provided string according to the json5 <a href="https://spec.json5.org/#strings">specification</a>.
+ *
* @param string String to quote
* @return Quoted string
*/
public String quote(String string) {
final char qt = options.isQuoteSingle() ? '\'' : '"';
- if(string == null || string.isEmpty()) {
+ if (string == null || string.isEmpty()) {
return String.valueOf(qt).repeat(2);
}
StringBuilder quoted = new StringBuilder(string.length() + 2);
quoted.append(qt);
- for(char c : string.toCharArray()) {
- if(c == qt) {
+ for (char c : string.toCharArray()) {
+ if (c == qt) {
quoted.append('\\');
quoted.append(c);
continue;
}
- switch(c) {
+ switch (c) {
case '\\':
quoted.append("\\\\");
break;
@@ -242,7 +271,7 @@
break;
default:
// escape non-graphical characters (https://www.unicode.org/versions/Unicode13.0.0/ch02.pdf#G286941)
- switch(Character.getType(c)) {
+ switch (Character.getType(c)) {
case Character.FORMAT:
case Character.LINE_SEPARATOR:
case Character.PARAGRAPH_SEPARATOR:
@@ -251,7 +280,7 @@
case Character.SURROGATE:
case Character.UNASSIGNED:
quoted.append("\\u");
- quoted.append(String.format("%04X", c));
+ quoted.append(String.format("%04X", (int) c));
break;
default:
quoted.append(c);
@@ -263,4 +292,17 @@
quoted.append(qt);
return quoted.toString();
}
+
+ private void writeElementComment(String comment, String indent) throws IOException {
+ String[] lines = comment.split("\n");
+ if (comment.startsWith("/*")) {
+ for (String line : lines) {
+ writer.append(line).append("\n").append(indent);
+ }
+ } else {
+ for (String line : lines) {
+ writer.append("// ").append(line).append('\n').append(indent);
+ }
+ }
+ }
}
\ No newline at end of file
In fact, I have impl it some weeks ago.
But for some reason, I can't create large PR directly.
So I paste the patches, and you can apply it locally to review my impl.
All patches's base commit is
d220224342fac20d9636fe37b6379afe2faef53eIt contains new UTs, and pass all UTs.
Here are 3 patches totally.
And if you need , i can write a degisn doc for you.