predicate, String key, String type) {
- if(!predicate.test(key))
- throw mismatch(key, type);
-
- return (T) values.get(key);
- }
-
- //
-
- private static JSONException mismatch(String key, String type) {
- return new JSONException("JSONObject[" + JSONStringify.quote(key) +"] is not of type " + type);
- }
-
- /**
- * Sanitizes an input value
- *
- * @param value the value
- * @return the sanitized value
- *
- * @throws JSONException if the value is illegal
- */
- static Object sanitize(Object value) {
- if(value == null)
- return null;
-
- if(value instanceof Boolean ||
- value instanceof String ||
- value instanceof JSONObject ||
- value instanceof JSONArray ||
- value instanceof Instant)
- return value;
-
- else if(value instanceof Number) {
- Number num = (Number) value;
-
- if(value instanceof Double) {
- double d = (Double) num;
-
- if(Double.isFinite(d))
- return BigDecimal.valueOf(d);
- }
-
- else if(value instanceof Float) {
- float f = (Float) num;
-
- if(Float.isFinite(f))
- return BigDecimal.valueOf(f);
-
- // NaN and Infinity
- return num.doubleValue();
- }
-
- else if(value instanceof Byte ||
- value instanceof Short ||
- value instanceof Integer ||
- value instanceof Long)
- return BigInteger.valueOf(num.longValue());
-
- else if(!(value instanceof BigDecimal ||
- value instanceof BigInteger))
- return BigDecimal.valueOf(num.doubleValue());
-
- return num;
- }
-
- else throw new JSONException("Illegal type '" + value.getClass() + "'");
- }
-
-}
diff --git a/src/main/java/at/syntaxerror/json5/JSONOptions.java b/src/main/java/at/syntaxerror/json5/JSONOptions.java
deleted file mode 100755
index 3c2e59c..0000000
--- a/src/main/java/at/syntaxerror/json5/JSONOptions.java
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2021 SyntaxError404
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package at.syntaxerror.json5;
-
-import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Getter;
-import lombok.NonNull;
-import lombok.Setter;
-import lombok.experimental.FieldDefaults;
-
-/**
- * This class used is used to customize the behaviour of {@link JSONParser parsing} and {@link JSONStringify stringifying}
- *
- * @author SyntaxError404
- * @since 1.1.0
- */
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
-@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
-@Getter
-@Builder(toBuilder = true)
-public class JSONOptions {
-
- /**
- * -- GETTER --
- * Returns the default options for parsing and stringifying
- *
- * @return the default options
- * @since 1.1.0
- *
- * -- SETTER --
- * Sets the default options for parsing and stringifying.
- * Must not be {@code null}
- *
- * @param defaultOptions the new default options
- * @since 1.1.0
- */
- @Getter
- @Setter
- @NonNull
- private static JSONOptions defaultOptions = builder().build();
-
- /**
- * Whether or not instants should be parsed as such.
- * If this is {@code false}, {@link #isParseStringInstants()} and {@link #isParseUnixInstants()}
- * are ignored
- *
- * Default: {@code true}
- *
- * This is a {@link JSONParser Parser}-only option
- *
- * @param parseInstants a boolean
- *
- * @return whether or not instants should be parsed
- * @since 1.1.0
- */
- @Builder.Default
- boolean parseInstants = true;
- /**
- * Whether or not string instants (according to
- * RFC 3339, Section 5.6)
- * should be parsed as such.
- * Ignored if {@link #isParseInstants()} is {@code false}
- *
- * Default: {@code true}
- *
- * This is a {@link JSONParser Parser}-only option
- *
- * @param parseStringInstants a boolean
- *
- * @return whether or not string instants should be parsed
- * @since 1.1.0
- */
- @Builder.Default
- boolean parseStringInstants = true;
- /**
- * Whether or not unix instants (integers) should be parsed as such.
- * Ignored if {@link #isParseInstants()} is {@code false}
- *
- * Default: {@code true}
- *
- * This is a {@link JSONParser Parser}-only option
- *
- * @param parseUnixInstants a boolean
- *
- * @return whether or not unix instants should be parsed
- * @since 1.1.0
- */
- @Builder.Default
- boolean parseUnixInstants = true;
-
- /**
- * Whether or not instants should be stringifyed as unix timestamps.
- * If this is {@code false}, instants will be stringifyed as strings
- * (according to RFC 3339, Section 5.6).
- *
- * Default: {@code false}
- *
- * This is a {@link JSONStringify Stringify}-only option
- *
- * @param stringifyUnixInstants a boolean
- *
- * @return whether or not instants should be stringifyed as unix timestamps
- * @since 1.1.0
- */
- @Builder.Default
- boolean stringifyUnixInstants = false;
-
- /**
- * Whether or not {@code NaN} should be allowed as a number
- *
- * Default: {@code true}
- *
- * @param allowNaN a boolean
- *
- * @return whether or not {@code NaN} should be allowed
- * @since 1.1.0
- */
- @Builder.Default
- boolean allowNaN = true;
-
- /**
- * Whether or not {@code Infinity} should be allowed as a number.
- * This applies to both {@code +Infinity} and {@code -Infinity}
- *
- * Default: {@code true}
- *
- * @param allowInfinity a boolean
- *
- * @return whether or not {@code Infinity} should be allowed
- * @since 1.1.0
- */
- @Builder.Default
- boolean allowInfinity = true;
-
- /**
- * Whether or not invalid unicode surrogate pairs should be allowed
- *
- * Default: {@code true}
- *
- * This is a {@link JSONParser Parser}-only option
- *
- * @param allowInvalidSurrogates a boolean
- *
- * @return whether or not invalid unicode surrogate pairs should be allowed
- * @since 1.1.0
- */
- @Builder.Default
- boolean allowInvalidSurrogates = true;
-
- /**
- * Whether or not string should be single-quoted ({@code '}) instead of double-quoted ({@code "}).
- * This also includes a {@link JSONObject JSONObject's} member names
- *
- * Default: {@code false}
- *
- * This is a {@link JSONStringify Stringify}-only option
- *
- * @param quoteSingle a boolean
- *
- * @return whether or not string should be single-quoted
- * @since 1.1.0
- */
- @Builder.Default
- boolean quoteSingle = false;
-
-}
diff --git a/src/main/java/at/syntaxerror/json5/JSONParser.java b/src/main/java/at/syntaxerror/json5/JSONParser.java
deleted file mode 100755
index 6c4a6e8..0000000
--- a/src/main/java/at/syntaxerror/json5/JSONParser.java
+++ /dev/null
@@ -1,730 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2021 SyntaxError404
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package at.syntaxerror.json5;
-
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.StringReader;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.time.Instant;
-import java.util.regex.Pattern;
-
-/**
- * A JSONParser is used to convert a source string into tokens, which then are used to
- * construct {@link JSONObject JSONObjects} and {@link JSONArray JSONArrays}
- *
- * @author SyntaxError404
- */
-public class JSONParser {
-
- 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)"
- );
-
- private final Reader reader;
- private final JSONOptions options;
-
- /** whether or not the end of the file has been reached */
- private boolean eof;
-
- /** whether or not the current character should be re-read */
- private boolean back;
-
- /** the absolute position in the string */
- private long index;
- /** the relative position in the line */
- private long character;
- /** the line number */
- private long line;
-
- /** the previous character */
- private char previous;
- /** the current character */
- private char current;
-
- /**
- * Constructs a new JSONParser from a Reader. The reader is not {@link Reader#close() closed}
- *
- * @param reader a reader
- * @param options the options for parsing
- * @since 1.1.0
- */
- public JSONParser(Reader reader, JSONOptions options) {
- this.reader = reader.markSupported() ?
- reader : new BufferedReader(reader);
-
- this.options = options == null ?
- JSONOptions.getDefaultOptions() : options;
-
- eof = false;
- back = false;
-
- index = -1;
- character = 0;
- line = 1;
-
- previous = 0;
- current = 0;
- }
-
- /**
- * Constructs a new JSONParser from a string
- *
- * @param source a string
- * @param options the options for parsing
- * @since 1.1.0
- */
- public JSONParser(String source, JSONOptions options) {
- this(new StringReader(source), options);
- }
-
- /**
- * Constructs a new JSONParser from an InputStream. The stream is not {@link InputStream#close() closed}
- *
- * @param stream a stream
- * @param options the options for parsing
- * @since 1.1.0
- */
- public JSONParser(InputStream stream, JSONOptions options) {
- this(new InputStreamReader(stream), options);
- }
-
- /**
- * Constructs a new JSONParser from a Reader. The reader is not {@link Reader#close() closed}.
- * This uses the {@link JSONOptions#getDefaultOptions() default options}
- *
- * @param reader a reader
- */
- public JSONParser(Reader reader) {
- this(reader, null);
- }
-
- /**
- * Constructs a new JSONParser from a string.
- * This uses the {@link JSONOptions#getDefaultOptions() default options}
- *
- * @param source a string
- */
- public JSONParser(String source) {
- this(source, null);
- }
-
- /**
- * Constructs a new JSONParser from an InputStream. The stream is not {@link InputStream#close() closed}.
- * This uses the {@link JSONOptions#getDefaultOptions() default options}
- *
- * @param stream a stream
- */
- public JSONParser(InputStream stream) {
- this(stream, null);
- }
-
- private boolean more() {
- if(back || eof)
- return back && !eof;
-
- return peek() > 0;
- }
-
- /**
- * Forces the parser to re-read the last character
- */
- public void back() {
- back = true;
- }
-
- private char peek() {
- if(eof)
- return 0;
-
- int c;
-
- try {
- reader.mark(1);
-
- c = reader.read();
-
- reader.reset();
- } catch(Exception e) {
- throw syntaxError("Could not peek from source", e);
- }
-
- return c == -1 ? 0 : (char) c;
- }
-
- private char next() {
- if(back) {
- back = false;
- return current;
- }
-
- int c;
-
- try {
- c = reader.read();
- } catch(Exception e) {
- throw syntaxError("Could not read from source", e);
- }
-
- if(c < 0) {
- eof = true;
- return 0;
- }
-
- previous = current;
- current = (char) c;
-
- index++;
-
- if(isLineTerminator(current) && (current != '\n' || (current == '\n' && previous != '\r'))) {
- line++;
- character = 0;
- }
- else character++;
-
- return current;
- }
-
- // https://262.ecma-international.org/5.1/#sec-7.3
- private boolean isLineTerminator(char c) {
- switch(c) {
- case '\n':
- case '\r':
- case 0x2028:
- case 0x2029:
- return true;
- default:
- return false;
- }
- }
-
- // https://spec.json5.org/#white-space
- private boolean isWhitespace(char c) {
- switch(c) {
- case '\t':
- case '\n':
- case 0x0B: // Vertical Tab
- case '\f':
- case '\r':
- case ' ':
- case 0xA0: // No-break space
- case 0x2028: // Line separator
- case 0x2029: // Paragraph separator
- case 0xFEFF: // Byte Order Mark
- return true;
- default:
- // Unicode category "Zs" (space separators)
- if(Character.getType(c) == Character.SPACE_SEPARATOR)
- return true;
-
- return false;
- }
- }
-
- // https://262.ecma-international.org/5.1/#sec-9.3.1
- private boolean isDecimalDigit(char c) {
- return c >= '0' && c <= '9';
- }
-
- private void nextMultiLineComment() {
- while(true) {
- char n = next();
-
- if(n == '*' && peek() == '/') {
- next();
- return;
- }
- }
- }
-
- private void nextSingleLineComment() {
- while(true) {
- char n = next();
-
- if(isLineTerminator(n) || n == 0)
- return;
- }
- }
-
- /**
- * Reads until encountering a character that is not a whitespace according to the
- * JSON5 Specification
- *
- * @return a non-whitespace character, or {@code 0} if the end of the stream has been reached
- */
- public char nextClean() {
- while(true) {
- if(!more())
- 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;
- }
- }
-
- private String nextCleanTo(String delimiters) {
- StringBuilder result = new StringBuilder();
-
- while(true) {
- if(!more())
- throw syntaxError("Unexpected end of data");
-
- char n = nextClean();
-
- if(delimiters.indexOf(n) > -1 || isWhitespace(n)) {
- back();
- break;
- }
-
- result.append(n);
- }
-
- return result.toString();
- }
-
- private int dehex(char c) {
- 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;
-
- return -1;
- }
-
- private char unicodeEscape(boolean member, boolean part) {
- String where = member ? "key" : "string";
-
- String value = "";
- int codepoint = 0;
-
- for(int i = 0; i < 4; ++i) {
- char n = next();
- value += n;
-
- int hex = dehex(n);
-
- if(hex == -1)
- throw syntaxError("Illegal unicode escape sequence '\\u" + value + "' in " + where);
-
- codepoint |= hex << ((3 - i) << 2);
- }
-
- 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(!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
- ));
- }
-
- // https://spec.json5.org/#prod-JSON5String
- private String nextString(char quote) {
- StringBuilder result = new StringBuilder();
-
- String value;
- int codepoint;
-
- char n = 0;
- char prev;
-
- while(true) {
- if(!more())
- throw syntaxError("Unexpected end of data");
-
- prev = n;
- n = next();
-
- if(n == quote)
- break;
-
- 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();
-
- // 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;
-
- case '0': // NUL
- char p = peek();
-
- if(isDecimalDigit(p))
- throw syntaxError("Illegal escape sequence '\\0" + p + "'");
-
- result.append((char) 0);
- continue;
-
- case 'x': // Hex escape sequence
- value = "";
- codepoint = 0;
-
- for(int i = 0; i < 2; ++i) {
- n = next();
- value += n;
-
- int hex = dehex(n);
-
- if(hex == -1)
- throw syntaxError("Illegal hex escape sequence '\\x" + value + "' in string");
-
- codepoint |= hex << ((1 - i) << 2);
- }
-
- n = (char) codepoint;
- break;
-
- case 'u': // Unicode escape sequence
- n = unicodeEscape(false, false);
- break;
-
- default:
- if(isDecimalDigit(n))
- throw syntaxError("Illegal escape sequence '\\" + n + "'");
-
- break;
- }
- }
-
- checkSurrogate(prev, n);
-
- result.append(n);
- }
-
- return result.toString();
- }
-
- private boolean isMemberNameChar(char n, boolean part) {
- if(n == '$' || n == '_' || n == 0x200C || n == 0x200D)
- return true;
-
- int type = Character.getType(n);
-
- switch(type) {
- case Character.UPPERCASE_LETTER:
- case Character.LOWERCASE_LETTER:
- case Character.TITLECASE_LETTER:
- case Character.MODIFIER_LETTER:
- case Character.OTHER_LETTER:
- case Character.LETTER_NUMBER:
- return true;
-
- case Character.NON_SPACING_MARK:
- case Character.COMBINING_SPACING_MARK:
- case Character.DECIMAL_DIGIT_NUMBER:
- case Character.CONNECTOR_PUNCTUATION:
- if(part)
- return true;
- break;
- }
-
- return false;
- }
-
- /**
- * Reads a member name from the source according to the
- * JSON5 Specification
- *
- * @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
- * JSON5 Specification
- *
- * @return an member name
- */
- public Object nextValue() {
- char n = nextClean();
-
- switch(n) {
- case '"':
- case '\'':
- String string = nextString(n);
-
- if(options.isParseInstants() && options.isParseStringInstants())
- try {
- return Instant.parse(string);
- } catch (Exception e) { }
-
- return string;
- case '{':
- back();
- return new JSONObject(this);
- case '[':
- back();
- return new JSONArray(this);
- }
-
- back();
-
- String string = nextCleanTo(",]}");
-
- if(string.equals("null"))
- return null;
-
- if(PATTERN_BOOLEAN.matcher(string).matches())
- return string.equals("true");
-
- if(PATTERN_NUMBER_INTEGER.matcher(string).matches()) {
- BigInteger bigint = new BigInteger(string);
-
- if(options.isParseInstants() && options.isParseUnixInstants())
- try {
- long unix = bigint.longValueExact();
-
- return Instant.ofEpochSecond(unix);
- } catch (Exception e) { }
-
- return bigint;
- }
-
- if(PATTERN_NUMBER_FLOAT.matcher(string).matches())
- return 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":
- if(!options.isAllowNaN())
- throw syntaxError("NaN is not allowed");
-
- d = Double.NaN;
- break;
- case "Infinity":
- if(!options.isAllowInfinity())
- throw syntaxError("Infinity is not allowed");
-
- d = Double.POSITIVE_INFINITY;
- break;
- }
-
- return factor * d;
- }
-
- if(PATTERN_NUMBER_HEX.matcher(string).matches()) {
- String hex;
-
- int factor;
-
- switch(string.charAt(0)) { // +, -, or 0
- case '+':
- hex = string.substring(3); // +0x
- factor = 1;
- break;
-
- case '-':
- hex = string.substring(3); // -0x
- factor = -1;
- break;
-
- default:
- hex = string.substring(2); // 0x
- factor = 1;
- break;
- }
-
- BigInteger bigint = new BigInteger(hex, 16);
-
- if(factor == -1)
- return bigint.negate();
-
- return bigint;
- }
-
- throw new JSONException("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
- * @return a JSONException
- */
- public JSONException syntaxError(String message, Throwable cause) {
- return new JSONException(message + this, cause);
- }
-
- /**
- * Constructs a new JSONException with a detail message
- *
- * @param message the detail message
- * @return a JSONException
- */
- public JSONException syntaxError(String message) {
- return new JSONException(message + this);
- }
-
- @Override
- public String toString() {
- return " at index " + index + " [character " + character + " in line " + line + "]";
- }
-
-}
diff --git a/src/main/java/at/syntaxerror/json5/JSONStringify.java b/src/main/java/at/syntaxerror/json5/JSONStringify.java
deleted file mode 100755
index fb820d2..0000000
--- a/src/main/java/at/syntaxerror/json5/JSONStringify.java
+++ /dev/null
@@ -1,345 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2021 SyntaxError404
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package at.syntaxerror.json5;
-
-import java.time.Instant;
-import java.util.Map;
-
-/**
- * A utility class for serializing {@link JSONObject JSONObjects} and
- * {@link JSONArray JSONArrays} into their string representations
- *
- * @author SyntaxError404
- *
- */
-public class JSONStringify {
-
- private JSONStringify() {
- throw new UnsupportedOperationException("Utility class");
- }
-
- /**
- * Converts a JSONObject into its string representation.
- * The indentation factor enables pretty-printing and defines
- * how many spaces (' ') should be placed before each key/value pair.
- * A factor of {@code < 1} disables pretty-printing and discards
- * any optional whitespace characters.
- *
- * {@code indentFactor = 2}:
- *
- * {
- * "key0": "value0",
- * "key1": {
- * "nested": 123
- * },
- * "key2": false
- * }
- *
- *
- * {@code indentFactor = 0}:
- *
- * {"key0":"value0","key1":{"nested":123},"key2":false}
- *
- *
- * @param object the JSONObject
- * @param indentFactor the indentation factor
- * @param options the options for stringifying
- * @return the string representation
- * @since 1.1.0
- */
- public static String toString(JSONObject object, int indentFactor, JSONOptions options) {
- return toString(
- object,
- "",
- Math.max(0, indentFactor),
- options == null ?
- JSONOptions.getDefaultOptions() : options
- );
- }
-
- /**
- * Converts a JSONArray into its string representation.
- * The indentation factor enables pretty-printing and defines
- * how many spaces (' ') should be placed before each value.
- * A factor of {@code < 1} disables pretty-printing and discards
- * any optional whitespace characters.
- *
- * {@code indentFactor = 2}:
- *
- * [
- * "value",
- * {
- * "nested": 123
- * },
- * false
- * ]
- *
- *
- * {@code indentFactor = 0}:
- *
- * ["value",{"nested":123},false]
- *
- *
- * @param array the JSONArray
- * @param indentFactor the indentation factor
- * @param options the options for stringifying
- * @return the string representation
- * @since 1.1.0
- */
- public static String toString(JSONArray array, int indentFactor, JSONOptions options) {
- return toString(
- array,
- "",
- Math.max(0, indentFactor),
- options == null ?
- JSONOptions.getDefaultOptions() : options
- );
- }
-
- /**
- * Converts a JSONObject into its string representation.
- * The indentation factor enables pretty-printing and defines
- * how many spaces (' ') should be placed before each key/value pair.
- * A factor of {@code < 1} disables pretty-printing and discards
- * any optional whitespace characters.
- *
- * {@code indentFactor = 2}:
- *
- * {
- * "key0": "value0",
- * "key1": {
- * "nested": 123
- * },
- * "key2": false
- * }
- *
- *
- * {@code indentFactor = 0}:
- *
- * {"key0":"value0","key1":{"nested":123},"key2":false}
- *
- * This uses the {@link JSONOptions#getDefaultOptions() default options}
- *
- * @param object the JSONObject
- * @param indentFactor the indentation factor
- * @return the string representation
- */
- public static String toString(JSONObject object, int indentFactor) {
- return toString(object, indentFactor, null);
- }
-
- /**
- * Converts a JSONArray into its string representation.
- * The indentation factor enables pretty-printing and defines
- * how many spaces (' ') should be placed before each value.
- * A factor of {@code < 1} disables pretty-printing and discards
- * any optional whitespace characters.
- *
- * {@code indentFactor = 2}:
- *
- * [
- * "value",
- * {
- * "nested": 123
- * },
- * false
- * ]
- *
- *
- * {@code indentFactor = 0}:
- *
- * ["value",{"nested":123},false]
- *
- * This uses the {@link JSONOptions#getDefaultOptions() default options}
- *
- * @param array the JSONArray
- * @param indentFactor the indentation factor
- * @return the string representation
- */
- public static String toString(JSONArray array, int indentFactor) {
- return toString(array, indentFactor, null);
- }
-
- private static String toString(JSONObject object, String indent, int indentFactor, JSONOptions options) {
- StringBuilder sb = new StringBuilder();
-
- String childIndent = indent + " ".repeat(indentFactor);
-
- sb.append('{');
-
- for(Map.Entry entry : object) {
- if(sb.length() != 1)
- sb.append(',');
-
- if(indentFactor > 0)
- sb.append('\n').append(childIndent);
-
- sb.append(quote(entry.getKey(), options))
- .append(':');
-
- if(indentFactor > 0)
- sb.append(' ');
-
- sb.append(toString(entry.getValue(), childIndent, indentFactor, options));
- }
-
- if(indentFactor > 0)
- sb.append('\n').append(indent);
-
- sb.append('}');
-
- return sb.toString();
- }
- private static String toString(JSONArray array, String indent, int indentFactor, JSONOptions options) {
- StringBuilder sb = new StringBuilder();
-
- String childIndent = indent + " ".repeat(indentFactor);
-
- sb.append('[');
-
- for(Object value : array) {
- if(sb.length() != 1)
- sb.append(',');
-
- if(indentFactor > 0)
- sb.append('\n').append(childIndent);
-
- sb.append(toString(value, childIndent, indentFactor, options));
- }
-
- if(indentFactor > 0)
- sb.append('\n').append(indent);
-
- sb.append(']');
-
- return sb.toString();
- }
-
- private static String toString(Object value, String indent, int indentFactor, JSONOptions options) {
- if(value == null)
- return "null";
-
- if(value instanceof JSONObject)
- return toString((JSONObject) value, indent, indentFactor, options);
-
- if(value instanceof JSONArray)
- return toString((JSONArray) value, indent, indentFactor, options);
-
- if(value instanceof String)
- return quote((String) value, options);
-
- if(value instanceof Instant) {
- Instant instant = (Instant) value;
-
- if(options.isStringifyUnixInstants())
- return String.valueOf(instant.getEpochSecond());
-
- return quote(instant.toString(), options);
- }
-
- if(value instanceof Double) {
- double d = (Double) value;
-
- if(!options.isAllowNaN() && Double.isNaN(d))
- throw new JSONException("Illegal NaN in JSON");
-
- if(!options.isAllowInfinity() && Double.isInfinite(d))
- throw new JSONException("Illegal Infinity in JSON");
- }
-
- return String.valueOf(value);
- }
-
- static String quote(String string) {
- return quote(string, null);
- }
-
- private static String quote(String string, JSONOptions options) {
- options = options == null ?
- JSONOptions.getDefaultOptions() : options;
-
- if(string == null || string.isEmpty())
- return options.isQuoteSingle() ? "''" : "\"\"";
-
- final char qt = options.isQuoteSingle() ? '\'' : '"';
-
- StringBuilder quoted = new StringBuilder(string.length() + 2);
-
- quoted.append(qt);
-
- for(char c : string.toCharArray()) {
- if(c == qt) {
- quoted.append('\\');
- quoted.append(c);
- continue;
- }
-
- switch(c) {
- case '\\':
- quoted.append("\\\\");
- break;
- case '\b':
- quoted.append("\\b");
- break;
- case '\f':
- quoted.append("\\f");
- break;
- case '\n':
- quoted.append("\\n");
- break;
- case '\r':
- quoted.append("\\r");
- break;
- case '\t':
- quoted.append("\\t");
- break;
- case 0x0B: // Vertical Tab
- quoted.append("\\v");
- break;
- default:
- // escape non-graphical characters (https://www.unicode.org/versions/Unicode13.0.0/ch02.pdf#G286941)
- switch(Character.getType(c)) {
- case Character.FORMAT:
- case Character.LINE_SEPARATOR:
- case Character.PARAGRAPH_SEPARATOR:
- case Character.CONTROL:
- case Character.PRIVATE_USE:
- case Character.SURROGATE:
- case Character.UNASSIGNED:
- quoted.append("\\u");
- quoted.append(String.format("%04X", c));
- break;
- default:
- quoted.append(c);
- break;
- }
- }
- }
-
- quoted.append(qt);
-
- return quoted.toString();
- }
-
-}
diff --git a/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Array.kt b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Array.kt
new file mode 100644
index 0000000..74e0b1c
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Array.kt
@@ -0,0 +1,63 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 SyntaxError404
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package at.syntaxerror.json5
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+
+
+/**
+ * A JSONArray is an array structure capable of holding multiple values, including other JSONArrays
+ * and [JSONObjects][DecodeJson5Object]
+ *
+ * @author SyntaxError404
+ */
+class DecodeJson5Array {
+
+ fun decode(parser: JSONParser): JsonArray {
+ val content: MutableList = mutableListOf()
+
+ if (parser.nextClean() != '[') {
+ throw parser.createSyntaxException("A JSONArray must begin with '['")
+ }
+ while (true) {
+ var c: Char = parser.nextClean()
+ when (c) {
+ Char.MIN_VALUE -> throw parser.createSyntaxException("A JSONArray must end with ']'")
+ ']' -> break // finish parsing this array
+ else -> parser.back()
+ }
+ val value = parser.nextValue()
+ content.add(value)
+ c = parser.nextClean()
+ when {
+ c == ']' -> break // finish parsing this array
+ c != ',' -> throw parser.createSyntaxException("Expected ',' or ']' after value, got '$c' instead")
+ }
+ }
+
+ return JsonArray(content)
+ }
+
+}
diff --git a/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Object.kt b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Object.kt
new file mode 100644
index 0000000..b53c23e
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/DecodeJson5Object.kt
@@ -0,0 +1,76 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 SyntaxError404
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package at.syntaxerror.json5
+
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+
+/**
+ * A JSONObject is a map (key-value) structure capable of holding multiple values, including other
+ * [JSONArrays][DecodeJson5Array] and JSONObjects
+ *
+ * @author SyntaxError404
+ */
+class DecodeJson5Object(
+ private val j5: Json5Module,
+) {
+
+ fun decode(parser: JSONParser): JsonObject {
+
+ val content: MutableMap = mutableMapOf()
+
+ var c: Char
+ var key: String
+ if (parser.nextClean() != '{') {
+ throw parser.createSyntaxException("A JSONObject must begin with '{'")
+ }
+ while (true) {
+ c = parser.nextClean()
+ key = when (c) {
+ Char.MIN_VALUE -> throw parser.createSyntaxException("A JSONObject must end with '}'")
+ '}' -> break // end of object
+ else -> {
+ parser.back()
+ parser.nextMemberName()
+ }
+ }
+ if (content.containsKey(key)) {
+ throw JSONException("Duplicate key ${j5.stringify.escapeString(key)}")
+ }
+ c = parser.nextClean()
+ if (c != ':') {
+ throw parser.createSyntaxException("Expected ':' after a key, got '$c' instead")
+ }
+ val value = parser.nextValue()
+ content[key] = value
+ c = parser.nextClean()
+ when {
+ c == '}' -> break // end of object
+ c != ',' -> throw parser.createSyntaxException("Expected ',' or '}' after value, got '$c' instead")
+ }
+ }
+
+ return JsonObject(content)
+ }
+}
diff --git a/src/main/java/module-info.java b/src/main/kotlin/at/syntaxerror/json5/JSONException.kt
old mode 100755
new mode 100644
similarity index 68%
rename from src/main/java/module-info.java
rename to src/main/kotlin/at/syntaxerror/json5/JSONException.kt
index f3587fb..14d8836
--- a/src/main/java/module-info.java
+++ b/src/main/kotlin/at/syntaxerror/json5/JSONException.kt
@@ -1,30 +1,42 @@
-/**
- *
+/*
* MIT License
- *
+ *
* Copyright (c) 2021 SyntaxError404
- *
+ *
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
- *
+ *
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
- *
+ *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
- *
*/
-module json5 {
- requires lombok;
-
- exports at.syntaxerror.json5;
-}
\ No newline at end of file
+package at.syntaxerror.json5
+
+/**
+ * An exception used by the JSON5 for Java Library if something went wrong
+ *
+ * @author SyntaxError404
+ * @version 1.0.0
+ */
+open class JSONException(
+ message: String,
+ cause: Throwable? = null,
+) : RuntimeException(message, cause) {
+
+ class SyntaxError(
+ message: String,
+ cause: Throwable? = null,
+ ) : JSONException(message, cause)
+
+}
diff --git a/src/main/kotlin/at/syntaxerror/json5/JSONOptions.kt b/src/main/kotlin/at/syntaxerror/json5/JSONOptions.kt
new file mode 100644
index 0000000..11b48e9
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/JSONOptions.kt
@@ -0,0 +1,68 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 SyntaxError404
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package at.syntaxerror.json5
+
+/**
+ * This class used is used to customize the behaviour of [parsing][JSONParser] and [stringifying][JSONStringify]
+ *
+ * @author SyntaxError404
+ * @since 1.1.0
+ */
+data class JSONOptions(
+ /**
+ * Whether `NaN` should be allowed as a number
+ *
+ * Default: `true`
+ */
+ var allowNaN: Boolean = true,
+
+ /**
+ * Whether `Infinity` should be allowed as a number.
+ * This applies to both `+Infinity` and `-Infinity`
+ *
+ * Default: `true`
+ */
+ var allowInfinity: Boolean = true,
+
+ /**
+ * Whether invalid unicode surrogate pairs should be allowed
+ *
+ * Default: `true`
+ *
+ * *This is a [Parser][JSONParser]-only option*
+ */
+ var allowInvalidSurrogates: Boolean = true,
+
+ /**
+ * Whether string should be single-quoted (`'`) instead of double-quoted (`"`).
+ * This also includes a [JSONObject's][DecodeJson5Object] member names
+ *
+ * Default: `false`
+ *
+ * *This is a [Stringify][JSONStringify]-only option*
+ */
+ var quoteSingle: Boolean = false,
+
+ var indentFactor: UInt = 2u
+)
diff --git a/src/main/kotlin/at/syntaxerror/json5/JSONParser.kt b/src/main/kotlin/at/syntaxerror/json5/JSONParser.kt
new file mode 100644
index 0000000..3eee827
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/JSONParser.kt
@@ -0,0 +1,457 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 SyntaxError404
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package at.syntaxerror.json5
+
+import at.syntaxerror.json5.JSONException.SyntaxError
+import java.io.BufferedReader
+import java.io.Reader
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonPrimitive
+
+/**
+ * A JSONParser is used to convert a source string into tokens, which then are used to construct
+ * [JSONObjects][DecodeJson5Object] and [JSONArrays][DecodeJson5Array]
+ *
+ * The reader is not [closed][Reader.close]
+ *
+ * @author SyntaxError404
+ */
+class JSONParser(
+ reader: Reader,
+ private val j5: Json5Module,
+) {
+
+ private val reader: Reader = if (reader.markSupported()) reader else BufferedReader(reader)
+ /** whether the end of the file has been reached */
+ private var eof: Boolean = false
+ /** whether the current character should be re-read */
+ private var back: Boolean = false
+ /** the absolute position in the string */
+ private var index: Long = -1
+ /** the relative position in the line */
+ private var character: Long = 0
+ /** the line number */
+ private var line: Long = 1
+ /** the previous character */
+ private var previous: Char = Char.MIN_VALUE
+ /** the current character */
+ private var current: Char = Char.MIN_VALUE
+
+ private val nextCleanToDelimiters: String = ",]}"
+
+ private fun more(): Boolean {
+ return if (back || eof) {
+ back && !eof
+ } else peek().code > 0
+ }
+
+ /** Forces the parser to re-read the last character */
+ fun back() {
+ back = true
+ }
+
+ private fun peek(): Char {
+ if (eof) {
+ return Char.MIN_VALUE
+ }
+ val c: Int
+ try {
+ reader.mark(1)
+ c = reader.read()
+ reader.reset()
+ } catch (e: Exception) {
+ throw createSyntaxException("Could not peek from source", e)
+ }
+ return if (c == -1) Char.MIN_VALUE else c.toChar()
+ }
+
+ private operator fun next(): Char {
+ if (back) {
+ back = false
+ return current
+ }
+ val c: Int = try {
+ reader.read()
+ } catch (e: Exception) {
+ throw createSyntaxException("Could not read from source", e)
+ }
+ if (c < 0) {
+ eof = true
+ return Char.MIN_VALUE
+ }
+ previous = current
+ current = c.toChar()
+ index++
+ if (isLineTerminator(current) && (current != '\n' || previous != '\r')) {
+ line++
+ character = 0
+ } else {
+ character++
+ }
+ return current
+ }
+
+ // https://262.ecma-international.org/5.1/#sec-7.3
+ private fun isLineTerminator(c: Char): Boolean {
+ return when (c) {
+ '\n', '\r', '\u2028', '\u2029' -> true
+ else -> false
+ }
+ }
+
+ // https://spec.json5.org/#white-space
+ private fun isWhitespace(c: Char): Boolean {
+ return when (c) {
+ '\t', '\n', '\u000B', Json5EscapeSequence.FormFeed.char,
+ '\r', ' ', '\u00A0', '\u2028', '\u2029', '\uFEFF' -> true
+ else ->
+ // Unicode category "Zs" (space separators)
+ Character.getType(c) == Character.SPACE_SEPARATOR.toInt()
+ }
+ }
+
+ private fun nextMultiLineComment() {
+ while (true) {
+ val n = next()
+ if (n == '*' && peek() == '/') {
+ next()
+ return
+ }
+ }
+ }
+
+ private fun nextSingleLineComment() {
+ while (true) {
+ val n = next()
+ if (isLineTerminator(n) || n.code == 0) {
+ return
+ }
+ }
+ }
+ /**
+ * Reads until encountering a character that is not a whitespace according to the
+ * [JSON5 Specification](https://spec.json5.org/#white-space)
+ *
+ * @return a non-whitespace character, or `0` if the end of the stream has been reached
+ */
+ fun nextClean(): Char {
+ while (true) {
+ if (!more()) {
+ throw createSyntaxException("Unexpected end of data")
+ }
+ val n = next()
+ if (n == '/') {
+ when (peek()) {
+ '*' -> {
+ next()
+ nextMultiLineComment()
+ }
+ '/' -> {
+ next()
+ nextSingleLineComment()
+ }
+ else -> {
+ return n
+ }
+ }
+ } else if (!isWhitespace(n)) {
+ return n
+ }
+ }
+ }
+
+ private fun nextCleanTo(delimiters: String = nextCleanToDelimiters): String {
+ val result = StringBuilder()
+ while (true) {
+ if (!more()) {
+ throw createSyntaxException("Unexpected end of data")
+ }
+ val n = nextClean()
+ if (delimiters.indexOf(n) > -1 || isWhitespace(n)) {
+ back()
+ break
+ }
+ result.append(n)
+ }
+ return result.toString()
+ }
+
+ private fun deHex(c: Char): Int? {
+ return when (c) {
+ in '0'..'9' -> c - '0'
+ in 'a'..'f' -> c - 'a' + 0xA
+ in 'A'..'F' -> c - 'A' + 0xA
+ else -> null
+ }
+ }
+
+ private fun unicodeEscape(member: Boolean, part: Boolean): Char {
+ var value = ""
+ var codepoint = 0
+ for (i in 0..3) {
+ val n = next()
+ value += n
+ val hex = deHex(n)
+ ?: throw createSyntaxException("Illegal unicode escape sequence '\\u$value' in ${if (member) "key" else "string"}")
+ codepoint = codepoint or (hex shl (3 - i shl 2))
+ }
+ if (member && !isMemberNameChar(codepoint.toChar(), part)) {
+ throw createSyntaxException("Illegal unicode escape sequence '\\u$value' in key")
+ }
+ return codepoint.toChar()
+ }
+
+ private fun checkSurrogate(hi: Char, lo: Char) {
+ if (j5.options.allowInvalidSurrogates) {
+ return
+ }
+ if (!Character.isHighSurrogate(hi) || !Character.isLowSurrogate(lo)) {
+ return
+ }
+ if (!Character.isSurrogatePair(hi, lo)) {
+ throw createSyntaxException(
+ String.format(
+ "Invalid surrogate pair: U+%04X and U+%04X",
+ hi, lo
+ )
+ )
+ }
+ }
+
+ // https://spec.json5.org/#prod-JSON5String
+ private fun nextString(quote: Char): String {
+ val result = StringBuilder()
+ var value: String
+ var codepoint: Int
+ var n = 0.toChar()
+ var prev: Char
+ while (true) {
+ if (!more()) {
+ throw createSyntaxException("Unexpected end of data")
+ }
+ prev = n
+ n = next()
+ if (n == quote) {
+ break
+ }
+ if (isLineTerminator(n) && n.code != 0x2028 && n.code != 0x2029) {
+ throw createSyntaxException("Unescaped line terminator in string")
+ }
+ if (n == '\\') {
+ n = next()
+ if (isLineTerminator(n)) {
+ if (n == '\r' && peek() == '\n') {
+ next()
+ }
+ // escaped line terminator/ line continuation
+ continue
+ } else {
+ when (n) {
+ '\'', '"', '\\' -> {
+ result.append(n)
+ continue
+ }
+ 'b' -> {
+ result.append('\b')
+ continue
+ }
+ 'f' -> {
+ result.append(Json5EscapeSequence.FormFeed.char)
+ continue
+ }
+ 'n' -> {
+ result.append('\n')
+ continue
+ }
+ 'r' -> {
+ result.append('\r')
+ continue
+ }
+ 't' -> {
+ result.append('\t')
+ continue
+ }
+ 'v' -> {
+ result.append(0x0B.toChar())
+ continue
+ }
+ '0' -> {
+ val p = peek()
+ if (p.isDigit()) {
+ throw createSyntaxException("Illegal escape sequence '\\0$p'")
+ }
+ result.append(0.toChar())
+ continue
+ }
+ 'x' -> {
+ value = ""
+ codepoint = 0
+ var i = 0
+ while (i < 2) {
+ n = next()
+ value += n
+ val hex = deHex(n)
+ ?: throw createSyntaxException("Illegal hex escape sequence '\\x$value' in string")
+ codepoint = codepoint or (hex shl (1 - i shl 2))
+ ++i
+ }
+ n = codepoint.toChar()
+ }
+ 'u' -> n = unicodeEscape(member = false, part = false)
+ else -> if (n.isDigit()) {
+ throw SyntaxError("Illegal escape sequence '\\$n'")
+ }
+ }
+ }
+ }
+ checkSurrogate(prev, n)
+ result.append(n)
+ }
+ return result.toString()
+ }
+
+ private fun isMemberNameChar(n: Char, isNotEmpty: Boolean): Boolean {
+ if (n == '$' || n == '_' || n.code == 0x200C || n.code == 0x200D) {
+ return true
+ }
+
+ return when (n.category) {
+
+ CharCategory.UPPERCASE_LETTER,
+ CharCategory.LOWERCASE_LETTER,
+ CharCategory.TITLECASE_LETTER,
+ CharCategory.MODIFIER_LETTER,
+ CharCategory.OTHER_LETTER,
+ CharCategory.LETTER_NUMBER -> return true
+
+ CharCategory.NON_SPACING_MARK,
+ CharCategory.COMBINING_SPACING_MARK,
+ CharCategory.DECIMAL_DIGIT_NUMBER,
+ CharCategory.CONNECTOR_PUNCTUATION -> isNotEmpty
+
+ else -> return false
+ }
+ }
+
+ /**
+ * Reads a member name from the source according to the
+ * [JSON5 Specification](https://spec.json5.org/#prod-JSON5MemberName)
+ */
+ fun nextMemberName(): String {
+ val result = StringBuilder()
+ var prev: Char
+ var n = next()
+ if (n == '"' || n == '\'') {
+ return nextString(n)
+ }
+ back()
+ n = 0.toChar()
+ while (true) {
+ if (!more()) {
+ throw createSyntaxException("Unexpected end of data")
+ }
+ val isNotEmpty = result.isNotEmpty()
+ prev = n
+ n = next()
+ if (n == '\\') { // unicode escape sequence
+ n = next()
+ if (n != 'u') {
+ throw createSyntaxException("Illegal escape sequence '\\$n' in key")
+ }
+ n = unicodeEscape(true, isNotEmpty)
+ } else if (!isMemberNameChar(n, isNotEmpty)) {
+ back()
+ break
+ }
+ checkSurrogate(prev, n)
+ result.append(n)
+ }
+ if (result.isEmpty()) {
+ throw createSyntaxException("Empty key")
+ }
+ return result.toString()
+ }
+
+ /**
+ * Reads a value from the source according to the
+ * [JSON5 Specification](https://spec.json5.org/#prod-JSON5Value)
+ */
+ fun nextValue(): JsonElement {
+ when (val n = nextClean()) {
+ '"', '\'' -> {
+ val string = nextString(n)
+ return JsonPrimitive(string)
+ }
+ '{' -> {
+ back()
+ return j5.objectDecoder.decode(this)
+ }
+ '[' -> {
+ back()
+ return j5.arrayDecoder.decode(this)
+ }
+ }
+ back()
+ val string = nextCleanTo()
+ return when {
+ string == "null" -> JsonNull
+ PATTERN_BOOLEAN.matches(string) -> JsonPrimitive(string == "true")
+
+ // val bigint = BigInteger(string)
+ // return bigint
+ PATTERN_NUMBER_INTEGER.matches(string) -> JsonPrimitive(string.toLong())
+
+ PATTERN_NUMBER_FLOAT.matches(string)
+ || PATTERN_NUMBER_NON_FINITE.matches(string) -> {
+ try {
+ JsonPrimitive(string.toDouble())
+ } catch (e: NumberFormatException) {
+ throw createSyntaxException("could not parse number '$string'")
+ }
+ }
+ PATTERN_NUMBER_HEX.matches(string) -> {
+ val hex = string.uppercase().split("0X").joinToString("")
+ JsonPrimitive(hex.toLong(16))
+ }
+ else -> throw JSONException("Illegal value '$string'")
+ }
+ }
+
+ fun createSyntaxException(message: String, cause: Throwable? = null): SyntaxError =
+ SyntaxError("$message, at index $index, character $character, line $line]", cause)
+
+ companion object {
+ private val PATTERN_BOOLEAN =
+ Regex("true|false")
+ private val PATTERN_NUMBER_FLOAT =
+ Regex("[+-]?((0|[1-9]\\d*)(\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?")
+ private val PATTERN_NUMBER_INTEGER =
+ Regex("[+-]?(0|[1-9]\\d*)")
+ private val PATTERN_NUMBER_HEX =
+ Regex("[+-]?0[xX][0-9a-fA-F]+")
+ private val PATTERN_NUMBER_NON_FINITE =
+ Regex("[+-]?(Infinity|NaN)")
+ }
+}
diff --git a/src/main/kotlin/at/syntaxerror/json5/JSONStringify.kt b/src/main/kotlin/at/syntaxerror/json5/JSONStringify.kt
new file mode 100644
index 0000000..71a09b4
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/JSONStringify.kt
@@ -0,0 +1,192 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 SyntaxError404
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package at.syntaxerror.json5
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+
+/**
+ * A utility class for serializing [JSONObjects][DecodeJson5Object] and [JSONArrays][DecodeJson5Array]
+ * into their string representations
+ *
+ * @author SyntaxError404
+ */
+class JSONStringify(
+ private val options: JSONOptions
+) {
+
+ private val quoteToken = if (options.quoteSingle) '\'' else '"'
+ private val emptyString = "$quoteToken$quoteToken"
+ private val indentFactor = options.indentFactor
+
+ /**
+ * Converts a JSONObject into its string representation. The indentation factor enables
+ * pretty-printing and defines how many spaces (' ') should be placed before each key/value pair.
+ * A factor of `< 1` disables pretty-printing and discards any optional whitespace
+ * characters.
+ *
+ * `indentFactor = 2`:
+ * ```
+ * {
+ * "key0": "value0",
+ * "key1": {
+ * "nested": 123
+ * },
+ * "key2": false
+ * }
+ * ```
+ *
+ * `indentFactor = 0`:
+ *
+ * ```
+ * {"key0":"value0","key1":{"nested":123},"key2":false}
+ * ```
+ */
+ fun encodeObject(
+ jsonObject: JsonObject,
+ indent: String = "",
+ ): String {
+ val childIndent = indent + " ".repeat(indentFactor.toInt())
+ val isIndented = indentFactor > 0u
+
+ val sb = StringBuilder()
+ sb.append('{')
+ jsonObject.forEach { (key, value) ->
+ if (sb.length != 1) {
+ sb.append(',')
+ }
+ if (isIndented) {
+ sb.append('\n').append(childIndent)
+ }
+ sb.append(escapeString(key)).append(':')
+ if (isIndented) {
+ sb.append(' ')
+ }
+ sb.append(encode(value, childIndent))
+ }
+ if (isIndented) {
+ sb.append('\n').append(indent)
+ }
+ sb.append('}')
+ return sb.toString()
+ }
+
+ /**
+ * Converts a JSONArray into its string representation. The indentation factor enables
+ * pretty-printing and defines how many spaces (' ') should be placed before each value. A factor
+ * of `< 1` disables pretty-printing and discards any optional whitespace characters.
+ *
+ *
+ * `indentFactor = 2`:
+ * ```
+ * [
+ * "value",
+ * {
+ * "nested": 123
+ * },
+ * false
+ * ]
+ * ```
+ *
+ * `indentFactor = 0`:
+ * ```
+ * ["value",{"nested":123},false]
+ * ```
+ */
+ fun encodeArray(
+ array: JsonArray,
+ indent: String = "",
+ ): String {
+ val childIndent = indent + " ".repeat(indentFactor.toInt())
+ val isIndented = indentFactor > 0u
+
+ val sb = StringBuilder()
+ sb.append('[')
+ for (value in array) {
+ if (sb.length != 1) {
+ sb.append(',')
+ }
+ if (isIndented) {
+ sb.append('\n').append(childIndent)
+ }
+ sb.append(encode(value, childIndent))
+ }
+ if (isIndented) {
+ sb.append('\n').append(indent)
+ }
+ sb.append(']')
+ return sb.toString()
+ }
+
+ private fun encode(
+ value: Any?,
+ indent: String,
+ ): String {
+ return when (value) {
+ null -> "null"
+ is JsonObject -> encodeObject(value, indent)
+ is JsonArray -> encodeArray(value, indent)
+ is String -> escapeString(value)
+ is Double -> {
+ when {
+ !options.allowNaN && value.isNaN() -> throw JSONException("Illegal NaN in JSON")
+ !options.allowInfinity && value.isInfinite() -> throw JSONException("Illegal Infinity in JSON")
+ else -> value.toString()
+ }
+ }
+ else -> value.toString()
+ }
+ }
+
+ fun escapeString(string: String?): String {
+ return if (string.isNullOrEmpty()) {
+ emptyString
+ } else {
+ string
+ .asSequence()
+ .joinToString(
+ separator = "",
+ prefix = quoteToken.toString(),
+ postfix = quoteToken.toString()
+ ) { c: Char ->
+
+ val formattedChar: String? = when (c) {
+ quoteToken -> "\\$quoteToken"
+ in Json5EscapeSequence.escapableChars -> Json5EscapeSequence.asEscapedString(c)
+ else -> when (c.category) {
+ CharCategory.FORMAT,
+ CharCategory.LINE_SEPARATOR,
+ CharCategory.PARAGRAPH_SEPARATOR,
+ CharCategory.CONTROL,
+ CharCategory.PRIVATE_USE,
+ CharCategory.SURROGATE,
+ CharCategory.UNASSIGNED -> String.format("\\u%04X", c)
+ else -> null
+ }
+ }
+ formattedChar ?: c.toString()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/at/syntaxerror/json5/Json5EscapeSequence.kt b/src/main/kotlin/at/syntaxerror/json5/Json5EscapeSequence.kt
new file mode 100644
index 0000000..da35df9
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/Json5EscapeSequence.kt
@@ -0,0 +1,31 @@
+package at.syntaxerror.json5
+
+/** https://spec.json5.org/#escapes */
+enum class Json5EscapeSequence(
+ val char: Char,
+ val escaped: String,
+) {
+ //@formatter:off
+ Apostrophe ( '\u0027', "\\'" ),
+ QuotationMark ( '\u0022', "\\\"" ),
+ ReverseSolidus ( '\u005C', "\\\\" ),
+ Backspace ( '\u0008', "\\b" ),
+ FormFeed ( '\u000C', "\\f" ),
+ LineFeed ( '\u000A', "\\n" ),
+ CarriageReturn ( '\u000D', "\\r" ),
+ HorizontalTab ( '\u0009', "\\t" ),
+ VerticalTab ( '\u000B', "\\v" ),
+ Null ( '\u0000', "\\0" ),
+ //@formatter:on
+ ;
+
+ companion object {
+ private val mapCharToRepresentation = values().associate { it.char to it.escaped }
+
+ val escapableChars = values().map { it.char }
+
+ fun asEscapedString(char: Char): String? = mapCharToRepresentation[char]
+
+ fun isEscapable(char: Char) = mapCharToRepresentation.containsKey(char)
+ }
+}
diff --git a/src/main/kotlin/at/syntaxerror/json5/Json5Module.kt b/src/main/kotlin/at/syntaxerror/json5/Json5Module.kt
new file mode 100644
index 0000000..2367e6a
--- /dev/null
+++ b/src/main/kotlin/at/syntaxerror/json5/Json5Module.kt
@@ -0,0 +1,46 @@
+package at.syntaxerror.json5
+
+
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.io.Reader
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+
+class Json5Module(
+ configure: JSONOptions.() -> Unit = {}
+) {
+ internal val options: JSONOptions = JSONOptions()
+ internal val stringify: JSONStringify = JSONStringify(options)
+
+ internal val arrayDecoder = DecodeJson5Array()
+ internal val objectDecoder = DecodeJson5Object(this)
+
+ init {
+ options.configure()
+ }
+
+ fun decodeObject(string: String): JsonObject = decodeObject(string.reader())
+ fun decodeObject(stream: InputStream): JsonObject = decodeObject(InputStreamReader(stream))
+
+ fun decodeObject(reader: Reader): JsonObject {
+ return reader.use { r ->
+ val parser = JSONParser(r, this)
+ objectDecoder.decode(parser)
+ }
+ }
+
+ fun decodeArray(string: String): JsonArray = decodeArray(string.reader())
+ fun decodeArray(stream: InputStream): JsonArray = decodeArray(InputStreamReader(stream))
+
+ fun decodeArray(reader: Reader): JsonArray {
+ return reader.use { r ->
+ val parser = JSONParser(r, this)
+ arrayDecoder.decode(parser)
+ }
+ }
+
+ fun encodeToString(array: JsonArray) = stringify.encodeArray(array)
+ fun encodeToString(jsonObject: JsonObject) = stringify.encodeObject(jsonObject)
+
+}
diff --git a/src/test/java/json5/UnitTests.java b/src/test/java/json5/UnitTests.java
deleted file mode 100755
index 928a5b5..0000000
--- a/src/test/java/json5/UnitTests.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2021 SyntaxError404
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-package json5;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.time.Instant;
-
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import at.syntaxerror.json5.JSONArray;
-import at.syntaxerror.json5.JSONObject;
-import at.syntaxerror.json5.JSONOptions;
-import at.syntaxerror.json5.JSONParser;
-
-/**
- * @author SyntaxError404
- *
- */
-class UnitTests {
-
- @BeforeAll
- static void setUpBeforeClass() throws Exception {
- // compile regex patterns
- JSONParser.class.toString();
- }
-
- @Test
- void testDoubleQuoted() {
- assertTrue(
- parse("{ a: \"Test \\\" 123\" }")
- .getString("a")
- .equals("Test \" 123")
- );
- }
-
- @Test
- void testSingleQuoted() {
- assertTrue(
- parse("{ a: 'Test \\' 123\' }")
- .getString("a")
- .equals("Test ' 123")
- );
- }
-
- @Test
- void testMixedQuoted() {
- assertTrue(
- parse("{ a: \"Test \\' 123\" }")
- .getString("a")
- .equals("Test ' 123")
- );
- }
-
- @Test
- void testStringify() {
- JSONOptions.setDefaultOptions(
- JSONOptions.builder()
- .stringifyUnixInstants(true)
- .build()
- );
-
- JSONObject json = new JSONObject();
-
- json.set("a", (Object) null);
- json.set("b", false);
- json.set("c", true);
- json.set("d", new JSONObject());
- json.set("e", new JSONArray());
- json.set("f", Double.NaN);
- json.set("g", 123e+45);
- json.set("h", (float) -123e45);
- json.set("i", 123L);
- json.set("j", "Lorem Ipsum");
- json.set("k", Instant.now());
-
- assertEquals(
- json.toString(),
- parse(json.toString()).toString()
- );
- }
-
- @Test
- void testEscapes() {
- assertTrue(
- parse("{ a: \"\\n\\r\\f\\b\\t\\v\\0\\u12Fa\\x7F\" }")
- .getString("a")
- .equals("\n\r\f\b\t\u000B\0\u12Fa\u007F")
- );
- }
-
- @Test
- void testMemberName() {
- assertTrue(
- parse("{ $Lorem\\u0041_Ipsum123指事å—: 0 }")
- .has("$LoremA_Ipsum123指事å—")
- );
- }
-
- @Test
- void testMultiComments() {
- assertTrue(
- parse("/**/{/**/a/**/:/**/'b'/**/}/**/")
- .has("a")
- );
- }
-
- @Test
- void testSingleComments() {
- assertTrue(
- parse("// test\n{ // lorem ipsum\n a: 'b'\n// test\n}// test")
- .has("a")
- );
- }
-
- /** @since 1.1.0 */
- @Test
- void testInstant() {
- assertTrue(
- parse("{a:1338150759534}")
- .isInstant("a")
- );
-
- assertEquals(
- parse("{a:1338150759534}")
- .getLong("a"),
- 1338150759534L
- );
-
- assertEquals(
- parse("{a:'2001-09-09T01:46:40Z'}")
- .getString("a"),
- "2001-09-09T01:46:40Z"
- );
- }
-
- /** @since 1.1.0 */
- @Test
- void testHex() {
- assertEquals(
- 0xCAFEBABEL,
- parse("{a: 0xCAFEBABE}")
- .getLong("a")
- );
- }
-
- @Test
- void testSpecial() {
- assertTrue(
- Double.isNaN(
- parse("{a: +NaN}")
- .getDouble("a")
- )
- );
-
- assertTrue(
- Double.isInfinite(
- parse("{a: -Infinity}")
- .getDouble("a")
- )
- );
- }
-
- JSONObject parse(String str) {
- return new JSONObject(new JSONParser(str));
- }
-
-}
diff --git a/src/test/kotlin/at/syntaxerror/json5/JSONArrayTests.kt b/src/test/kotlin/at/syntaxerror/json5/JSONArrayTests.kt
new file mode 100644
index 0000000..7d3b16d
--- /dev/null
+++ b/src/test/kotlin/at/syntaxerror/json5/JSONArrayTests.kt
@@ -0,0 +1,91 @@
+package at.syntaxerror.json5
+
+import io.kotest.assertions.assertSoftly
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.assertions.withClue
+import io.kotest.core.spec.style.BehaviorSpec
+import io.kotest.matchers.collections.shouldContainInOrder
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldContain
+import io.kotest.property.Arb
+import io.kotest.property.arbitrary.Codepoint
+import io.kotest.property.arbitrary.codepoints
+import io.kotest.property.arbitrary.filter
+import io.kotest.property.checkAll
+import kotlinx.serialization.json.JsonPrimitive
+
+class JSONArrayTests : BehaviorSpec({
+
+ val j5 = Json5Module { }
+
+ Given("a JSON5 array should start with '['") {
+ val validArrayStarter = '['
+
+ And("an array with a valid starting character") {
+ When("the valid array is parsed") {
+
+ val valid = """
+ $validArrayStarter
+ "I'm a string",
+ 10,
+ ]
+ """.trimIndent()
+
+ val result = j5.decodeArray(valid)
+
+ Then("expect the array can be pretty-printed") {
+ val pretty = j5.encodeToString(result)
+ pretty shouldBe
+ //language=JSON5
+ """
+ [
+ "I'm a string",
+ 10
+ ]
+ """.trimIndent()
+ }
+ Then("expect the array can be compact-printed") {
+ //language=JSON5
+ result.toString() shouldBe """["I'm a string",10]"""
+ }
+ Then("expect the array matches an equivalent List") {
+
+ assertSoftly(result) {
+ withClue(joinToString { it.javaClass.simpleName }) {
+ shouldHaveSize(2)
+ shouldContainInOrder(JsonPrimitive("I'm a string"), JsonPrimitive(10))
+ }
+ }
+ }
+ }
+ }
+
+ And("an array with an invalid starting character") {
+ val invalidArrayStarterArb =
+ Arb.codepoints().filter { it != Codepoint(validArrayStarter.code) }
+ When("the invalid array is parsed") {
+
+ Then("expect a syntax exception") {
+ checkAll(invalidArrayStarterArb) { invalidArrayStarter ->
+
+ val invalid = """
+ $invalidArrayStarter
+ "key": "value"
+ ]
+ """.trimIndent()
+
+ val thrown = shouldThrow {
+ j5.decodeArray(invalid)
+ }
+
+ assertSoftly(thrown.message) {
+ shouldContain("must begin with")
+ shouldContain("$validArrayStarter")
+ }
+ }
+ }
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/at/syntaxerror/json5/JSONParserTests.kt b/src/test/kotlin/at/syntaxerror/json5/JSONParserTests.kt
new file mode 100644
index 0000000..5c39d44
--- /dev/null
+++ b/src/test/kotlin/at/syntaxerror/json5/JSONParserTests.kt
@@ -0,0 +1,102 @@
+package at.syntaxerror.json5
+
+import io.kotest.assertions.assertSoftly
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.BehaviorSpec
+import io.kotest.matchers.collections.shouldContainInOrder
+import io.kotest.matchers.collections.shouldHaveSize
+import io.kotest.matchers.comparables.shouldBeEqualComparingTo
+import io.kotest.matchers.nulls.shouldNotBeNull
+import io.kotest.matchers.string.shouldContain
+import io.kotest.matchers.string.shouldMatch
+import io.kotest.matchers.types.shouldBeInstanceOf
+import io.kotest.property.Arb
+import io.kotest.property.Exhaustive
+import io.kotest.property.arbitrary.bind
+import io.kotest.property.arbitrary.positiveLong
+import io.kotest.property.checkAll
+import io.kotest.property.exhaustive.collection
+import java.math.BigInteger
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+import org.intellij.lang.annotations.Language
+
+class JSONParserTests : BehaviorSpec({
+
+ val j5 = Json5Module()
+
+ Given("a valid json5 array") {
+ @Language("JSON5")
+ val valid = """
+ [
+ "I'm a string",
+ 10,
+ ]
+ """.trimIndent()
+ When("JSONParser parses the array as a stream") {
+
+ val parser = JSONParser(valid.reader(), j5)
+ val parsedValue = parser.nextValue()
+
+ Then("expect the value is a JSON Array") {
+ assertSoftly(parsedValue) {
+ shouldBeInstanceOf()
+ shouldHaveSize(2)
+ shouldContainInOrder(JsonPrimitive("I'm a string"), JsonPrimitive(10))
+ }
+ }
+ Then("expect there are no more values") {
+ val thrown = shouldThrow {
+ parser.nextValue()
+ }
+ assertSoftly(thrown.message) {
+ shouldContain("Unexpected end of data")
+ }
+ }
+ }
+ }
+
+ Given("an initial number encoded as a hexadecimal value") {
+
+ When("JSONParser parsers an Json5 array that contains the hexadecimal value") {
+
+ Then("expect the parsed value equals the initial number") {
+
+ val arbHex = Arb.bind(
+ Exhaustive.collection(setOf("+", "", "-")),
+ Exhaustive.collection(setOf("0x", "0X")),
+ Arb.positiveLong(),
+ ) { sign, prefix, long ->
+ val hex = long.toString(16)
+ val expected = BigInteger.valueOf(long).run {
+ if (sign == "-") negate() else abs()
+ }
+ expected.longValueExact() to (sign + prefix + hex)
+ }
+
+ checkAll(arbHex) { (expectedNumber, hexString) ->
+ hexString shouldMatch Regex("[+-]?0[xX][0-9a-fA-F]+")
+
+ val json5Array = """ [ $hexString ] """
+
+ val parser = JSONParser(json5Array.reader(), j5)
+ val parsedValue = parser.nextValue()
+
+ assertSoftly(parsedValue) {
+ shouldBeInstanceOf()
+ shouldHaveSize(1)
+ val parsedHex = elementAt(0)
+ parsedHex.shouldBeInstanceOf()
+ assertSoftly(parsedHex.jsonPrimitive.longOrNull) {
+ shouldNotBeNull()
+ shouldBeEqualComparingTo(expectedNumber)
+ }
+
+ }
+ }
+ }
+ }
+ }
+})
diff --git a/src/test/kotlin/at/syntaxerror/json5/JsonToJson5Test.kt b/src/test/kotlin/at/syntaxerror/json5/JsonToJson5Test.kt
new file mode 100644
index 0000000..024e6be
--- /dev/null
+++ b/src/test/kotlin/at/syntaxerror/json5/JsonToJson5Test.kt
@@ -0,0 +1,119 @@
+package at.syntaxerror.json5
+
+import io.kotest.assertions.json.shouldEqualJson
+import io.kotest.core.spec.style.BehaviorSpec
+import io.kotest.matchers.shouldBe
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.add
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlinx.serialization.json.putJsonArray
+
+class JsonToJson5Test : BehaviorSpec({
+
+ val j5 = Json5Module()
+
+ Given("A string-encoded JSON object") {
+
+ //language=JSON
+ val json = """
+ {
+ "widget": {
+ "debug": "on",
+ "window": {
+ "title": "Sample Konfabulator Widget",
+ "name": "main_window",
+ "width": 500,
+ "height": 500
+ },
+ "image": {
+ "src": "Images/Sun.png",
+ "name": "sun1",
+ "hOffset": 250,
+ "vOffset": 250,
+ "alignment": "center"
+ },
+ "text": {
+ "data": "Click Here",
+ "size": 36,
+ "style": "bold",
+ "name": "text1",
+ "hOffset": 250,
+ "vOffset": 100,
+ "alignment": "center",
+ "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
+ }
+ }
+ }
+ """.trimIndent()
+
+ Then("expect it can be converted to a JSON5 string") {
+ val jsonObject = j5.decodeObject(json)
+ val json5String = j5.encodeToString(jsonObject)
+ json5String shouldEqualJson json
+ }
+ }
+
+
+ Given("A string-encoded JSON5 object") {
+
+ //language=JSON5
+ val json5 = """
+ {
+ // comments
+ unquoted: 'and you can quote me on that',
+ singleQuotes: 'I can use "double quotes" here',
+ lineBreaks: "Look, Mom! \
+ No \\n's!",
+ hexadecimal: 0xdecaf,
+ leadingDecimalPoint: .8675309,
+ andTrailing: 8675309.,
+ positiveSign: +1,
+ trailingComma: 'in objects',
+ andIn: [
+ 'arrays',
+ ],
+ "backwardsCompatible": "with JSON",
+ }
+ """.trimIndent()
+
+ val jsonObject: JsonObject = j5.decodeObject(json5)
+
+ Then("expect it can be parsed to a JsonObject") {
+ jsonObject shouldBe buildJsonObject {
+ put("unquoted", "and you can quote me on that")
+ put("singleQuotes", """I can use "double quotes" here""")
+ put("lineBreaks", """Look, Mom! No \n's!""")
+ put("hexadecimal", 912559)
+ put("leadingDecimalPoint", 0.8675309)
+ put("andTrailing", 8675309.0)
+ put("positiveSign", 1)
+ put("trailingComma", "in objects")
+ putJsonArray("andIn") { add("arrays") }
+ put("backwardsCompatible", "with JSON")
+ }
+ }
+
+ Then("expect it can be converted to a JSON string") {
+
+ val jsonString = j5.encodeToString(jsonObject)
+ //language=JSON5
+ jsonString shouldBe """
+ {
+ "unquoted": "and you can quote me on that",
+ "singleQuotes": "I can use \"double quotes\" here",
+ "lineBreaks": "Look, Mom! No \\n's!",
+ "hexadecimal": 912559,
+ "leadingDecimalPoint": 0.8675309,
+ "andTrailing": 8675309.0,
+ "positiveSign": 1,
+ "trailingComma": "in objects",
+ "andIn": [
+ "arrays"
+ ],
+ "backwardsCompatible": "with JSON"
+ }
+ """.trimIndent()
+ }
+ }
+})
diff --git a/src/test/kotlin/at/syntaxerror/json5/UnitTests.kt b/src/test/kotlin/at/syntaxerror/json5/UnitTests.kt
new file mode 100644
index 0000000..22b47ad
--- /dev/null
+++ b/src/test/kotlin/at/syntaxerror/json5/UnitTests.kt
@@ -0,0 +1,235 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2021 SyntaxError404
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package at.syntaxerror.json5
+
+import java.time.Instant
+import java.util.stream.Stream
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.contentOrNull
+import kotlinx.serialization.json.doubleOrNull
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.longOrNull
+import kotlinx.serialization.json.put
+import kotlinx.serialization.json.putJsonArray
+import kotlinx.serialization.json.putJsonObject
+import org.intellij.lang.annotations.Language
+import org.junit.jupiter.api.Assertions.assertAll
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.DynamicTest
+import org.junit.jupiter.api.DynamicTest.dynamicTest
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestFactory
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+import org.junit.jupiter.params.provider.ValueSource
+
+/**
+ * @author SyntaxError404
+ */
+internal class UnitTests {
+
+ private val j5 = Json5Module()
+
+ @ParameterizedTest(name = "{index}: {0}")
+ @CsvSource(
+ useHeadersInDisplayName = true,
+ delimiter = '|',
+ quoteCharacter = Char.MIN_VALUE, // prevent JUnit from interfering with our quotes
+ textBlock =
+ """
+Title | Input value | Expected value
+#-------------------------------------|-----------------|-----------------
+double quote in double-quoted string | "Test \" 123" | Test " 123
+double quote in single-quoted string | 'Test \" 123' | Test " 123
+single quote in double-quoted string | "Test \' 123" | Test ' 123
+single quote in single-quoted string | 'Test \' 123' | Test ' 123
+"""
+ )
+ fun testDoubleQuoted(title: String, inputValue: String, expectedValue: String) {
+ assertTrue(title.isNotEmpty(), "dummy var - used for test name")
+
+ val parsedObject = j5.decodeObject("""{ a: $inputValue }""")
+
+ assertTrue(parsedObject.containsKey("a"))
+ assertEquals(expectedValue, parsedObject["a"]?.jsonPrimitive?.contentOrNull)
+ }
+
+ @Test
+ fun testStringify() {
+ val jsonObject = buildJsonObject {
+ put("a", null as String?)
+ put("b", false)
+ put("c", true)
+ putJsonObject("d") {}
+ putJsonArray("e") {}
+ put("f", Double.NaN)
+ put("g", 123e+45)
+ put("h", (-123e45).toFloat())
+ put("i", 123L)
+ put("j", "Lorem Ipsum")
+ put("k", Instant.ofEpochSecond(1639908193).toString())
+ }
+
+ @Language("JSON5")
+ val expected =
+ """
+ {
+ "a": null,
+ "b": false,
+ "c": true,
+ "d": {
+ },
+ "e": [
+ ],
+ "f": NaN,
+ "g": 1.23E47,
+ "h": -Infinity,
+ "i": 123,
+ "j": "Lorem Ipsum",
+ "k": "2021-12-19T10:03:13Z"
+ }
+ """.trimIndent()
+ // TODO set up Instant encode/decode
+ // "k": 1639908193
+ assertAll(
+ { assertEquals(expected, j5.encodeToString(jsonObject)) },
+ {
+ val parsedValue = j5.decodeObject(expected)
+ assertEquals(expected, j5.encodeToString(parsedValue))
+ },
+ )
+ }
+
+ @TestFactory
+ fun `test escaped characters`(): Stream {
+ return listOf(
+ """ \n """ to '\n',
+ """ \r """ to '\r',
+ """ \u000c """ to '\u000c',
+ """ \b """ to '\b',
+ """ \t """ to '\t',
+ """ \v """ to '\u000B',
+ """ \0 """ to '\u0000',
+ """ \u12Fa """ to '\u12Fa',
+ """ \u007F """ to '\u007F',
+ )
+ .map { (input, expectedChar) -> input.trim() to expectedChar }
+ .map { (input, expectedChar) ->
+ dynamicTest("expect escaped char '$input is mapped to actual char value") {
+ val parsedValue = j5.decodeObject("""{ a: "$input" }""")
+ assertTrue(parsedValue.containsKey("a"))
+ assertEquals(expectedChar.toString(), parsedValue["a"]?.jsonPrimitive?.contentOrNull)
+ }
+ }.stream()
+ }
+
+ @Test
+ fun testEscapes() {
+
+ val inputValue = """\n\r\u000c\b\t\v\0\u12Fa\x7F"""
+ val expectedValue = "\n\r\u000c\b\t\u000B\u0000\u12Fa\u007F"
+
+ val parsedValue = j5.decodeObject("""{ a: "$inputValue" }""")
+
+ assertTrue(parsedValue.containsKey("a"))
+ assertEquals(expectedValue, parsedValue["a"]?.jsonPrimitive?.contentOrNull)
+ }
+
+ @Test
+ fun testMemberName() {
+ // note: requires UTF-8
+
+ val inputKey = "\$Lorem\\u0041_Ipsum123指事å—"
+ val expectedKey = "\$LoremA_Ipsum123指事å—"
+
+ val parsedValue = j5.decodeObject("{ $inputKey: 0 }")
+
+ assertTrue(parsedValue.containsKey(expectedKey))
+ assertEquals(0, parsedValue[expectedKey]?.jsonPrimitive?.longOrNull)
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ //language=JSON5
+ strings = [
+ """
+ // test
+ { // lorem ipsum
+ a: 'b'
+ // test
+ }// test
+ """,
+ """
+ /**/{
+ /**/ a /**/: /**/'b'
+ /**/
+ }/**/
+ """,
+ ]
+ )
+ fun testComments(inputJson: String) {
+ val parsedValue = j5.decodeObject(inputJson)
+
+ assertTrue(parsedValue.containsKey("a"))
+ assertEquals("b", parsedValue["a"]?.jsonPrimitive?.contentOrNull)
+ }
+
+ @Test
+ fun testHex() {
+
+ val parsedObject = j5.decodeObject("""{ a: 0xCAFEBABE }""")
+
+ assertTrue(parsedObject.containsKey("a"))
+ val actualValue = parsedObject["a"]?.jsonPrimitive?.longOrNull
+ assertEquals(0xCAFEBABE, actualValue)
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["NaN", "-NaN", "+NaN"])
+ fun `expect NaN value is parsed to NaN Double`(nanValue: String) {
+
+ val jsonString = """ { a: $nanValue } """
+ val parsedObject = j5.decodeObject(jsonString)
+
+ assertTrue(parsedObject.containsKey("a"))
+ assertTrue(
+ parsedObject["a"]?.jsonPrimitive?.doubleOrNull?.isNaN() == true
+ )
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["Infinity", "-Infinity", "+Infinity"])
+ fun `expect Infinity value is parsed to Infinite Double`(nanValue: String) {
+
+ val jsonString = """ { a: $nanValue } """
+ val parsedObject = j5.decodeObject(jsonString)
+
+ assertTrue(parsedObject.containsKey("a"))
+ assertTrue(
+ parsedObject["a"]?.jsonPrimitive?.doubleOrNull?.isInfinite() == true
+ )
+ }
+
+}