Skip to content

An impl for read/write Json5 comments- patch1 #25

@Zim-Inn

Description

@Zim-Inn

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions