perf: use json5 library

https://github.com/lisonge/kotlin-json5
This commit is contained in:
lisonge 2024-08-14 19:46:44 +08:00
parent a6cde2094d
commit d41fb7f469
10 changed files with 3 additions and 814 deletions

View File

@ -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)
}

View File

@ -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" }

1
json5/.gitignore vendored
View File

@ -1 +0,0 @@
/build

View File

@ -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)
}
}
}
}

View File

@ -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]"
}
}
}
}
}
}

View File

@ -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<String, JsonElement>()
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<JsonElement>()
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++
}
}
}
}
}

View File

@ -1,15 +0,0 @@
package li.songe.json5
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.serializer
inline fun <reified T> Json.encodeToJson5String(value: T): String {
return Json5.encodeToString(
encodeToJsonElement(serializersModule.serializer(), value),
)
}
inline fun <reified T> Json.decodeFromJson5String(value: String): T {
return decodeFromJsonElement<T>(Json5.parseToJson5Element(value))
}

View File

@ -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
}

View File

@ -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")
}
}

View File

@ -22,5 +22,3 @@ dependencyResolutionManagement {
maven("https://jitpack.io")
}
}
include(":json5")