Skip to content

Commit 4b7961b

Browse files
authored
Merge pull request #92 from holodorum/json-transpiler
JSON as a Kson compilation target
2 parents 836ff0e + 1a279fb commit 4b7961b

File tree

5 files changed

+507
-70
lines changed

5 files changed

+507
-70
lines changed

build.gradle.kts

Lines changed: 2 additions & 1 deletion
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"
@@ -141,6 +142,7 @@ kotlin {
141142
dependencies {
142143
implementation(kotlin("test-common"))
143144
implementation(kotlin("test-annotations-common"))
145+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
144146
}
145147
}
146148
val jvmMain by getting
@@ -160,4 +162,3 @@ kotlin {
160162
val nativeKsonTest by getting
161163
}
162164
}
163-

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

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package org.kson
22

3+
import org.kson.CompileTarget.*
34
import org.kson.CompileTarget.Kson
4-
import org.kson.CompileTarget.Yaml
55
import org.kson.ast.AstNode
66
import org.kson.ast.KsonRoot
77
import org.kson.collections.ImmutableList
@@ -53,6 +53,17 @@ class Kson {
5353
return YamlParseResult(parseToAst(source, compileConfig.coreConfig), compileConfig)
5454
}
5555

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

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

143166
/**
144167
* Type to denote a supported Kson compilation target and hold the compilation's configuration
145168
*/
146169
sealed class CompileTarget(val coreConfig: CoreCompileConfig) {
170+
/**
171+
* Whether this compilation should preserve comments from the input [Kson] source in the compiled output
172+
*/
173+
abstract val preserveComments: Boolean
174+
147175
/**
148176
* Compile target for serializing a Kson AST out to Kson source
149177
*
150178
* @param coreCompileConfig the [CoreCompileConfig] for this compile
151179
*/
152180
class Kson(
181+
override val preserveComments: Boolean = true,
153182
coreCompileConfig: CoreCompileConfig = CoreCompileConfig()
154183
) : CompileTarget(coreCompileConfig)
155184

@@ -160,19 +189,30 @@ sealed class CompileTarget(val coreConfig: CoreCompileConfig) {
160189
* @param coreCompileConfig the [CoreCompileConfig] for this compile
161190
*/
162191
class Yaml(
192+
override val preserveComments: Boolean = true,
163193
val retainEmbedTags: Boolean = false,
164194
coreCompileConfig: CoreCompileConfig = CoreCompileConfig()
165195
) : CompileTarget(coreCompileConfig)
196+
197+
/**
198+
* Compile target for Json transpilation
199+
*
200+
* @param retainEmbedTags If true, embed blocks will be compiled to objects containing both tag and content
201+
* @param coreCompileConfig the [CoreCompileConfig] for this compile
202+
*/
203+
class Json(
204+
val retainEmbedTags: Boolean = false,
205+
coreCompileConfig: CoreCompileConfig = CoreCompileConfig()
206+
) : CompileTarget(coreCompileConfig) {
207+
// Json does not support comments
208+
override val preserveComments: Boolean = false
209+
}
166210
}
167211

168212
/**
169213
* Configuration applicable to all compile targets
170214
*/
171215
data class CoreCompileConfig(
172-
/**
173-
* Whether this compilation should preserve comments from the input [Kson] source in the compiled output
174-
*/
175-
val preserveComments: Boolean = true,
176216
/**
177217
* The [JSON Schema](https://json-schema.org/) to enforce in this compilation
178218
*/

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

Lines changed: 63 additions & 12 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
@@ -65,7 +66,7 @@ abstract class AstNode {
6566
* [CompileTarget]
6667
*/
6768
fun toSource(indent: Indent, compileTarget: CompileTarget): String {
68-
return if (compileTarget.coreConfig.preserveComments && this is Documented && comments.isNotEmpty()) {
69+
return if (compileTarget.preserveComments && this is Documented && comments.isNotEmpty()) {
6970
// if we have comments, write them followed by the node content on the next line with an appropriate indent
7071
indent.firstLineIndent() + comments.joinToString("\n${indent.bodyLinesIndent()}") +
7172
"\n" + toSourceInternal(indent.clone(false), compileTarget)
@@ -109,9 +110,9 @@ 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) +
114-
if (compileTarget.coreConfig.preserveComments && documentEndComments.isNotEmpty()) {
115+
if (compileTarget.preserveComments && documentEndComments.isNotEmpty()) {
115116
"\n\n" + documentEndComments.joinToString("\n")
116117
} else {
117118
""
@@ -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() + "\"${escapeStringLiterals(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() + "\"${escapeStringLiterals(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() + "\"${escapeStringLiterals(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": "${escapeStringLiterals(embedContent)}"
376+
|}
377+
""".trimMargin()
378+
}
379+
}
345380
}
346381
}
347382
}
@@ -375,3 +410,19 @@ private fun renderMultilineYamlString(
375410
contentIndent.bodyLinesIndent() + line
376411
}
377412
}
413+
414+
/**
415+
* Escapes quotes and whitespace characters (newlines, carriage returns and tabs) in a string,
416+
* useful for instance when serializing a Kson-escaped string (which allows raw whitespace)
417+
* to a JSON-escaped string (which does not).
418+
*
419+
* @param str The string to escape
420+
* @return The string with quotes and whitespace characters (newlines, carriage returns, tabs) escaped with backslashes
421+
*/
422+
private fun escapeStringLiterals(str: String): String {
423+
return str
424+
.replace("\"", "\\\"")
425+
.replace("\n", "\\n")
426+
.replace("\r", "\\r")
427+
.replace("\t", "\\t")
428+
}

0 commit comments

Comments
 (0)