mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
This commit is contained in:
parent
c78de4e513
commit
a6cde2094d
|
@ -189,6 +189,8 @@ composeCompiler {
|
|||
dependencies {
|
||||
|
||||
implementation(project(mapOf("path" to ":selector")))
|
||||
implementation(project(mapOf("path" to ":json5")))
|
||||
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
|
|
@ -16,8 +16,8 @@ import kotlinx.serialization.json.jsonPrimitive
|
|||
import kotlinx.serialization.json.long
|
||||
import li.songe.gkd.service.checkSelector
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.json5ToJson
|
||||
import li.songe.gkd.util.toast
|
||||
import li.songe.json5.Json5
|
||||
import li.songe.selector.Selector
|
||||
import net.objecthunter.exp4j.Expression
|
||||
import net.objecthunter.exp4j.ExpressionBuilder
|
||||
|
@ -798,8 +798,9 @@ data class RawSubscription(
|
|||
}
|
||||
|
||||
fun parse(source: String, json5: Boolean = true): RawSubscription {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
val subscription = jsonToSubscriptionRaw(json.parseToJsonElement(text).jsonObject)
|
||||
val element =
|
||||
if (json5) Json5.parseToJson5Element(source) else json.parseToJsonElement(source)
|
||||
val subscription = jsonToSubscriptionRaw(element.jsonObject)
|
||||
subscription.categories.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("id=${subscription.id}, duplicated category: key=${v.key}")
|
||||
}
|
||||
|
@ -840,9 +841,8 @@ data class RawSubscription(
|
|||
return a
|
||||
}
|
||||
|
||||
fun parseRawApp(source: String, json5: Boolean = true): RawApp {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
return parseApp(json.parseToJsonElement(text).jsonObject)
|
||||
fun parseRawApp(source: String): RawApp {
|
||||
return parseApp(Json5.parseToJson5Element(source).jsonObject)
|
||||
}
|
||||
|
||||
fun parseGroup(jsonObject: JsonObject): RawAppGroup {
|
||||
|
@ -853,14 +853,12 @@ data class RawSubscription(
|
|||
return g
|
||||
}
|
||||
|
||||
fun parseRawGroup(source: String, json5: Boolean = true): RawAppGroup {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
return parseGroup(json.parseToJsonElement(text).jsonObject)
|
||||
fun parseRawGroup(source: String): RawAppGroup {
|
||||
return parseGroup(Json5.parseToJson5Element(source).jsonObject)
|
||||
}
|
||||
|
||||
fun parseRawGlobalGroup(source: String, json5: Boolean = true): RawGlobalGroup {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
val g = jsonToGlobalGroups(json.parseToJsonElement(text).jsonObject, 0)
|
||||
fun parseRawGlobalGroup(source: String): RawGlobalGroup {
|
||||
val g = jsonToGlobalGroups(Json5.parseToJson5Element(source).jsonObject, 0)
|
||||
g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
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.ProfileTransitions
|
||||
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.json
|
||||
import li.songe.gkd.util.json5ToJson
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.throttle
|
||||
import li.songe.gkd.util.toast
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
import li.songe.json5.Json5
|
||||
|
||||
@RootNavGraph
|
||||
@Destination(style = ProfileTransitions::class)
|
||||
|
@ -285,7 +285,7 @@ fun AppItemPage(
|
|||
},
|
||||
)
|
||||
}
|
||||
if (editable && subsItem != null && subsRaw != null) {
|
||||
if (editable && subsRaw != null) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(text = "删除", color = MaterialTheme.colorScheme.error)
|
||||
|
@ -435,8 +435,9 @@ fun AppItemPage(
|
|||
setEditGroupRaw(null)
|
||||
return@launchAsFn
|
||||
}
|
||||
|
||||
val element = try {
|
||||
json.parseToJsonElement(json5ToJson(source)).jsonObject
|
||||
Json5.parseToJson5Element(source).jsonObject
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
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.LocalNavController
|
||||
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.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
|
@ -260,7 +260,7 @@ fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
|
|||
error = true,
|
||||
)
|
||||
updateSubscription(
|
||||
rawSubs!!.copy(
|
||||
rawSubs.copy(
|
||||
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.SortTypeOption
|
||||
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.launchAsFn
|
||||
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.SubsConfig
|
||||
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.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.SubsVersion
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.json5.decodeFromJson5String
|
||||
import java.net.URI
|
||||
|
||||
val subsItemsFlow by lazy {
|
||||
|
@ -121,7 +122,7 @@ fun getGroupRawEnable(
|
|||
enable
|
||||
} else {
|
||||
null
|
||||
} ?: group.enable != false
|
||||
} ?: group.enable ?: true
|
||||
}
|
||||
|
||||
data class RuleSummary(
|
||||
|
@ -190,7 +191,7 @@ val ruleSummaryFlow by lazy {
|
|||
mutableMapOf<RawSubscription.RawGlobalGroup, List<GlobalRule>>()
|
||||
rawSubs.globalGroups.filter { g ->
|
||||
(subGlobalSubsConfigs.find { c -> c.groupKey == g.key }?.enable
|
||||
?: g.enable != false) && g.valid
|
||||
?: g.enable ?: true) && g.valid
|
||||
}.forEach { groupRaw ->
|
||||
val config = subGlobalSubsConfigs.find { c -> c.groupKey == groupRaw.key }
|
||||
val g = ResolvedGlobalGroup(
|
||||
|
@ -347,10 +348,8 @@ private suspend fun updateSubs(subsEntry: SubsEntry): RawSubscription? {
|
|||
val checkUpdateUrl = subsEntry.checkUpdateUrl
|
||||
if (checkUpdateUrl != null && subsRaw != null) {
|
||||
try {
|
||||
val subsVersion = json.decodeFromString<SubsVersion>(
|
||||
json5ToJson(
|
||||
client.get(checkUpdateUrl).bodyAsText()
|
||||
)
|
||||
val subsVersion = json.decodeFromJson5String<SubsVersion>(
|
||||
client.get(checkUpdateUrl).bodyAsText()
|
||||
)
|
||||
LogUtils.d(
|
||||
"快速检测更新:id=${subsRaw.id},version=${subsRaw.version}",
|
||||
|
|
|
@ -29,6 +29,7 @@ plugins {
|
|||
alias(libs.plugins.kotlin.compose) apply false
|
||||
|
||||
alias(libs.plugins.rikka.refine) apply false
|
||||
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
|
||||
}
|
||||
|
||||
// can not work with Kotlin Multiplatform
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[versions]
|
||||
kotlin = "2.0.10"
|
||||
ksp = "2.0.10-1.0.24"
|
||||
android = "8.5.1"
|
||||
android = "8.5.2"
|
||||
compose = "1.6.8"
|
||||
rikka = "4.4.0"
|
||||
room = "2.6.1"
|
||||
|
@ -10,12 +10,14 @@ ktor = "2.3.12"
|
|||
hilt = "2.52"
|
||||
destinations = "1.10.2"
|
||||
coil = "2.7.0"
|
||||
jetbrainsKotlinJvm = "2.0.10"
|
||||
|
||||
[libraries]
|
||||
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_serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", 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_preview = { module = "androidx.compose.ui:ui-tooling-preview", 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_hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
||||
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 {
|
||||
jvm {
|
||||
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
jvm()
|
||||
// https://kotlinlang.org/docs/js-to-kotlin-interop.html#kotlin-types-in-javascript
|
||||
js(IR) {
|
||||
binaries.executable()
|
||||
|
|
|
@ -23,3 +23,4 @@ dependencyResolutionManagement {
|
|||
}
|
||||
}
|
||||
|
||||
include(":json5")
|
||||
|
|
Loading…
Reference in New Issue
Block a user