perf: json5 by kotlin
Some checks are pending
Build-Apk / build (push) Waiting to run

This commit is contained in:
lisonge 2024-08-13 22:38:49 +08:00
parent c78de4e513
commit a6cde2094d
19 changed files with 844 additions and 132 deletions

View File

@ -189,6 +189,8 @@ composeCompiler {
dependencies { dependencies {
implementation(project(mapOf("path" to ":selector"))) implementation(project(mapOf("path" to ":selector")))
implementation(project(mapOf("path" to ":json5")))
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)

View File

@ -16,8 +16,8 @@ import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long import kotlinx.serialization.json.long
import li.songe.gkd.service.checkSelector import li.songe.gkd.service.checkSelector
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.json5ToJson
import li.songe.gkd.util.toast import li.songe.gkd.util.toast
import li.songe.json5.Json5
import li.songe.selector.Selector import li.songe.selector.Selector
import net.objecthunter.exp4j.Expression import net.objecthunter.exp4j.Expression
import net.objecthunter.exp4j.ExpressionBuilder import net.objecthunter.exp4j.ExpressionBuilder
@ -798,8 +798,9 @@ data class RawSubscription(
} }
fun parse(source: String, json5: Boolean = true): RawSubscription { fun parse(source: String, json5: Boolean = true): RawSubscription {
val text = if (json5) json5ToJson(source) else source val element =
val subscription = jsonToSubscriptionRaw(json.parseToJsonElement(text).jsonObject) if (json5) Json5.parseToJson5Element(source) else json.parseToJsonElement(source)
val subscription = jsonToSubscriptionRaw(element.jsonObject)
subscription.categories.findDuplicatedItem { v -> v.key }?.let { v -> subscription.categories.findDuplicatedItem { v -> v.key }?.let { v ->
error("id=${subscription.id}, duplicated category: key=${v.key}") error("id=${subscription.id}, duplicated category: key=${v.key}")
} }
@ -840,9 +841,8 @@ data class RawSubscription(
return a return a
} }
fun parseRawApp(source: String, json5: Boolean = true): RawApp { fun parseRawApp(source: String): RawApp {
val text = if (json5) json5ToJson(source) else source return parseApp(Json5.parseToJson5Element(source).jsonObject)
return parseApp(json.parseToJsonElement(text).jsonObject)
} }
fun parseGroup(jsonObject: JsonObject): RawAppGroup { fun parseGroup(jsonObject: JsonObject): RawAppGroup {
@ -853,14 +853,12 @@ data class RawSubscription(
return g return g
} }
fun parseRawGroup(source: String, json5: Boolean = true): RawAppGroup { fun parseRawGroup(source: String): RawAppGroup {
val text = if (json5) json5ToJson(source) else source return parseGroup(Json5.parseToJson5Element(source).jsonObject)
return parseGroup(json.parseToJsonElement(text).jsonObject)
} }
fun parseRawGlobalGroup(source: String, json5: Boolean = true): RawGlobalGroup { fun parseRawGlobalGroup(source: String): RawGlobalGroup {
val text = if (json5) json5ToJson(source) else source val g = jsonToGlobalGroups(Json5.parseToJson5Element(source).jsonObject, 0)
val g = jsonToGlobalGroups(json.parseToJsonElement(text).jsonObject, 0)
g.rules.findDuplicatedItem { v -> v.key }?.let { v -> g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
error("duplicated global rule: key=${v.key}") error("duplicated global rule: key=${v.key}")
} }

View File

@ -75,15 +75,15 @@ import li.songe.gkd.util.LocalMainViewModel
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.encodeToJson5String import li.songe.json5.encodeToJson5String
import li.songe.gkd.util.getGroupRawEnable import li.songe.gkd.util.getGroupRawEnable
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.json5ToJson
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
import li.songe.gkd.util.throttle import li.songe.gkd.util.throttle
import li.songe.gkd.util.toast import li.songe.gkd.util.toast
import li.songe.gkd.util.updateSubscription import li.songe.gkd.util.updateSubscription
import li.songe.json5.Json5
@RootNavGraph @RootNavGraph
@Destination(style = ProfileTransitions::class) @Destination(style = ProfileTransitions::class)
@ -285,7 +285,7 @@ fun AppItemPage(
}, },
) )
} }
if (editable && subsItem != null && subsRaw != null) { if (editable && subsRaw != null) {
DropdownMenuItem( DropdownMenuItem(
text = { text = {
Text(text = "删除", color = MaterialTheme.colorScheme.error) Text(text = "删除", color = MaterialTheme.colorScheme.error)
@ -435,8 +435,9 @@ fun AppItemPage(
setEditGroupRaw(null) setEditGroupRaw(null)
return@launchAsFn return@launchAsFn
} }
val element = try { val element = try {
json.parseToJsonElement(json5ToJson(source)).jsonObject Json5.parseToJson5Element(source).jsonObject
} catch (e: Exception) { } catch (e: Exception) {
LogUtils.d(e) LogUtils.d(e)
error("非法JSON:${e.message}") error("非法JSON:${e.message}")

View File

@ -72,7 +72,7 @@ import li.songe.gkd.ui.style.itemPadding
import li.songe.gkd.util.LocalMainViewModel import li.songe.gkd.util.LocalMainViewModel
import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.encodeToJson5String import li.songe.json5.encodeToJson5String
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry
@ -260,7 +260,7 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
error = true, error = true,
) )
updateSubscription( updateSubscription(
rawSubs!!.copy( rawSubs.copy(
globalGroups = rawSubs.globalGroups.filter { g -> g.key != group.key } globalGroups = rawSubs.globalGroups.filter { g -> g.key != group.key }
) )
) )

View File

@ -65,7 +65,7 @@ import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.SortTypeOption import li.songe.gkd.util.SortTypeOption
import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.encodeToJson5String import li.songe.json5.encodeToJson5String
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchTry

View File

@ -41,7 +41,7 @@ import li.songe.gkd.data.AppInfo
import li.songe.gkd.data.RawSubscription import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsConfig import li.songe.gkd.data.SubsConfig
import li.songe.gkd.ui.style.appItemPadding import li.songe.gkd.ui.style.appItemPadding
import li.songe.gkd.util.encodeToJson5String import li.songe.json5.encodeToJson5String
import li.songe.gkd.util.json import li.songe.gkd.util.json
import li.songe.gkd.util.toast import li.songe.gkd.util.toast

View File

@ -1,99 +0,0 @@
package li.songe.gkd.util
import blue.endless.jankson.Jankson
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.serializer
private val json5IdentifierReg = Regex("[a-zA-Z_][a-zA-Z0-9_]*")
/**
* https://spec.json5.org/#strings
*/
private fun escapeString(value: String): String {
val wrapChar = '\''
val sb = StringBuilder()
sb.append(wrapChar)
value.forEach { c ->
val escapeChar = when (c) {
wrapChar -> wrapChar
'\n' -> 'n'
'\r' -> 'r'
'\t' -> 't'
'\b' -> 'b'
'\\' -> '\\'
else -> null
}
if (escapeChar != null) {
sb.append("\\" + escapeChar)
} else {
when (c.code) {
in 0..0xf -> {
sb.append("\\x0" + c.code.toString(16))
}
in 0..0x1f -> {
sb.append("\\x" + c.code.toString(16))
}
else -> {
sb.append(c)
}
}
}
}
sb.append(wrapChar)
return sb.toString()
}
fun convertJsonElementToJson5(element: JsonElement, indent: Int = 2): String {
val spaces = "\u0020".repeat(indent)
return when (element) {
is JsonPrimitive -> {
val content = element.content
if (element.isString) {
escapeString(content)
} else {
content
}
}
is JsonObject -> {
if (element.isEmpty()) {
"{}"
} else {
val entries = element.entries.joinToString(",\n") { (key, value) ->
// If key is a valid identifier, no quotes are needed
if (key.matches(json5IdentifierReg)) {
"$key: ${convertJsonElementToJson5(value, indent)}"
} else {
"${escapeString(key)}: ${convertJsonElementToJson5(value, indent)}"
}
}.lineSequence().map { l -> spaces + l }.joinToString("\n")
"{\n$entries\n}"
}
}
is JsonArray -> {
if (element.isEmpty()) {
"[]"
} else {
val elements =
element.joinToString(",\n") { convertJsonElementToJson5(it, indent) }
.lineSequence().map { l -> spaces + l }.joinToString("\n")
"[\n$elements\n]"
}
}
}
}
inline fun <reified T> Json.encodeToJson5String(value: T): String {
return convertJsonElementToJson5(encodeToJsonElement(serializersModule.serializer(), value))
}
fun json5ToJson(source: String): String {
return Jankson.builder().build().load(source).toJson()
}

View File

@ -32,6 +32,7 @@ import li.songe.gkd.data.SubsConfig
import li.songe.gkd.data.SubsItem import li.songe.gkd.data.SubsItem
import li.songe.gkd.data.SubsVersion import li.songe.gkd.data.SubsVersion
import li.songe.gkd.db.DbSet import li.songe.gkd.db.DbSet
import li.songe.json5.decodeFromJson5String
import java.net.URI import java.net.URI
val subsItemsFlow by lazy { val subsItemsFlow by lazy {
@ -121,7 +122,7 @@ fun getGroupRawEnable(
enable enable
} else { } else {
null null
} ?: group.enable != false } ?: group.enable ?: true
} }
data class RuleSummary( data class RuleSummary(
@ -190,7 +191,7 @@ val ruleSummaryFlow by lazy {
mutableMapOf<RawSubscription.RawGlobalGroup, List<GlobalRule>>() mutableMapOf<RawSubscription.RawGlobalGroup, List<GlobalRule>>()
rawSubs.globalGroups.filter { g -> rawSubs.globalGroups.filter { g ->
(subGlobalSubsConfigs.find { c -> c.groupKey == g.key }?.enable (subGlobalSubsConfigs.find { c -> c.groupKey == g.key }?.enable
?: g.enable != false) && g.valid ?: g.enable ?: true) && g.valid
}.forEach { groupRaw -> }.forEach { groupRaw ->
val config = subGlobalSubsConfigs.find { c -> c.groupKey == groupRaw.key } val config = subGlobalSubsConfigs.find { c -> c.groupKey == groupRaw.key }
val g = ResolvedGlobalGroup( val g = ResolvedGlobalGroup(
@ -347,10 +348,8 @@ private suspend fun updateSubs(subsEntry: SubsEntry): RawSubscription? {
val checkUpdateUrl = subsEntry.checkUpdateUrl val checkUpdateUrl = subsEntry.checkUpdateUrl
if (checkUpdateUrl != null && subsRaw != null) { if (checkUpdateUrl != null && subsRaw != null) {
try { try {
val subsVersion = json.decodeFromString<SubsVersion>( val subsVersion = json.decodeFromJson5String<SubsVersion>(
json5ToJson( client.get(checkUpdateUrl).bodyAsText()
client.get(checkUpdateUrl).bodyAsText()
)
) )
LogUtils.d( LogUtils.d(
"快速检测更新:id=${subsRaw.id},version=${subsRaw.version}", "快速检测更新:id=${subsRaw.id},version=${subsRaw.version}",

View File

@ -29,6 +29,7 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.rikka.refine) apply false alias(libs.plugins.rikka.refine) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
} }
// can not work with Kotlin Multiplatform // can not work with Kotlin Multiplatform

View File

@ -1,7 +1,7 @@
[versions] [versions]
kotlin = "2.0.10" kotlin = "2.0.10"
ksp = "2.0.10-1.0.24" ksp = "2.0.10-1.0.24"
android = "8.5.1" android = "8.5.2"
compose = "1.6.8" compose = "1.6.8"
rikka = "4.4.0" rikka = "4.4.0"
room = "2.6.1" room = "2.6.1"
@ -10,12 +10,14 @@ ktor = "2.3.12"
hilt = "2.52" hilt = "2.52"
destinations = "1.10.2" destinations = "1.10.2"
coil = "2.7.0" coil = "2.7.0"
jetbrainsKotlinJvm = "2.0.10"
[libraries] [libraries]
android_gradle = { module = "com.android.tools.build:gradle", version.ref = "android" } android_gradle = { module = "com.android.tools.build:gradle", version.ref = "android" }
kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin_serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } kotlin_serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
kotlin_stdlib_common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "kotlin" } kotlin_stdlib_common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "kotlin" }
kotlin_test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
compose_ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose_ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
compose_preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } compose_preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
compose_tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose_tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
@ -80,3 +82,4 @@ rikka_refine = { id = "dev.rikka.tools.refine", version.ref = "rikka" }
google_ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } google_ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
google_hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } google_hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
androidx_room = { id = "androidx.room", version.ref = "room" } androidx_room = { id = "androidx.room", version.ref = "room" }
jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }

1
json5/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

22
json5/build.gradle.kts Normal file
View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,64 @@
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

@ -0,0 +1,539 @@
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

@ -0,0 +1,15 @@
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

@ -0,0 +1,149 @@
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

@ -0,0 +1,21 @@
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

@ -7,12 +7,7 @@ plugins {
} }
kotlin { kotlin {
jvm { jvm()
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
// https://kotlinlang.org/docs/js-to-kotlin-interop.html#kotlin-types-in-javascript // https://kotlinlang.org/docs/js-to-kotlin-interop.html#kotlin-types-in-javascript
js(IR) { js(IR) {
binaries.executable() binaries.executable()

View File

@ -23,3 +23,4 @@ dependencyResolutionManagement {
} }
} }
include(":json5")