Skip to content

Commit 34dd09e

Browse files
committed
Add JSON support to Kson compiler
- Introduced a new CompileTarget for JSON, allowing Kson to parse and compile to JSON format. - Added a `parseToJson` method in the Kson class to handle JSON compilation. - Updated the AST classes to support JSON output formatting. - Enhanced KsonTest to validate JSON outputs alongside existing Kson and YAML tests. - Implemented a JSON validation utility for test cases across platforms (JVM, JS, Native). - Updated build.gradle.kts to include Kotlin serialization dependency for JSON handling.
1 parent 625e66e commit 34dd09e

File tree

8 files changed

+508
-59
lines changed

8 files changed

+508
-59
lines changed

build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ val sharedProps = Properties().apply {
1313

1414
plugins {
1515
kotlin("multiplatform") version "2.1.10"
16+
kotlin("plugin.serialization") version "2.1.10"
1617

1718
// configured by `jvmWrapper` block below
1819
id("me.filippov.gradle.jvm.wrapper") version "0.14.0"
@@ -148,6 +149,7 @@ kotlin {
148149
dependencies {
149150
implementation(kotlin("test-junit"))
150151
implementation("org.yaml:snakeyaml:2.2")
152+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
151153
}
152154
}
153155
val jsMain by getting

src/commonMain/kotlin/org/kson/Kson.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.kson
22

33
import org.kson.CompileTarget.Kson
44
import org.kson.CompileTarget.Yaml
5+
import org.kson.CompileTarget.Json
56
import org.kson.ast.AstNode
67
import org.kson.ast.KsonRoot
78
import org.kson.collections.ImmutableList
@@ -53,6 +54,17 @@ class Kson {
5354
return YamlParseResult(parseToAst(source, compileConfig.coreConfig), compileConfig)
5455
}
5556

57+
/**
58+
* Parse the given Kson [source] and compile it to Json
59+
*
60+
* @param source The Kson source to parse
61+
* @param compileConfig a [CompileTarget.Json] object with this compilation's config
62+
* @return A [JsonParseResult]
63+
*/
64+
fun parseToJson(source: String, compileConfig: Json = Json()): JsonParseResult {
65+
return JsonParseResult(parseToAst(source, compileConfig.coreConfig), compileConfig)
66+
}
67+
5668
/**
5769
* Parse the given Kson [source] and re-compile it out to Kson. Useful for testing and transformations
5870
* like re-writing Json into Kson (the Json is itself Kson since Kson is a superset of Json, whereas the
@@ -139,6 +151,18 @@ class YamlParseResult(
139151
val yaml: String? = astParseResult.ast?.toSource(AstNode.Indent(), compileConfig)
140152
}
141153

154+
class JsonParseResult(
155+
private val astParseResult: AstParseResult,
156+
compileConfig: Json
157+
) : ParseResult by astParseResult {
158+
/**
159+
* The Json compiled from some Kson source, or null if there were errors trying to parse
160+
* (consult [astParseResult] for information on any errors)
161+
*/
162+
val json: String? = astParseResult.ast?.toSource(AstNode.Indent(), compileConfig)
163+
}
164+
165+
142166

143167
/**
144168
* Type to denote a supported Kson compilation target and hold the compilation's configuration
@@ -170,6 +194,18 @@ sealed class CompileTarget(val coreConfig: CoreCompileConfig) {
170194
val retainEmbedTags: Boolean = false,
171195
coreCompileConfig: CoreCompileConfig = CoreCompileConfig()
172196
) : CompileTarget(coreCompileConfig)
197+
198+
/**
199+
* Compile target for Json transpilation
200+
*
201+
* @param retainEmbedTags If true, embed blocks will be compiled to objects containing both tag and content
202+
* @param coreCompileConfig the [CoreCompileConfig] for this compile
203+
*/
204+
class Json(
205+
override val preserveComments: Boolean = false,
206+
val retainEmbedTags: Boolean = false,
207+
coreCompileConfig: CoreCompileConfig = CoreCompileConfig()
208+
) : CompileTarget(coreCompileConfig)
173209
}
174210

