mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-15 19:22:26 +08:00
This commit is contained in:
parent
c78de4e513
commit
a6cde2094d
|
@ -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)
|
||||||
|
|
|
@ -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}")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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}",
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
1
json5/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
22
json5/build.gradle.kts
Normal file
22
json5/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
json5/src/commonMain/kotlin/li/songe/json5/Json5.kt
Normal file
64
json5/src/commonMain/kotlin/li/songe/json5/Json5.kt
Normal 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]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
539
json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt
Normal file
539
json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt
Normal 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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
json5/src/commonMain/kotlin/li/songe/json5/JsonExt.kt
Normal file
15
json5/src/commonMain/kotlin/li/songe/json5/JsonExt.kt
Normal 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))
|
||||||
|
}
|
149
json5/src/commonMain/kotlin/li/songe/json5/Util.kt
Normal file
149
json5/src/commonMain/kotlin/li/songe/json5/Util.kt
Normal 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
|
||||||
|
}
|
21
json5/src/commonTest/kotlin/li/songe/json5/Json5Test.kt
Normal file
21
json5/src/commonTest/kotlin/li/songe/json5/Json5Test.kt
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -23,3 +23,4 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include(":json5")
|
||||||
|
|
Loading…
Reference in New Issue
Block a user