From d41fb7f4693f0ff5f273de4922a1ed1f88a90fed Mon Sep 17 00:00:00 2001 From: lisonge Date: Wed, 14 Aug 2024 19:46:44 +0800 Subject: [PATCH] perf: use json5 library https://github.com/lisonge/kotlin-json5 --- app/build.gradle.kts | 3 +- gradle/libs.versions.toml | 1 + json5/.gitignore | 1 - json5/build.gradle.kts | 22 - .../commonMain/kotlin/li/songe/json5/Json5.kt | 64 --- .../kotlin/li/songe/json5/Json5Decoder.kt | 539 ------------------ .../kotlin/li/songe/json5/JsonExt.kt | 15 - .../commonMain/kotlin/li/songe/json5/Util.kt | 149 ----- .../kotlin/li/songe/json5/Json5Test.kt | 21 - settings.gradle.kts | 2 - 10 files changed, 3 insertions(+), 814 deletions(-) delete mode 100644 json5/.gitignore delete mode 100644 json5/build.gradle.kts delete mode 100644 json5/src/commonMain/kotlin/li/songe/json5/Json5.kt delete mode 100644 json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt delete mode 100644 json5/src/commonMain/kotlin/li/songe/json5/JsonExt.kt delete mode 100644 json5/src/commonMain/kotlin/li/songe/json5/Util.kt delete mode 100644 json5/src/commonTest/kotlin/li/songe/json5/Json5Test.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8f12cf5..0aeb5e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -189,7 +189,6 @@ composeCompiler { dependencies { implementation(project(mapOf("path" to ":selector"))) - implementation(project(mapOf("path" to ":json5"))) implementation(libs.androidx.appcompat) implementation(libs.androidx.core.ktx) @@ -259,4 +258,6 @@ dependencies { implementation(libs.toaster) implementation(libs.permissions) + + implementation(libs.json5) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5178be..763f9f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,7 @@ reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.3.0" exp4j = { module = "net.objecthunter:exp4j", version = "0.4.8" } toaster = { module = "com.github.getActivity:Toaster", version = "12.6" } permissions = { module = "com.github.getActivity:XXPermissions", version = "18.63" } +json5 = { module = "io.github.lisonge:json5", version = "0.0.2" } [plugins] android_library = { id = "com.android.library", version.ref = "android" } diff --git a/json5/.gitignore b/json5/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/json5/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/json5/build.gradle.kts b/json5/build.gradle.kts deleted file mode 100644 index 5b8922e..0000000 --- a/json5/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ -plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.kotlin.serialization) -} - -kotlin { - jvm() - - sourceSets { - val commonMain by getting { - dependencies { - api(libs.kotlinx.serialization.json) - } - } - val commonTest by getting { - dependencies { - implementation(libs.kotlin.test) - implementation(libs.kotlinx.serialization.json) - } - } - } -} diff --git a/json5/src/commonMain/kotlin/li/songe/json5/Json5.kt b/json5/src/commonMain/kotlin/li/songe/json5/Json5.kt deleted file mode 100644 index fd6bd60..0000000 --- a/json5/src/commonMain/kotlin/li/songe/json5/Json5.kt +++ /dev/null @@ -1,64 +0,0 @@ -package li.songe.json5 - -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive - -object Json5 { - fun parseToJson5Element(string: String): JsonElement { - return Json5Decoder(string).read() - } - - fun encodeToString(element: JsonElement, indent: Int = 2) = encodeToString(element, indent, 0) - - private fun encodeToString(element: JsonElement, indent: Int = 2, depth: Int = 0): String { - val lineSeparator = if (indent == 0) "" else "\n" - val keySeparator = if (indent == 0) ":" else ": " - val prefixSpaces = if (indent == 0) "" else " ".repeat(indent * (depth + 1)) - val closingSpaces = if (indent == 0) "" else " ".repeat(indent * depth) - - return when (element) { - is JsonPrimitive -> { - if (element.isString) { - stringifyString(element.content) - } else { - element.content - } - } - - is JsonObject -> { - if (element.isEmpty()) { - "{}" - } else { - element.entries.joinToString(",$lineSeparator") { (key, value) -> - "${prefixSpaces}${stringifyKey(key)}${keySeparator}${ - encodeToString( - value, - indent, - depth + 1 - ) - }" - }.let { - "{$lineSeparator$it$lineSeparator$closingSpaces}" - } - } - } - - is JsonArray -> { - if (element.isEmpty()) { - "[]" - } else { - element.joinToString(",$lineSeparator") { - "${prefixSpaces}${encodeToString(it, indent, depth + 1)}" - }.let { - "[$lineSeparator$it$lineSeparator$closingSpaces]" - } - } - } - } - } - -} - - diff --git a/json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt b/json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt deleted file mode 100644 index 6010712..0000000 --- a/json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt +++ /dev/null @@ -1,539 +0,0 @@ -package li.songe.json5 - -import kotlinx.serialization.json.* -import java.lang.StringBuilder -import kotlin.collections.set -import kotlin.let -import kotlin.ranges.contains -import kotlin.text.endsWith -import kotlin.text.getOrNull -import kotlin.text.substring -import kotlin.text.toDouble -import kotlin.text.toInt -import kotlin.text.toLong -import kotlin.text.trimEnd - -// https://spec.json5.org/ -internal class Json5Decoder(private val input: CharSequence) { - private var i = 0 - private val char: Char? - get() = input.getOrNull(i) - private val end: Boolean - get() = i >= input.length - - private fun stop(): Nothing { - if (end) { - error("Unexpected Char: EOF") - } - error("Unexpected Char: $char at index $i") - } - - fun read(): JsonElement { - val root = i == 0 - readUseless() - val element = when (char) { - '{' -> readObject() - '[' -> readArray() - '"', '\'' -> JsonPrimitive(readString()) - in '0'..'9', '-', '+', '.', 'N', 'I' -> JsonPrimitive(readNumber()) - 't' -> { // true - i++ - next('r') - next('u') - next('e') - JsonPrimitive(true) - } - - 'f' -> { // false - i++ - next('a') - next('l') - next('s') - next('e') - JsonPrimitive(false) - } - - 'n' -> { // null - i++ - next('u') - next('l') - next('l') - JsonNull - } - - else -> stop() - } - if (root) { - readUseless() - if (!end) { - stop() - } - } - return element - } - - private fun next(c: Char) { - if (c == char) { - i++ - return - } - stop() - } - - private fun readObject(): JsonObject { - i++ - readUseless() - if (char == '}') { - i++ - return JsonObject(emptyMap()) - } - val map = mutableMapOf() - while (true) { - readUseless() - val key = readObjectKey() - readUseless() - next(':') - readUseless() - val value = read() - map[key] = value - readUseless() - if (char == '}') { - i++ - break - } else if (char == ',') { - i++ - readUseless() - if (char == '}') { - i++ - break - } - } else { - stop() - } - } - return JsonObject(map) - } - - private fun readObjectKey(): String { - val c = char - if (c == '\'' || c == '"') { - return readString() - } - val sb = StringBuilder() - if (c == '\\') { - i++ - next('u') - repeat(4) { - if (!isHexDigit(char)) { - stop() - } - i++ - } - val n = input.substring(i - 4, i).toInt(16).toChar() - if (!isIdStartChar(n)) { - stop() - } - sb.append(n) - } else if (!isIdStartChar(c)) { - stop() - } else { - sb.append(c) - } - i++ - while (!end) { - if (char == '\\') { - i++ - next('u') - repeat(4) { - if (!isHexDigit(char)) { - stop() - } - i++ - } - val n = input.substring(i - 4, i).toInt(16).toChar() - if (!isIdContinueChar(n)) { - stop() - } - sb.append(n) - } else if (isIdContinueChar(char)) { - sb.append(char) - i++ - } else { - break - } - } - return sb.toString() - } - - private fun readArray(): JsonArray { - i++ - readUseless() - if (char == ']') { - i++ - return JsonArray(emptyList()) - } - val list = mutableListOf() - while (true) { - readUseless() - list.add(read()) - readUseless() - if (char == ']') { - i++ - break - } else if (char == ',') { - i++ - readUseless() - if (char == ']') { - i++ - break - } - } else { - stop() - } - } - return JsonArray(list) - } - - private fun readString(): String { - val wrapChar = char!! - i++ - val sb = StringBuilder() - while (true) { - when (char) { - null -> stop() - wrapChar -> { - i++ - break - } - - '\\' -> { - i++ - when (char) { - null -> stop() - wrapChar -> { - sb.append(wrapChar) - i++ - } - - 'x' -> { - i++ - repeat(2) { - if (!isHexDigit(char)) { - stop() - } - i++ - } - val hex = input.substring(i - 2, i) - sb.append(hex.toInt(16).toChar()) - } - - 'u' -> { - i++ - repeat(4) { - if (!isHexDigit(char)) { - stop() - } - i++ - } - val hex = input.substring(i - 4, i) - sb.append(hex.toInt(16).toChar()) - } - - '\'' -> { - sb.append('\'') - i++ - } - - '\"' -> { - sb.append('\"') - i++ - } - - '\\' -> { - sb.append('\\') - i++ - } - - 'b' -> { - sb.append('\b') - i++ - } - - 'f' -> { - sb.append('\u000C') - i++ - } - - 'n' -> { - sb.append('\n') - i++ - } - - 'r' -> { - sb.append('\r') - i++ - } - - 't' -> { - sb.append('\t') - i++ - } - - 'v' -> { - sb.append('\u000B') - i++ - } - - '0' -> { - sb.append('\u0000') - i++ - if (isDigit(char)) { - stop() - } - } - - // multiline string - '\u000D' -> {// \r - i++ - if (char == '\u000A') {// \n - i++ - } - } - - // multiline string - '\u000A', '\u2028', '\u2029' -> { - i++ - } - - in '1'..'9' -> stop() - - else -> { - sb.append(char) - } - } - } - - else -> { - sb.append(char) - i++ - } - } - } - return sb.toString() - } - - private fun readNumber(signal: Boolean = false): Number { - return when (char) { - '-' -> { - if (!signal) { - i++ - val n = readNumber(true) - if (n is Double) { - return -n - } - if (n is Long) { - return -n - } - if (n is Int) { - return -n - } - stop() - } else { - stop() - } - } - - '+' -> { - if (!signal) { - i++ - return readNumber(true) - } else { - stop() - } - } - - 'N' -> {// NaN - i++ - next('a') - next('N') - Double.NaN - } - - 'I' -> {// Infinity - i++ - next('n') - next('f') - next('i') - next('n') - next('i') - next('t') - next('y') - Double.POSITIVE_INFINITY - } - - '.' -> { - var start = i - i++ - readInteger() - val numPart = input.substring(start, i).trimEnd('0').let { - if (it == ".") { // .0 -> 0 - "0" - } else { - it - } - } - if (numPart == "0") { - 0L - } else { - if (isPowerStartChar(char)) { - start = i + 1 - readNumberPower() - val power = input.substring(start, i) - (numPart + power).toDouble() - } else { - input.substring(start, i).toDouble() - } - } - } - - in '0'..'9' -> { - var start = i - var hasHex = false - if (char == '0') { // 0x11 - i++ - if (isDigit(char)) {// not allow 00 01 - stop() - } else if (isHexStartChar(char)) { - i++ - hasHex = true - } - } - if (hasHex) { - if (!isHexDigit(char)) { - stop() - } - i++ - while (!end && isHexDigit(char)) { - i++ - } - input.substring(start + 2, i).toLong(16) - } else { - var hasPoint = false // 1.2 - while (!end) { - if (char == '.') { - if (!hasPoint) { - hasPoint = true - } else { - stop() - } - } else if (!isDigit(char)) { - break - } - i++ - } - val hasEndPoint = hasPoint && input[i - 1] == '.' // kotlin not support 1. - val numPart = if (hasEndPoint) { - hasPoint = false - input.substring(start, i - 1) // 1. -> 1 - } else { - if (hasPoint) { - input.substring(start, i).trimEnd('0').let { // 1.10 -> 1.1, 1.0 -> 1. - if (it.endsWith('.')) { // 1. -> 1 - hasPoint = false - it.substring(0, it.length - 1) - } else { - it - } - } - } else { - input.substring(start, i) - } - } - if (isPowerStartChar(char)) { - start = i - readNumberPower() - val power = input.substring(start, i) - (numPart + power).toDouble() - } else { - if (hasPoint) { - numPart.toDouble() - } else { - numPart.run { toLongOrNull() ?: toDouble() } - } - } - } - } - - else -> stop() - } - } - - private fun readInteger() { - val start = i - while (isDigit(char)) { - i++ - } - if (start == i) { - stop() - } - } - - private fun readNumberPower() { - i++ - if (char == '-' || char == '+') { - i++ - } - readInteger() - } - - private fun readUseless() { - while (true) { - val oldIndex = i - readCommentOrWhitespace() - if (oldIndex == i) { - return - } - } - } - - private fun readCommentOrWhitespace() { - when { - char == '/' -> { - i++ - when (char) { - '/' -> { - i++ - while (!isNewLine(char) && !end) { - i++ - } - } - - '*' -> { - i++ - while (true) { - when (char) { - null -> stop() - '*' -> { - if (input.getOrNull(i + 1) == '/') { - i += 2 - break - } - } - } - i++ - } - } - - else -> stop() - } - } - - isWhiteSpace(char) -> { - i++ - while (isWhiteSpace(char)) { - i++ - } - } - } - } -} diff --git a/json5/src/commonMain/kotlin/li/songe/json5/JsonExt.kt b/json5/src/commonMain/kotlin/li/songe/json5/JsonExt.kt deleted file mode 100644 index ee4aa87..0000000 --- a/json5/src/commonMain/kotlin/li/songe/json5/JsonExt.kt +++ /dev/null @@ -1,15 +0,0 @@ -package li.songe.json5 - -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.serializer - -inline fun Json.encodeToJson5String(value: T): String { - return Json5.encodeToString( - encodeToJsonElement(serializersModule.serializer(), value), - ) -} - -inline fun Json.decodeFromJson5String(value: String): T { - return decodeFromJsonElement(Json5.parseToJson5Element(value)) -} diff --git a/json5/src/commonMain/kotlin/li/songe/json5/Util.kt b/json5/src/commonMain/kotlin/li/songe/json5/Util.kt deleted file mode 100644 index 4037a0a..0000000 --- a/json5/src/commonMain/kotlin/li/songe/json5/Util.kt +++ /dev/null @@ -1,149 +0,0 @@ -package li.songe.json5 - -import kotlin.text.category - -private val unicodeLetterCategories = hashSetOf( - CharCategory.UPPERCASE_LETTER, - CharCategory.LOWERCASE_LETTER, - CharCategory.TITLECASE_LETTER, - CharCategory.MODIFIER_LETTER, - CharCategory.OTHER_LETTER, - CharCategory.LETTER_NUMBER, -) - -private val unicodeIdCategories = hashSetOf( - CharCategory.NON_SPACING_MARK, - CharCategory.COMBINING_SPACING_MARK, - CharCategory.DECIMAL_DIGIT_NUMBER, - CharCategory.CONNECTOR_PUNCTUATION, -) - -internal fun isIdStartChar(c: Char?): Boolean { - c ?: return false - return c.category in unicodeLetterCategories || c == '_' || c == '$' -} - -internal fun isIdContinueChar(c: Char?): Boolean { - c ?: return false - return isIdStartChar(c) || c.category in unicodeIdCategories || c == '\u200C' || c == '\u200D' -} - -internal fun isDigit(c: Char?): Boolean { - c ?: return false - return c in '0'..'9' -} - -internal fun isHexDigit(c: Char?): Boolean { - c ?: return false - return (c in '0'..'9') || (c in 'A'..'F') || (c in 'a'..'f') -} - -internal fun isPowerStartChar(c: Char?): Boolean { - c ?: return false - return c == 'e' || c == 'E' -} - -internal fun isHexStartChar(c: Char?): Boolean { - c ?: return false - return c == 'x' || c == 'X' -} - -internal fun isWhiteSpace(c: Char?): Boolean { - c ?: return false - return when (c) { - '\u0009' -> true - in '\u000A'..'\u000D' -> true - '\u0020' -> true - '\u00A0' -> true - '\u2028' -> true - '\u2029' -> true - '\uFEFF' -> true - - '\u1680' -> true - in '\u2000'..'\u200A' -> true - '\u202F' -> true - '\u205F' -> true - '\u3000' -> true - else -> false - } -} - -internal fun isNewLine(c: Char?): Boolean { - c ?: return false - return when (c) { - '\u000A' -> true - '\u000D' -> true - '\u2028' -> true - '\u2029' -> true - else -> false - } -} - -private val escapeReplacements = hashMapOf( - '\\' to "\\\\", - '\b' to "\\b", - '\u000C' to "\\f", - '\n' to "\\n", - '\r' to "\\r", - '\t' to "\\t", - '\u000B' to "\\v", - '\u0000' to "\\0", - '\u2028' to "\\u2028", - '\u2029' to "\\u2029", -) - -internal fun stringifyString(value: String, singleQuote: Boolean = true): String { - // https://github.com/json5/json5/blob/main/lib/stringify.js - val wrapChar = if (singleQuote) '\'' else '"' - val sb = StringBuilder() - sb.append(wrapChar) - value.forEachIndexed { i, c -> - when { - c == wrapChar -> { - sb.append("\\$wrapChar") - } - - c == '\u0000' -> { - if (isDigit(value.getOrNull(i + 1))) { - // "\u00002" -> \x002 - sb.append("\\x00") - } else { - sb.append("\\0") - } - } - - c in escapeReplacements.keys -> { - sb.append(escapeReplacements[c]) - } - - c.code in 0..0xf -> { - sb.append("\\x0" + c.code.toString(16)) - } - - c.code in 0..0x1f -> { - sb.append("\\x" + c.code.toString(16)) - } - - else -> { - sb.append(c) - } - } - } - sb.append(wrapChar) - return sb.toString() -} - -internal fun stringifyKey(key: String, singleQuote: Boolean = true): String { - if (key.isEmpty()) { - return stringifyString(key, singleQuote) - } - if (!isIdStartChar(key[0])) { - return stringifyString(key, singleQuote) - } - for (c in key) { - if (!isIdContinueChar(c)) { - return stringifyString(key, singleQuote) - } - } - return key -} diff --git a/json5/src/commonTest/kotlin/li/songe/json5/Json5Test.kt b/json5/src/commonTest/kotlin/li/songe/json5/Json5Test.kt deleted file mode 100644 index 61d12c4..0000000 --- a/json5/src/commonTest/kotlin/li/songe/json5/Json5Test.kt +++ /dev/null @@ -1,21 +0,0 @@ -package li.songe.json5 - -import kotlin.test.Test - -class Json5Test { - - @Test - fun parse() { - // https://github.com/json5/json5/blob/main/test/parse.js - val element = Json5.parseToJson5Element("[1,2,3,'\\x002\\n']/*23*///1") - println("element: $element") - } - - @Test - fun format() { - val element = Json5.parseToJson5Element("{'a-1':1,b:{c:['d',{f:233}]}}") - println("element: $element") - val formatted = Json5.encodeToString(element, 2) - println("formatted:\n$formatted") - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index e49f1f7..d39139b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,5 +22,3 @@ dependencyResolutionManagement { maven("https://jitpack.io") } } - -include(":json5")