-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Even more common JsonSupport * Replace platform specific JsonSupport to common. * Move JsonToPlotSpec and PlotSvgExportTest to portable. * Fix tests - replace ' to " in plotSpec to meet JSON string specification. * Minor refactoring.
- Loading branch information
1 parent
644bfc4
commit 2d0c23b
Showing
16 changed files
with
571 additions
and
242 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
base-portable/src/commonMain/kotlin/jetbrains/datalore/base/json/JsonLexer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
/* | ||
* Copyright (c) 2020. JetBrains s.r.o. | ||
* Use of this source code is governed by the MIT license that can be found in the LICENSE file. | ||
*/ | ||
|
||
package jetbrains.datalore.base.json | ||
|
||
internal class JsonLexer( | ||
private val input: String | ||
) { | ||
private var i = 0 | ||
private var tokenStart = 0 | ||
var currentToken: Token? = null | ||
private set | ||
|
||
private val currentChar: Char | ||
get() = input[i] | ||
|
||
init { | ||
nextToken() // read first token | ||
} | ||
|
||
fun nextToken() { | ||
advanceWhile { it.isWhitespace() } | ||
|
||
if (isFinished()) { | ||
return | ||
} | ||
|
||
when { | ||
currentChar == '{' -> Token.LEFT_BRACE.also { advance() } | ||
currentChar == '}' -> Token.RIGHT_BRACE.also { advance() } | ||
currentChar == '[' -> Token.LEFT_BRACKET.also { advance() } | ||
currentChar == ']' -> Token.RIGHT_BRACKET.also { advance() } | ||
currentChar == ',' -> Token.COMMA.also { advance() } | ||
currentChar == ':' -> Token.COLON.also { advance() } | ||
currentChar == 't' -> Token.TRUE.also { read("true") } | ||
currentChar == 'f' -> Token.FALSE.also { read("false") } | ||
currentChar == 'n' -> Token.NULL.also { read("null") } | ||
currentChar == '"' -> Token.STRING.also { readString() } | ||
readNumber() -> Token.NUMBER | ||
else -> error("$i:${currentChar} - unkown token") | ||
}.also { currentToken = it } | ||
} | ||
|
||
fun tokenValue() = input.substring(tokenStart, i) | ||
|
||
private fun readString() { | ||
startToken() | ||
advance() // opening quote | ||
while(!(currentChar == '"')) { | ||
if(currentChar == '\\') { | ||
advance() | ||
when { | ||
currentChar == 'u' -> { | ||
advance() | ||
repeat(4) { | ||
require(currentChar.isHex()); | ||
advance() | ||
} | ||
} | ||
currentChar in SPECIAL_CHARS -> advance() | ||
else -> error("Invalid escape sequence") | ||
} | ||
} else { | ||
advance() | ||
} | ||
} | ||
advance() // closing quote | ||
} | ||
|
||
private fun readNumber(): Boolean { | ||
if (!(currentChar.isDigit() || currentChar == '-')) { | ||
return false | ||
} | ||
|
||
startToken() | ||
advanceIfCurrent('-') | ||
advanceWhile { it.isDigit() } | ||
|
||
advanceIfCurrent('.') { | ||
require(currentChar.isDigit()) { "Number should have decimal part" } | ||
advanceWhile { it.isDigit() } | ||
} | ||
|
||
advanceIfCurrent('e', 'E') { | ||
advanceIfCurrent('+', '-') | ||
advanceWhile { it.isDigit() } | ||
} | ||
|
||
return true | ||
} | ||
|
||
fun isFinished(): Boolean = i == input.length | ||
private fun startToken() { tokenStart = i } | ||
private fun advance() { ++i } | ||
|
||
private fun read(str: String) { | ||
return str.forEach { | ||
require(currentChar == it) { "Wrong data: $str" } | ||
require(!isFinished()) { "Unexpected end of string" } | ||
advance() | ||
} | ||
} | ||
|
||
private fun advanceWhile(pred: (Char) -> Boolean) { | ||
while (!isFinished() && pred(currentChar)) advance() | ||
} | ||
|
||
private fun advanceIfCurrent(vararg expected: Char, then: () -> Unit = {}) { | ||
if (!isFinished() && currentChar in expected) { | ||
advance() | ||
then() | ||
} | ||
} | ||
|
||
companion object { | ||
private val digits: CharRange = '0'..'9' | ||
private fun Char?.isDigit() = this in digits | ||
private fun Char.isHex(): Boolean { return isDigit() || this in 'a'..'f' || this in 'A'..'F' } | ||
} | ||
} |
89 changes: 89 additions & 0 deletions
89
base-portable/src/commonMain/kotlin/jetbrains/datalore/base/json/JsonParser.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
* Copyright (c) 2020. JetBrains s.r.o. | ||
* Use of this source code is governed by the MIT license that can be found in the LICENSE file. | ||
*/ | ||
|
||
package jetbrains.datalore.base.json | ||
|
||
class JsonParser( | ||
private val json: String | ||
) { | ||
fun parseJson(): Any? { | ||
val lexer = JsonLexer(json) | ||
return parseValue(lexer) | ||
} | ||
|
||
private fun parseValue(lexer: JsonLexer): Any? { | ||
return when(lexer.currentToken) { | ||
Token.STRING -> lexer.tokenValue().unescape().also { lexer.nextToken() } | ||
Token.NUMBER -> lexer.tokenValue().toDouble().also { lexer.nextToken() } | ||
Token.FALSE -> false.also { lexer.nextToken() } | ||
Token.TRUE -> true.also { lexer.nextToken() } | ||
Token.NULL -> null.also { lexer.nextToken() } | ||
Token.LEFT_BRACE -> parseObject(lexer) | ||
Token.LEFT_BRACKET -> parseArray(lexer) | ||
else -> error("Invalid token: ${lexer.currentToken}") | ||
} | ||
} | ||
|
||
private fun parseArray(lexer: JsonLexer): MutableList<Any?> { | ||
fun checkCurrentToken(token: Token) { require(lexer.currentToken, token, "[Arr] ") } | ||
|
||
val list = mutableListOf<Any?>() | ||
|
||
checkCurrentToken(Token.LEFT_BRACKET) | ||
lexer.nextToken() | ||
|
||
while (lexer.currentToken != Token.RIGHT_BRACKET) { | ||
if (list.isNotEmpty()) { | ||
checkCurrentToken(Token.COMMA) | ||
lexer.nextToken() | ||
} | ||
list.add(parseValue(lexer)) | ||
} | ||
|
||
checkCurrentToken(Token.RIGHT_BRACKET) | ||
lexer.nextToken() | ||
|
||
return list | ||
} | ||
|
||
private fun parseObject(lexer: JsonLexer): Map<String, Any?> { | ||
fun checkCurrentToken(token: Token) { require(lexer.currentToken, token, "[Obj] ") } | ||
|
||
val map = mutableMapOf<String, Any?>() | ||
|
||
checkCurrentToken(Token.LEFT_BRACE) | ||
lexer.nextToken() | ||
|
||
while (lexer.currentToken != Token.RIGHT_BRACE) { | ||
if (map.isNotEmpty()) { | ||
checkCurrentToken(Token.COMMA) | ||
lexer.nextToken() | ||
} | ||
|
||
checkCurrentToken(Token.STRING) | ||
val key = lexer.tokenValue().unescape() | ||
lexer.nextToken() | ||
|
||
checkCurrentToken(Token.COLON) | ||
lexer.nextToken() | ||
|
||
val value = parseValue(lexer) | ||
map[key] = value | ||
} | ||
|
||
checkCurrentToken(Token.RIGHT_BRACE) | ||
lexer.nextToken() | ||
|
||
return map | ||
} | ||
|
||
private fun require(current: Token?, expected: Token?, messagePrefix: String? = null) { | ||
if (current != expected) { | ||
throw JsonException(messagePrefix + "Expected token: $expected, actual: $current") | ||
} | ||
} | ||
|
||
class JsonException(message: String) : Exception(message) | ||
} |
93 changes: 93 additions & 0 deletions
93
base-portable/src/commonMain/kotlin/jetbrains/datalore/base/json/JsonSupport.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* Copyright (c) 2019. JetBrains s.r.o. | ||
* Use of this source code is governed by the MIT license that can be found in the LICENSE file. | ||
*/ | ||
|
||
package jetbrains.datalore.base.json | ||
|
||
object JsonSupport { | ||
fun parseJson(jsonString: String): MutableMap<String, Any?> { | ||
@Suppress("UNCHECKED_CAST") | ||
return JsonParser(jsonString).parseJson() as MutableMap<String, Any?> | ||
} | ||
fun formatJson(o: Any): String { | ||
return JsonFormatter().formatJson(o) | ||
} | ||
} | ||
|
||
|
||
// Usefull resources: | ||
// https://www.ietf.org/rfc/rfc4627.txt | ||
// https://github.com/nst/JSONTestSuite | ||
|
||
internal enum class Token { | ||
LEFT_BRACE, | ||
RIGHT_BRACE, | ||
LEFT_BRACKET, | ||
RIGHT_BRACKET, | ||
COMMA, | ||
COLON, | ||
STRING, | ||
NUMBER, | ||
TRUE, | ||
FALSE, | ||
NULL, | ||
} | ||
|
||
internal val SPECIAL_CHARS = mapOf( | ||
'"' to '"', | ||
'\\' to '\\', | ||
'/' to '/', | ||
'b' to '\b', | ||
'f' to '\u000C', | ||
'n' to '\n', | ||
'r' to '\r', | ||
't' to '\t' | ||
) | ||
|
||
private val CONTROL_CHARS = (0 until 0x20).map(Int::toChar).toSet() | ||
|
||
fun String.escape(): String { | ||
var output: StringBuilder? = null | ||
var i = 0 | ||
|
||
fun appendOutput(str: String) { | ||
output = (output ?: StringBuilder(substring(0, i))).append(str) | ||
} | ||
|
||
while(i < length) { | ||
when(val ch = get(i)) { | ||
'\\' -> appendOutput("""\\""") | ||
'"' -> appendOutput("""\"""") | ||
'\n' -> appendOutput("""\n""") | ||
'\r' -> appendOutput("""\r""") | ||
'\t' -> appendOutput("""\t""") | ||
in CONTROL_CHARS -> appendOutput("""\u${ch.toInt().toString(16).padStart(4, '0')}""") | ||
else -> output?.append(ch) | ||
} | ||
i++ | ||
} | ||
return output?.toString() ?: this | ||
} | ||
|
||
fun String.unescape(): String { | ||
var output: StringBuilder? = null | ||
val start = 1 | ||
val end = length - 1 | ||
|
||
var i = start | ||
while(i < end) { | ||
val ch = get(i) | ||
if (ch == '\\') { | ||
output = output ?: StringBuilder(substring(start, i)) | ||
when(val escapedChar = get(++i)) { | ||
in SPECIAL_CHARS -> SPECIAL_CHARS[escapedChar].also { i++ } | ||
'u' -> substring(i + 1, i + 5).toInt(16).toChar().also { i += 5 } | ||
else -> throw JsonParser.JsonException("Invalid escape character: ${escapedChar}") | ||
}.let { output.append(it) } | ||
} else { | ||
output?.append(ch); i++ | ||
} | ||
} | ||
return output?.toString() ?: substring(start, end) | ||
} |
18 changes: 0 additions & 18 deletions
18
base-portable/src/commonMain/kotlin/jetbrains/datalore/base/json/PortableUtils.kt
This file was deleted.
Oops, something went wrong.
11 changes: 0 additions & 11 deletions
11
base/src/commonMain/kotlin/jetbrains/datalore/base/json/JsonSupport.kt
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.