175211
/**

src/commonMain/kotlin/org/kson/ast/Ast.kt

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.kson.ast
33
import org.kson.CompileTarget
44
import org.kson.CompileTarget.Kson
55
import org.kson.CompileTarget.Yaml
6+
import org.kson.CompileTarget.Json
67
import org.kson.ast.AstNode.Indent
78
import org.kson.parser.EMBED_DELIMITER
89
import org.kson.parser.NumberParser
@@ -109,7 +110,7 @@ class KsonRoot(
109110
*/
110111
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
111112
return when (compileTarget) {
112-
is Kson, is Yaml -> {
113+
is Kson, is Yaml, is Json -> {
113114
rootNode.toSource(indent, compileTarget) +
114115
if (compileTarget.preserveComments && documentEndComments.isNotEmpty()) {
115116
"\n\n" + documentEndComments.joinToString("\n")
@@ -127,7 +128,7 @@ class ObjectDefinitionNode(private val internalsNode: ObjectInternalsNode) :
127128
ValueNode() {
128129
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
129130
return when (compileTarget) {
130-
is Kson, is Yaml -> {
131+
is Kson, is Yaml, is Json -> {
131132
internalsNode.toSource(indent, compileTarget)
132133
}
133134
}
@@ -148,12 +149,24 @@ class ObjectInternalsNode(private val properties: List<ObjectPropertyNode>) : Va
148149
""".trimMargin()
149150
}
150151
}
152+
153+
is Json -> {
154+
if (properties.isEmpty()) {
155+
"${indent.firstLineIndent()}{}"
156+
} else {
157+
"""
158+
|${indent.firstLineIndent()}{
159+
|${properties.joinToString(",\n") { it.toSource(indent.next(false), compileTarget) }}
160+
|${indent.bodyLinesIndent()}}
161+
""".trimMargin()
162+
}
163+
}
151164

152165
is Yaml -> {
153166
if (properties.isEmpty()) {
154167
indent.firstLineIndent() + "{}"
155168
} else {
156-
properties.joinToString("\n") {
169+
properties.joinToString("\n") {
157170
it.toSource(indent, compileTarget)
158171
}
159172
}
@@ -170,7 +183,7 @@ class ObjectPropertyNode(
170183
AstNode(), Documented {
171184
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
172185
return when (compileTarget) {
173-
is Kson -> {
186+
is Kson, is Json -> {
174187
"${name.toSource(indent, compileTarget)}: ${
175188
value.toSource(
176189
indent.clone(true),
@@ -194,7 +207,7 @@ class ObjectPropertyNode(
194207
class ListNode(private val elements: List<ListElementNode>) : ValueNode() {
195208
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
196209
return when (compileTarget) {
197-
is Kson -> {
210+
is Kson, is Json -> {
198211
// We pad our list bracket with newlines if our list is non-empty
199212
val bracketPadding = if (elements.isEmpty()) "" else "\n"
200213
val endBraceIndent = if (elements.isEmpty()) "" else indent.bodyLinesIndent()
@@ -221,7 +234,7 @@ class ListNode(private val elements: List<ListElementNode>) : ValueNode() {
221234
class ListElementNode(val value: ValueNode, override val comments: List<String>) : AstNode(), Documented {
222235
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
223236
return when (compileTarget) {
224-
is Kson -> {
237+
is Kson, is Json -> {
225238
value.toSource(indent, compileTarget)
226239
}
227240
is Yaml -> {
@@ -245,6 +258,10 @@ open class StringNode(override val stringContent: String) : KeywordNode() {
245258
is Kson, is Yaml -> {
246259
indent.firstLineIndent() + "\"" + stringContent + "\""
247260
}
261+
262+
is Json -> {
263+
indent.firstLineIndent() + "\"${escapeJsonString(stringContent)}\""
264+
}
248265
}
249266
}
250267
}
@@ -255,6 +272,10 @@ class IdentifierNode(override val stringContent: String) : KeywordNode() {
255272
is Kson, is Yaml -> {
256273
indent.firstLineIndent() + stringContent
257274
}
275+
276+
is Json -> {
277+
indent.firstLineIndent() + "\"${escapeJsonString(stringContent)}\""
278+
}
258279
}
259280
}
260281
}
@@ -271,7 +292,7 @@ class NumberNode(stringValue: String) : ValueNode() {
271292

272293
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
273294
return when (compileTarget) {
274-
is Kson, is Yaml -> {
295+
is Kson, is Yaml, is Json-> {
275296
indent.firstLineIndent() + value.asString
276297
}
277298
}
@@ -281,7 +302,7 @@ class NumberNode(stringValue: String) : ValueNode() {
281302
class TrueNode : ValueNode() {
282303
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
283304
return when (compileTarget) {
284-
is Kson, is Yaml -> {
305+
is Kson, is Yaml, is Json -> {
285306
indent.firstLineIndent() + "true"
286307
}
287308
}
@@ -291,7 +312,7 @@ class TrueNode : ValueNode() {
291312
class FalseNode : ValueNode() {
292313
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
293314
return when (compileTarget) {
294-
is Kson, is Yaml -> {
315+
is Kson, is Yaml, is Json-> {
295316
indent.firstLineIndent() + "false"
296317
}
297318
}
@@ -301,7 +322,7 @@ class FalseNode : ValueNode() {
301322
class NullNode : ValueNode() {
302323
override fun toSourceInternal(indent: Indent, compileTarget: CompileTarget): String {
303324
return when (compileTarget) {
304-
is Kson, is Yaml -> {
325+
is Kson, is Yaml, is Json -> {
305326
indent.firstLineIndent() + "null"
306327
}
307328
}
@@ -342,6 +363,20 @@ class EmbedBlockNode(private val embedTag: String, private val embedContent: Str
342363
renderMultilineYamlString(embedContent, indent, indent.next(false))
343364
}
344365
}
366+
367+
is Json -> {
368+
if (!compileTarget.retainEmbedTags) {
369+
indent.firstLineIndent() + "\"${escapeJsonString(embedContent)}\""
370+
} else {
371+
val nextIndent = indent.next(false)
372+
"""
373+
|${indent.firstLineIndent()}{
374+
|${nextIndent.bodyLinesIndent()}"$EMBED_TAG_KEYWORD": "$embedTag",
375+
|${nextIndent.bodyLinesIndent()}"$EMBED_CONTENT_KEYWORD": "${escapeJsonString(embedContent)}"
376+
|}
377+
""".trimMargin()
378+
}
379+
}
345380
}
346381
}
347382
}
@@ -375,3 +410,20 @@ private fun renderMultilineYamlString(
375410
contentIndent.bodyLinesIndent() + line
376411
}
377412
}
413+
414+
/**
415+
* Escapes a string for use in JSON
416+
*
417+
* @param str The string to escape
418+
* @return The escaped string with all JSON special characters properly escaped
419+
*/
420+
private fun escapeJsonString(str: String): String {
421+
// This function escapes JSON characters
422+
return str
423+
.replace("\\", "\\\\") // Must be first to avoid double-escaping
424+
.replace("\"", "\\\"")
425+
.replace("\b", "\\b")
426+
.replace("\n", "\\n")
427+
.replace("\r", "\\r")
428+
.replace("\t", "\\t")
429+
}

0 commit comments

Comments
 (0)