Skip to content

Commit fae2587

Browse files
committed
perf: encodeToString by stack
1 parent a67b3ac commit fae2587

File tree

7 files changed

+194
-145
lines changed

7 files changed

+194
-145
lines changed

json5/src/commonMain/kotlin/li/songe/json5/BaseParser.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ internal fun BaseParser.readNumber(): Json5Number = when (char) {
229229

230230
// https://github.com/json5/json5/blob/b935d4a280eafa8835e6182551b63809e61243b0/lib/parse.js#L570
231231
internal fun BaseParser.readString(): String {
232-
val wrapChar = char!! // must be ' or "
232+
val wrapChar = char // must be ' or "
233233
i++
234234
// most
235235
for (j in i..input.lastIndex) {
@@ -242,15 +242,15 @@ internal fun BaseParser.readString(): String {
242242
}
243243
val sb = StringBuilder()
244244
while (true) {
245-
when (char) {
245+
when (val c1 = char) {
246246
wrapChar -> {
247247
i++
248248
break
249249
}
250250

251251
'\\' -> {
252252
i++
253-
when (char) {
253+
when (val c2 = char) {
254254
null -> stop()
255255
wrapChar -> {
256256
sb.append(wrapChar)
@@ -341,7 +341,7 @@ internal fun BaseParser.readString(): String {
341341
in '1'..'9' -> stop()
342342

343343
else -> {
344-
sb.append(char)
344+
sb.append(c2)
345345
i++
346346
}
347347
}
@@ -350,7 +350,7 @@ internal fun BaseParser.readString(): String {
350350
null, '\n', '\r' -> stop()
351351

352352
else -> {
353-
sb.append(char)
353+
sb.append(c1)
354354
i++
355355
}
356356
}

json5/src/commonMain/kotlin/li/songe/json5/EncodeUtil.kt

Lines changed: 166 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,176 @@ import kotlinx.serialization.json.JsonArray
44
import kotlinx.serialization.json.JsonElement
55
import kotlinx.serialization.json.JsonObject
66
import kotlinx.serialization.json.JsonPrimitive
7-
import kotlin.collections.component1
8-
import kotlin.collections.component2
9-
10-
11-
internal fun innerEncodeToString(
12-
element: JsonElement,
13-
config: Json5EncoderConfig,
14-
level: Int = 0,
15-
): String = if (element is JsonPrimitive) {
16-
if (element.isString) {
17-
stringifyString(element.content, config.singleQuote)
18-
} else {
19-
element.content
7+
8+
internal fun stringifyKey(key: String, config: Json5EncoderConfig): String {
9+
if (key.isEmpty() || !config.unquotedKey) {
10+
return stringifyString(key, config.singleQuote)
11+
}
12+
if (!isIdStartChar(key[0])) {
13+
return stringifyString(key, config.singleQuote)
2014
}
21-
} else {
22-
val indent = config.indent
23-
val lineSeparator = if (indent.isEmpty()) "" else "\n"
24-
val keySeparator = if (indent.isEmpty()) ":" else ": "
25-
val newLevel = level + 1
26-
val prefixSpaces = if (indent.isEmpty()) "" else indent.repeat(newLevel)
27-
val closingSpaces = if (indent.isEmpty()) "" else indent.repeat(level)
28-
val postfix = if (config.trailingComma) "," else ""
29-
if (element is JsonObject) {
30-
if (element.isEmpty()) {
31-
"{}"
32-
} else {
33-
element.entries.joinToString(",$lineSeparator", postfix = postfix) { (key, value) ->
34-
"${prefixSpaces}${stringifyKey(key, config.singleQuote, config.unquotedKey)}${keySeparator}${
35-
innerEncodeToString(
36-
value,
37-
config,
38-
newLevel
39-
)
40-
}"
41-
}.let {
42-
"{$lineSeparator$it$lineSeparator$closingSpaces}"
15+
for (c in key) {
16+
if (!isIdContinueChar(c)) {
17+
return stringifyString(key, config.singleQuote)
18+
}
19+
}
20+
return key
21+
}
22+
23+
private val escapeReplacements = hashMapOf(
24+
'\\' to "\\\\",
25+
'\b' to "\\b",
26+
'\u000C' to "\\f",
27+
'\n' to "\\n",
28+
'\r' to "\\r",
29+
'\t' to "\\t",
30+
'\u000B' to "\\v",
31+
'\u0000' to "\\0",
32+
'\u2028' to "\\u2028",
33+
'\u2029' to "\\u2029",
34+
)
35+
36+
// https://github.com/json5/json5/blob/b935d4a280eafa8835e6182551b63809e61243b0/lib/stringify.js#L104
37+
internal fun stringifyString(value: String, singleQuote: Boolean): String {
38+
val wrapChar = if (singleQuote) '\'' else '"'
39+
val sb = StringBuilder()
40+
sb.append(wrapChar)
41+
value.forEachIndexed { i, c ->
42+
when {
43+
c == wrapChar -> {
44+
sb.append("\\$wrapChar")
45+
}
46+
47+
c == '\u0000' -> {
48+
if (isDigit(value.getOrNull(i + 1))) {
49+
// "\u00002" -> \x002, avoid octal ambiguity
50+
sb.append("\\x00")
51+
} else {
52+
sb.append("\\0")
53+
}
54+
}
55+
56+
c in escapeReplacements -> {
57+
sb.append(escapeReplacements[c])
58+
}
59+
60+
c.code in 0..0x1f -> {
61+
sb.append("\\x" + c.code.toString(16).padStart(2, '0'))
62+
}
63+
64+
else -> {
65+
sb.append(c)
4366
}
4467
}
45-
} else if (element is JsonArray) {
46-
if (element.isEmpty()) {
47-
"[]"
48-
} else {
49-
element.joinToString(",$lineSeparator", postfix = postfix) {
50-
"${prefixSpaces}${innerEncodeToString(it, config, newLevel)}"
51-
}.let {
52-
"[$lineSeparator$it$lineSeparator$closingSpaces]"
68+
}
69+
sb.append(wrapChar)
70+
return sb.toString()
71+
}
72+
73+
internal fun stringifyPrimitive(value: JsonPrimitive, config: Json5EncoderConfig): String = when {
74+
value.isString -> stringifyString(value.content, config.singleQuote)
75+
else -> value.content
76+
}
77+
78+
internal fun JsonObject.getByIndex(index: Int): Map.Entry<String, JsonElement> {
79+
var i = 0
80+
forEach {
81+
if (i == index) return it
82+
i++
83+
}
84+
throw IndexOutOfBoundsException("Index: $index, Size: $size")
85+
}
86+
87+
private fun getIndent(config: Json5EncoderConfig, ind: Int): String {
88+
return config.indent.repeat(ind)
89+
}
90+
91+
internal fun innerEncodeToString(element: JsonElement, config: Json5EncoderConfig): String {
92+
val sb = StringBuilder()
93+
val elementStack = mutableListOf(element)
94+
val indexStack = mutableListOf(0)
95+
val indentStack = mutableListOf(0)
96+
97+
val hasIndent = config.indent.isNotEmpty()
98+
val keySeparator = if (hasIndent) ": " else ":"
99+
fun newLine() {
100+
if (hasIndent) {
101+
sb.append("\n")
102+
}
103+
}
104+
105+
while (elementStack.isNotEmpty()) {
106+
val value = elementStack.pop()
107+
val idx = indexStack.pop()
108+
val ind = indentStack.pop()
109+
110+
when (value) {
111+
is JsonPrimitive -> sb.append(stringifyPrimitive(value, config))
112+
is JsonArray -> {
113+
if (idx == 0) {
114+
sb.append("[")
115+
if (value.isNotEmpty()) {
116+
newLine()
117+
}
118+
}
119+
120+
if (idx < value.size) {
121+
if (idx > 0) {
122+
sb.append(",")
123+
newLine()
124+
}
125+
if (hasIndent) sb.append(getIndent(config, ind + 1))
126+
127+
elementStack.add(value)
128+
indexStack.add(idx + 1)
129+
indentStack.add(ind)
130+
131+
elementStack.add(value[idx])
132+
indexStack.add(0)
133+
indentStack.add(ind + 1)
134+
} else {
135+
if (value.isNotEmpty()) {
136+
if (config.trailingComma) sb.append(",")
137+
if (hasIndent) sb.append("\n").append(getIndent(config, ind))
138+
}
139+
sb.append("]")
140+
}
141+
}
142+
143+
is JsonObject -> {
144+
if (idx == 0) {
145+
sb.append("{")
146+
if (value.isNotEmpty()) {
147+
newLine()
148+
}
149+
}
150+
if (idx < value.size) {
151+
if (idx > 0) {
152+
sb.append(",")
153+
newLine()
154+
}
155+
val (k, v) = value.getByIndex(idx)
156+
if (hasIndent) sb.append(getIndent(config, ind + 1))
157+
sb.append(stringifyKey(k, config)).append(keySeparator)
158+
159+
elementStack.add(value)
160+
indexStack.add(idx + 1)
161+
indentStack.add(ind)
162+
163+
elementStack.add(v)
164+
indexStack.add(0)
165+
indentStack.add(ind + 1)
166+
} else {
167+
if (value.isNotEmpty()) {
168+
if (config.trailingComma) sb.append(",")
169+
if (hasIndent) sb.append("\n").append(getIndent(config, ind))
170+
}
171+
sb.append("}")
172+
}
53173
}
54174
}
55-
} else {
56-
throw IllegalArgumentException()
57175
}
176+
177+
return sb.toString()
58178
}
179+

json5/src/commonMain/kotlin/li/songe/json5/Json5Decoder.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ internal class Json5Decoder(override val input: CharSequence) : BaseParser {
6161

6262
fun buildTokenSeq() = sequence {
6363
while (!end) {
64-
val token = charToJson5Token(stack.lastOrNull() is MutableMap<*, *>)
65-
if (token == null) {
66-
stop()
67-
}
64+
val token = charToJson5Token(stack.lastOrNull() is MutableMap<*, *>) ?: stop()
6865
yield(token)
6966
}
7067
}

json5/src/commonMain/kotlin/li/songe/json5/Json5LooseDecoder.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ internal class Json5LooseDecoder(override val input: CharSequence) : BaseParser
3636
val inMapLeft = scopes.lastOrNull() == Json5Token.LeftBrace && lastVisibleToken.let {
3737
it == Json5Token.Comma || it == Json5Token.LeftBrace
3838
}
39-
val token = charToJson5Token(inMapLeft)
40-
when (token) {
39+
when (val token = charToJson5Token(inMapLeft)) {
4140
null -> {
4241
i++
4342
}

json5/src/commonMain/kotlin/li/songe/json5/LooseUtil.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ internal fun BaseParser.readLooseComment() {
4343
}
4444

4545
internal fun BaseParser.readLooseString() {
46-
val wrapChar = char!!
46+
val wrapChar = char // it must be ' or "
4747
i++
4848
while (true) {
4949
when (char) {

0 commit comments

Comments
 (0)