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 {
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)

View File

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

View File

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

View File

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

View File

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

View File

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

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.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}",

View File

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

View File

@ -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
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 {
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()

View File

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