From c63be7e8271249347ca6763c17a22193200b72a8 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Fri, 27 Jan 2023 02:24:32 -0700 Subject: [PATCH 01/11] Activate dormant tests and remove type annotation cases --- .../ktoml/encoders/EncodingAnnotationTest.kt | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt index 066d3e36..8e97b484 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt @@ -159,7 +159,6 @@ class EncodingAnnotationTest { } @Test - @Ignore fun multilineArrayTest() { @Serializable data class File( @@ -170,7 +169,8 @@ class EncodingAnnotationTest { "fox", "jumps", "over", "the", "lazy", "dog" ), - val fib: @TomlMultiline List = + @TomlMultiline + val fib: List = listOf(1, 1, 2, 3, 5, 8, 13) ) @@ -203,40 +203,29 @@ class EncodingAnnotationTest { } @Test - @Ignore fun integerRepresentationTest() { @Serializable data class File( @TomlInteger(DECIMAL) - val decA: Long = 0, - val decB: @TomlInteger(DECIMAL) Long = 1, + val dec: Long = 0, @TomlInteger(BINARY) - val binA: Long = 2, - val binB: @TomlInteger(BINARY) Long = 3, + val bin: Long = 2, @TomlInteger(GROUPED) - val groA: Long = 1_000_000, - val groB: @TomlInteger(GROUPED) Long = 1_000, + val gro: Long = 1_000_000, @TomlInteger(HEX) - val hexA: Long = 4, - val hexB: @TomlInteger(HEX) Long = 5, + val hex: Long = 4, @TomlInteger(OCTAL) - val octA: Long = 6, - val octB: @TomlInteger(OCTAL) Long = 7 + val oct: Long = 6, ) assertEncodedEquals( value = File(), expectedToml = """ - decA = 0 - decB = 1 - binA = 0b10 - binB = 0b11 - groA = 1_000_000 - groB = 1_000 - hexA = 0x4 - hexB = 0x5 - octA = 0o6 - octB = 0o7 + dec = 0 + bin = 0b10 + gro = 1_000_000 + hex = 0x4 + oct = 0o6 """.trimIndent() ) } @@ -257,13 +246,14 @@ class EncodingAnnotationTest { } @Test - @Ignore fun multilineStringTest() { @Serializable data class File( @TomlMultiline val mlTextA: String = "\n\\tMultiline\ntext!\n", - val mlTextB: @[TomlMultiline TomlLiteral] String = "\n\"Multiline\n\"text!\n" + @TomlLiteral + @TomlMultiline + val mlTextB: String = "\n\"Multiline\n\"text!\n" ) val tripleQuotes = "\"\"\"" From 3b6e48e4f68248ea3520de8e2595cf6c40c00906 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Fri, 27 Jan 2023 02:28:07 -0700 Subject: [PATCH 02/11] Implement integer representation --- .../ktoml/encoders/TomlAbstractEncoder.kt | 9 +---- .../ktoml/tree/nodes/pairs/values/TomlLong.kt | 38 +++++++++++++++++-- .../akuleshov7/ktoml/writers/TomlEmitter.kt | 21 +++++++++- .../ktoml/encoders/EncodingAnnotationTest.kt | 4 +- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt index 6a06a730..32f6e0f4 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt @@ -7,7 +7,6 @@ import com.akuleshov7.ktoml.tree.nodes.TomlNode import com.akuleshov7.ktoml.tree.nodes.pairs.values.* import com.akuleshov7.ktoml.utils.bareKeyRegex import com.akuleshov7.ktoml.utils.literalKeyCandidateRegex -import com.akuleshov7.ktoml.writers.IntegerRepresentation.DECIMAL import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -96,14 +95,8 @@ public abstract class TomlAbstractEncoder protected constructor( } override fun encodeLong(value: Long) { - if (attributes.intRepresentation != DECIMAL) { - throw UnsupportedEncodingFeatureException( - "Non-decimal integer representation is not yet supported." - ) - } - if (!encodeAsKey(value, "Long")) { - appendValue(TomlLong(value)) + appendValue(TomlLong(value, attributes.intRepresentation)) } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt index ef0a126b..3f8f303d 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt @@ -1,22 +1,54 @@ package com.akuleshov7.ktoml.tree.nodes.pairs.values import com.akuleshov7.ktoml.TomlOutputConfig +import com.akuleshov7.ktoml.writers.IntegerRepresentation +import com.akuleshov7.ktoml.writers.IntegerRepresentation.* import com.akuleshov7.ktoml.writers.TomlEmitter /** * Toml AST Node for a representation of Arbitrary 64-bit signed integers: key = 1 * @property content + * @property representation The representation of the integer. */ public class TomlLong internal constructor( - override var content: Any + override var content: Any, + public var representation: IntegerRepresentation = DECIMAL ) : TomlValue() { - public constructor(content: String, lineNo: Int) : this(content.toLong()) + public constructor(content: String, lineNo: Int) : this(content.parse()) + + private constructor(pair: Pair) : this(pair.first, pair.second) override fun write( emitter: TomlEmitter, config: TomlOutputConfig, multiline: Boolean ) { - emitter.emitValue(content as Long) + emitter.emitValue(content as Long, representation) + } + + public companion object { + private const val BIN_RADIX = 2 + private const val HEX_RADIX = 16 + private const val OCT_RADIX = 8 + private val prefixRegex = "(?<=0[box])".toRegex() + + private fun String.parse(): Pair { + val value = replace("_", "").split(prefixRegex, limit = 2) + + return if (value.size == 2) { + val (prefix, digits) = value + + when (prefix) { + "0b" -> digits.toLong(BIN_RADIX) to BINARY + "0o" -> digits.toLong(OCT_RADIX) to OCTAL + "0x" -> digits.toLong(HEX_RADIX) to HEX + else -> throw NumberFormatException( + "Invalid radix prefix $prefix: expected \"0b\", \"0o\", or \"0x\"." + ) + } + } else { + value.first().toLong() to DECIMAL + } + } } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt index 149cd22d..e2fe2c08 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt @@ -198,6 +198,7 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { * @param representation How the integer will be represented in TOML. * @return this instance */ + @Suppress("MAGIC_NUMBER") public fun emitValue(integer: Long, representation: IntegerRepresentation = DECIMAL): TomlEmitter = when (representation) { DECIMAL -> emit(integer.toString()) @@ -210,7 +211,25 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { OCTAL -> emit("0o") .emit(integer.toString(8)) - GROUPED -> TODO() + GROUPED -> + emit( + if (integer < 1000) { + // No grouping needed. + integer.toString() + } else { + buildList { + var cur = integer + + do { + val group = cur % 1_000 + cur /= 1_000 + this += group + } while (cur > 0) + }.reversed().joinToString(separator = "_") { + it.toString().padStart(3, '0') + }.trimStart('0') + } + ) } /** diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt index 8e97b484..9b096212 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt @@ -211,7 +211,7 @@ class EncodingAnnotationTest { @TomlInteger(BINARY) val bin: Long = 2, @TomlInteger(GROUPED) - val gro: Long = 1_000_000, + val gro: Long = 9_999_099_009, @TomlInteger(HEX) val hex: Long = 4, @TomlInteger(OCTAL) @@ -223,7 +223,7 @@ class EncodingAnnotationTest { expectedToml = """ dec = 0 bin = 0b10 - gro = 1_000_000 + gro = 9_999_099_009 hex = 0x4 oct = 0o6 """.trimIndent() From 7f10ad513e3f4f95f1abb00b893f3ec3fc83ef6f Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Fri, 27 Jan 2023 04:10:36 -0700 Subject: [PATCH 03/11] Implement multiline strings --- .../ktoml/encoders/TomlAbstractEncoder.kt | 10 +--- .../tree/nodes/pairs/values/TomlArray.kt | 9 +-- .../nodes/pairs/values/TomlBasicString.kt | 16 ++---- .../tree/nodes/pairs/values/TomlBoolean.kt | 3 +- .../tree/nodes/pairs/values/TomlDateTime.kt | 3 +- .../tree/nodes/pairs/values/TomlDouble.kt | 3 +- .../nodes/pairs/values/TomlLiteralString.kt | 35 ++++++++---- .../ktoml/tree/nodes/pairs/values/TomlLong.kt | 3 +- .../ktoml/tree/nodes/pairs/values/TomlNull.kt | 3 +- .../tree/nodes/pairs/values/TomlValue.kt | 18 ++++-- .../com/akuleshov7/ktoml/utils/Regexes.kt | 20 ++++++- .../ktoml/utils/SpecialCharacters.kt | 57 +++++++++++-------- .../akuleshov7/ktoml/writers/TomlEmitter.kt | 1 - .../ktoml/encoders/EncodingAnnotationTest.kt | 15 ++++- 14 files changed, 120 insertions(+), 76 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt index 32f6e0f4..77ba53c2 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt @@ -105,18 +105,12 @@ public abstract class TomlAbstractEncoder protected constructor( } override fun encodeString(value: String) { - if (attributes.isMultiline) { - throw UnsupportedEncodingFeatureException( - "Multiline strings are not yet supported." - ) - } - if (!encodeAsKey(value)) { appendValue( if (attributes.isLiteral) { - TomlLiteralString(value) + TomlLiteralString(value, attributes.isMultiline) } else { - TomlBasicString(value) + TomlBasicString(value, attributes.isMultiline) } ) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt index d0deed4e..36ed9703 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt @@ -12,9 +12,11 @@ import com.akuleshov7.ktoml.writers.TomlEmitter /** * Toml AST Node for a representation of arrays: key = [value1, value2, value3] * @property content + * @property multiline */ public class TomlArray internal constructor( - override var content: Any + override var content: Any, + public var multiline: Boolean = false ) : TomlValue() { public constructor( rawContent: String, @@ -59,8 +61,7 @@ public class TomlArray internal constructor( @Suppress("UNCHECKED_CAST") public override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { emitter.startArray() @@ -81,7 +82,7 @@ public class TomlArray internal constructor( emitter.emitNewLine() .emitIndent() - value.write(emitter, config, multiline = value is TomlArray) + value.write(emitter, config) if (i < last) { emitter.emitElementDelimiter() diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt index 514038cb..95b4b8d7 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt @@ -2,7 +2,6 @@ package com.akuleshov7.ktoml.tree.nodes.pairs.values import com.akuleshov7.ktoml.TomlOutputConfig import com.akuleshov7.ktoml.exceptions.ParseException -import com.akuleshov7.ktoml.exceptions.TomlWritingException import com.akuleshov7.ktoml.parsers.trimQuotes import com.akuleshov7.ktoml.utils.convertSpecialCharacters import com.akuleshov7.ktoml.utils.escapeSpecialCharacters @@ -11,9 +10,11 @@ import com.akuleshov7.ktoml.writers.TomlEmitter /** * Toml AST Node for a representation of string values: key = "value" (always should have quotes due to TOML standard) * @property content + * @property multiline Whether the string is multiline. */ public class TomlBasicString internal constructor( - override var content: Any + override var content: Any, + public var multiline: Boolean = false ) : TomlValue() { public constructor( content: String, @@ -22,19 +23,12 @@ public class TomlBasicString internal constructor( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { - if (multiline) { - throw TomlWritingException( - "Multiline strings are not yet supported." - ) - } - val content = content as String emitter.emitValue( - content.escapeSpecialCharacters(), + content.escapeSpecialCharacters(multiline), isLiteral = false, multiline ) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBoolean.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBoolean.kt index 09e36542..c5e39a25 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBoolean.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBoolean.kt @@ -14,8 +14,7 @@ public class TomlBoolean internal constructor( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { emitter.emitValue(content as Boolean) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDateTime.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDateTime.kt index 1d973c70..bc706865 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDateTime.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDateTime.kt @@ -17,8 +17,7 @@ internal constructor( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { when (val content = content) { is Instant -> emitter.emitValue(content) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDouble.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDouble.kt index d87f9088..ea383f84 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDouble.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlDouble.kt @@ -19,8 +19,7 @@ internal constructor( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { emitter.emitValue(content as Double) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLiteralString.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLiteralString.kt index 79cdba47..433803a3 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLiteralString.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLiteralString.kt @@ -7,6 +7,7 @@ import com.akuleshov7.ktoml.exceptions.ParseException import com.akuleshov7.ktoml.exceptions.TomlWritingException import com.akuleshov7.ktoml.parsers.trimSingleQuotes import com.akuleshov7.ktoml.utils.controlCharacterRegex +import com.akuleshov7.ktoml.utils.multilineControlCharacterRegex import com.akuleshov7.ktoml.writers.TomlEmitter /** @@ -14,9 +15,11 @@ import com.akuleshov7.ktoml.writers.TomlEmitter * The only difference from the TOML specification (https://toml.io/en/v1.0.0) is that we will have one escaped symbol - * single quote and so it will be possible to use a single quote inside. * @property content + * @property multiline Whether the string is multiline. */ public class TomlLiteralString internal constructor( - override var content: Any + override var content: Any, + public var multiline: Boolean = false ) : TomlValue() { public constructor( content: String, @@ -39,19 +42,12 @@ public class TomlLiteralString internal constructor( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { - if (multiline) { - throw TomlWritingException( - "Multiline strings are not yet supported." - ) - } - val content = content as String emitter.emitValue( - content.escapeQuotesAndVerify(config), + content.escapeQuotesAndVerify(config, multiline), isLiteral = true, multiline ) @@ -79,8 +75,25 @@ public class TomlLiteralString internal constructor( */ private fun String.convertSingleQuotes(): String = this.replace("\\'", "'") - private fun String.escapeQuotesAndVerify(config: TomlOutputConfig) = + private fun String.escapeQuotesAndVerify(config: TomlOutputConfig, multiline: Boolean) = when { + multiline -> + when { + multilineControlCharacterRegex in this -> + throw TomlWritingException( + "Control characters (excluding tab and line" + + " terminators) are not permitted in" + + " multiline literal strings." + ) + config.allowEscapedQuotesInLiteralStrings -> + replace("'''", "''\\'") + "'''" in this -> + throw TomlWritingException( + "Three or more consecutive single quotes are not" + + " permitted in multiline literal strings." + ) + else -> this + } controlCharacterRegex in this -> throw TomlWritingException( "Control characters (excluding tab) are not permitted" + diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt index 3f8f303d..62ed350c 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlLong.kt @@ -20,8 +20,7 @@ public class TomlLong internal constructor( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { emitter.emitValue(content as Long, representation) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlNull.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlNull.kt index e0d0a3d1..03f7c479 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlNull.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlNull.kt @@ -18,8 +18,7 @@ public class TomlNull() : TomlValue() { override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { emitter.emitNullValue() } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlValue.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlValue.kt index 0d79d49d..a8827c38 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlValue.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlValue.kt @@ -29,17 +29,25 @@ public sealed class TomlValue { multiline: Boolean = false ): Unit = write(emitter, config.output, multiline) + @Deprecated( + message = "The multiline parameter overload is deprecated, use the multiline" + + " property on supported types instead. Will be removed in next releases.", + replaceWith = ReplaceWith("write(emitter, config)") + ) + public fun write( + emitter: TomlEmitter, + config: TomlOutputConfig = TomlOutputConfig(), + multiline: Boolean + ): Unit = write(emitter, config) + /** - * Writes this value to the specified [emitter], optionally writing the value - * [multiline] (if supported by the value type). + * Writes this value to the specified [emitter]. * * @param emitter * @param config - * @param multiline */ public abstract fun write( emitter: TomlEmitter, - config: TomlOutputConfig = TomlOutputConfig(), - multiline: Boolean = false + config: TomlOutputConfig = TomlOutputConfig() ) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Regexes.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Regexes.kt index 19dcda3b..7ed8bb26 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Regexes.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/utils/Regexes.kt @@ -15,6 +15,13 @@ package com.akuleshov7.ktoml.utils internal val controlCharacterRegex: Regex = Regex("""[\x00-\x08\x0A-\x1F\x7F\x80-\x9F]""") +/** + * Matches a multiline literal string. This is the same as [controlCharacterRegex], + * but excludes line terminators. + */ +internal val multilineControlCharacterRegex: Regex = + Regex("""[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F\x80-\x9F]""") + /** * Matches an unescaped backlash character. A backslash will be excluded if followed * by: @@ -25,9 +32,20 @@ internal val controlCharacterRegex: Regex = internal val unescapedBackslashRegex: Regex = Regex("""\\\\|\\(?![btnfr"]|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})""") +/** + * Matches an unescaped backlash character in a multiline string. A backslash will + * be excluded if followed by: + * - another backslash + * - any of the "compact escapes" (i.e. `\t`, `\"`, etc.) + * - a Unicode escape, "u" or "U" followed by 4 or 8 hex digits respectively + * - A line terminator (line break) + */ +internal val multilineUnescapedBackslashRegex: Regex = + Regex("""\\\\|\\(?![btnfr"\r\n]|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})""", RegexOption.MULTILINE) + /** * Matches an unescaped double quote character, possibly with escaped backslashes - * preceding it. Used to + * preceding it. */ internal val unescapedDoubleQuoteRegex: Regex = Regex("""(? - when (val char = match.value.single()) { - '\t' -> "\\t" - '\b' -> "\\b" - '\n' -> "\\n" - '\u000C' -> "\\f" - '\r' -> "\\r" - else -> { - val code = char.code +public fun String.escapeSpecialCharacters(multiline: Boolean = false): String = + if (multiline) { + val withCtrlCharsEscaped = escapeCtrlChars(multilineControlCharacterRegex) + val withQuotesEscaped = withCtrlCharsEscaped.replace("\"\"\"", "\"\"\\\"") - val hexDigits = code.toString(HEX_RADIX) - - "\\$SIMPLE_UNICODE_PREFIX${ - hexDigits.padStart(SIMPLE_UNICODE_LENGTH, '0') - }" + withQuotesEscaped.replace( + multilineUnescapedBackslashRegex, + Regex.escapeReplacement("\\\\") + ) + } else { + val withCtrlCharsEscaped = escapeCtrlChars(controlCharacterRegex) + val withQuotesEscaped = withCtrlCharsEscaped.replace(unescapedDoubleQuoteRegex) { match -> + match.value.replace("\"", "\\\"") } + + withQuotesEscaped.replace( + unescapedBackslashRegex, + Regex.escapeReplacement("\\\\") + ) } - } - val withQuotesEscaped = withCtrlCharsEscaped.replace(unescapedDoubleQuoteRegex) { match -> - match.value.replace("\"", "\\\"") - } +private fun String.escapeCtrlChars(regex: Regex) = replace(regex) { match -> + when (val char = match.value.single()) { + '\t' -> "\\t" + '\b' -> "\\b" + '\n' -> "\\n" + '\u000C' -> "\\f" + '\r' -> "\\r" + else -> { + val code = char.code + + val hexDigits = code.toString(HEX_RADIX) - return withQuotesEscaped.replace( - unescapedBackslashRegex, - Regex.escapeReplacement("\\\\") - ) + "\\$SIMPLE_UNICODE_PREFIX${ + hexDigits.padStart(SIMPLE_UNICODE_LENGTH, '0') + }" + } + } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt index e2fe2c08..30b96573 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt @@ -180,7 +180,6 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { val quotes = if (isLiteral) "'''" else "\"\"\"" emit(quotes) - .emitNewLine() .emit(string) .emit(quotes) } else { diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt index 9b096212..3147c7b8 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/encoders/EncodingAnnotationTest.kt @@ -251,9 +251,16 @@ class EncodingAnnotationTest { data class File( @TomlMultiline val mlTextA: String = "\n\\tMultiline\ntext!\n", + @TomlMultiline + val mlTextB: String = """ + + Text with escaped quotes ""\"\ + and line break + + """.trimIndent(), @TomlLiteral @TomlMultiline - val mlTextB: String = "\n\"Multiline\n\"text!\n" + val mlTextC: String = "\n\"Multiline\ntext!\"\n" ) val tripleQuotes = "\"\"\"" @@ -265,7 +272,11 @@ class EncodingAnnotationTest { \tMultiline text! $tripleQuotes - mlTextB = ''' + mlTextB = $tripleQuotes + Text with escaped quotes ""\"\ + and line break + $tripleQuotes + mlTextC = ''' "Multiline text!" ''' From 1d298827fb1a51669b983d8cad2cd7960c65aeb0 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Sat, 28 Jan 2023 02:28:37 -0700 Subject: [PATCH 04/11] Implement multiline arrays --- .../ktoml/encoders/TomlAbstractEncoder.kt | 9 +++++++ .../ktoml/encoders/TomlArrayEncoder.kt | 2 +- .../akuleshov7/ktoml/tree/nodes/TomlFile.kt | 10 ++------ .../akuleshov7/ktoml/tree/nodes/TomlNode.kt | 25 +++++++++++++++---- .../tree/nodes/other/TomlStubEmptyNode.kt | 3 +-- .../ktoml/tree/nodes/pairs/TomlKeyValue.kt | 7 +++--- .../tree/nodes/pairs/TomlKeyValueArray.kt | 8 +++--- .../tree/nodes/pairs/TomlKeyValuePrimitive.kt | 5 ++-- .../tree/nodes/pairs/values/TomlArray.kt | 2 +- .../tree/nodes/tables/TomlArrayOfTables.kt | 12 ++++----- .../tree/nodes/tables/TomlInlineTable.kt | 3 +-- .../ktoml/tree/nodes/tables/TomlTable.kt | 7 +++--- .../tree/nodes/tables/TomlTablePrimitive.kt | 7 +++--- .../ktoml/writers/KeyValueWriteTest.kt | 21 ++++++++-------- .../ktoml/writers/ValueWriteTest.kt | 23 +++++++++-------- 15 files changed, 81 insertions(+), 63 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt index 77ba53c2..5840a854 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlAbstractEncoder.kt @@ -190,6 +190,15 @@ public abstract class TomlAbstractEncoder protected constructor( } } + // Force primitive array elements to be single-line. + if (attributes.isInline && descriptor.kind == StructureKind.LIST) { + when (typeDescriptor.kind) { + is PrimitiveKind, + SerialKind.ENUM -> attributes.isMultiline = false + else -> { } + } + } + return true } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlArrayEncoder.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlArrayEncoder.kt index 3178eeb3..62a84cd3 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlArrayEncoder.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/encoders/TomlArrayEncoder.kt @@ -117,7 +117,7 @@ public class TomlArrayEncoder internal constructor( override fun endStructure(descriptor: SerialDescriptor) { if (attributes.isInline) { - val array = TomlArray(values) + val array = TomlArray(values, attributes.isMultiline) parent?.let { appendValueTo(array, parent) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlFile.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlFile.kt index e620b16d..b1cef672 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlFile.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlFile.kt @@ -26,14 +26,8 @@ public class TomlFile() : TomlNode( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean - ): Unit = - emitter.writeChildren( - children, - config, - multiline - ) + config: TomlOutputConfig + ): Unit = emitter.writeChildren(children, config) override fun toString(): String = "rootNode" } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlNode.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlNode.kt index 251c06cb..37074e8f 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlNode.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/TomlNode.kt @@ -251,16 +251,31 @@ public sealed class TomlNode( * @param config The [TomlConfig] instance. Defaults to the node's config. * @param multiline Whether to write the node over multiple lines, if possible. */ - public abstract fun write( + @Deprecated( + message = "The multiline parameter overload is deprecated, use the multiline" + + " property on supported TomlValue types instead. Will be removed in next releases.", + replaceWith = ReplaceWith("write(emitter, config)") + ) + public fun write( emitter: TomlEmitter, config: TomlOutputConfig = TomlOutputConfig(), multiline: Boolean = false + ): Unit = write(emitter, config) + + /** + * Writes this node as text to [emitter]. + * + * @param emitter The [TomlEmitter] instance to write to. + * @param config The [TomlConfig] instance. Defaults to the node's config. + */ + public abstract fun write( + emitter: TomlEmitter, + config: TomlOutputConfig = TomlOutputConfig() ) protected open fun TomlEmitter.writeChildren( children: List, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { val last = children.lastIndex @@ -272,7 +287,7 @@ public sealed class TomlNode( emitIndent() } - child.write(emitter = this, config, multiline) + child.write(emitter = this, config) if (child is TomlKeyValue || child is TomlInlineTable) { writeChildInlineComment(child) @@ -283,7 +298,7 @@ public sealed class TomlNode( // A single newline follows single-line pairs, except when a table // follows. Two newlines follow multi-line pairs. - if ((child is TomlKeyValueArray && multiline) || children[i + 1] is TomlTable) { + if ((child is TomlKeyValueArray && child.isMultiline()) || children[i + 1] is TomlTable) { emitNewLine() } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/other/TomlStubEmptyNode.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/other/TomlStubEmptyNode.kt index e69a9e39..d2820ca2 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/other/TomlStubEmptyNode.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/other/TomlStubEmptyNode.kt @@ -24,8 +24,7 @@ public class TomlStubEmptyNode(lineNo: Int) : TomlNode( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { // Nothing to write in stub nodes. } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValue.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValue.kt index 687e9119..343b0dcb 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValue.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValue.kt @@ -58,16 +58,17 @@ internal interface TomlKeyValue { ) } + fun isMultiline() = false + fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { key.write(emitter) emitter.emitPairDelimiter() - value.write(emitter, config, multiline) + value.write(emitter, config) } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValueArray.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValueArray.kt index 3b5c1e3e..ca36c5d4 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValueArray.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValueArray.kt @@ -4,6 +4,7 @@ import com.akuleshov7.ktoml.TomlConfig import com.akuleshov7.ktoml.TomlInputConfig import com.akuleshov7.ktoml.TomlOutputConfig import com.akuleshov7.ktoml.tree.nodes.pairs.keys.TomlKey +import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlArray import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlValue import com.akuleshov7.ktoml.writers.TomlEmitter @@ -77,9 +78,10 @@ public class TomlKeyValueArray( config.input ) + override fun isMultiline(): Boolean = (value as TomlArray).multiline + public override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean - ): Unit = super.write(emitter, config, multiline) + config: TomlOutputConfig + ): Unit = super.write(emitter, config) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValuePrimitive.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValuePrimitive.kt index 742be98b..12d8ebb6 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValuePrimitive.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/TomlKeyValuePrimitive.kt @@ -78,7 +78,6 @@ public class TomlKeyValuePrimitive( public override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean - ): Unit = super.write(emitter, config, multiline) + config: TomlOutputConfig + ): Unit = super.write(emitter, config) } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt index 36ed9703..75a2aadc 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlArray.kt @@ -67,7 +67,7 @@ public class TomlArray internal constructor( val content = (content as List).map { if (it is List<*>) { - TomlArray(it) + TomlArray(it, multiline) } else { it as TomlValue } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlArrayOfTables.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlArrayOfTables.kt index bac8fbf8..42184c4a 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlArrayOfTables.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlArrayOfTables.kt @@ -62,8 +62,7 @@ public class TomlArrayOfTables( override fun TomlEmitter.writeChildren( children: List, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { val last = children.lastIndex @@ -83,7 +82,7 @@ public class TomlArrayOfTables( indent() - child.write(emitter = this, config, multiline) + child.write(emitter = this, config) dedent() @@ -97,7 +96,7 @@ public class TomlArrayOfTables( } } } else { - child.write(emitter = this, config, multiline) + child.write(emitter = this, config) } } } @@ -119,9 +118,8 @@ public class TomlArrayOfTablesElement( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean - ): Unit = emitter.writeChildren(children, config, multiline) + config: TomlOutputConfig + ): Unit = emitter.writeChildren(children, config) override fun toString(): String = EMPTY_TECHNICAL_NODE } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlInlineTable.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlInlineTable.kt index d7142374..fa348412 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlInlineTable.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlInlineTable.kt @@ -100,8 +100,7 @@ public class TomlInlineTable internal constructor( public override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { key.write(emitter) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTable.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTable.kt index d1c525f6..5bc37808 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTable.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTable.kt @@ -73,8 +73,7 @@ public abstract class TomlTable( override fun write( emitter: TomlEmitter, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { // Todo: Option to explicitly define super tables? // Todo: Support dotted key-value pairs (i.e. a.b.c.d = 7) @@ -94,11 +93,11 @@ public abstract class TomlTable( emitter.indent() - emitter.writeChildren(children, config, multiline) + emitter.writeChildren(children, config) emitter.dedent() } else { - emitter.writeChildren(children, config, multiline) + emitter.writeChildren(children, config) } } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTablePrimitive.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTablePrimitive.kt index cffd7647..ad647128 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTablePrimitive.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/tables/TomlTablePrimitive.kt @@ -74,8 +74,7 @@ public class TomlTablePrimitive( override fun TomlEmitter.writeChildren( children: List, - config: TomlOutputConfig, - multiline: Boolean + config: TomlOutputConfig ) { if (children.first() is TomlStubEmptyNode) { return @@ -109,14 +108,14 @@ public class TomlTablePrimitive( else -> emitIndent() } - child.write(emitter = this, config, multiline) + child.write(emitter = this, config) writeChildInlineComment(child) if (i < children.lastIndex) { emitNewLine() // A single newline follows single-line pairs, except when a table // follows. Two newlines follow multi-line pairs. - if ((child is TomlKeyValue && multiline) || children[i + 1] is TomlTable) { + if ((child is TomlKeyValue && child.isMultiline()) || children[i + 1] is TomlTable) { emitNewLine() } } diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/KeyValueWriteTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/KeyValueWriteTest.kt index c83640b5..6323a83b 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/KeyValueWriteTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/KeyValueWriteTest.kt @@ -6,6 +6,7 @@ import com.akuleshov7.ktoml.tree.nodes.TomlInlineTable import com.akuleshov7.ktoml.tree.nodes.TomlKeyValueArray import com.akuleshov7.ktoml.tree.nodes.TomlKeyValuePrimitive import com.akuleshov7.ktoml.tree.nodes.TomlNode +import com.akuleshov7.ktoml.tree.nodes.pairs.values.TomlArray import kotlin.test.Test import kotlin.test.assertEquals @@ -99,8 +100,7 @@ fun testTomlPrimitivePair( ) = testTomlPair( TomlKeyValuePrimitive(pair, 0, config = inputConfig), expectedString = "${pair.first} = ${pair.second}", - outputConfig, - multiline = false + outputConfig ) fun testTomlArrayPair( @@ -109,10 +109,13 @@ fun testTomlArrayPair( inputConfig: TomlInputConfig = TomlInputConfig(), outputConfig: TomlOutputConfig = TomlOutputConfig() ) = testTomlPair( - TomlKeyValueArray(pair, 0, config = inputConfig), + TomlKeyValueArray(pair, 0, config = inputConfig).also { + val array = it.value as TomlArray + + array.multiline = multiline + }, expectedString = "${pair.first} = ${pair.second}", - outputConfig, - multiline + outputConfig ) fun testTomlInlineTablePair( @@ -122,22 +125,20 @@ fun testTomlInlineTablePair( ) = testTomlPair( TomlInlineTable(pair, 0, config = inputConfig), expectedString = "${pair.first} = ${pair.second}", - outputConfig, - multiline = false + outputConfig ) fun testTomlPair( pair: TomlNode, expectedString: String, - config: TomlOutputConfig = TomlOutputConfig(), - multiline: Boolean + config: TomlOutputConfig = TomlOutputConfig() ) { assertEquals( expectedString, actual = buildString { val emitter = TomlStringEmitter(this, config) - pair.write(emitter, config, multiline) + pair.write(emitter, config) } ) } \ No newline at end of file diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt index ce81397d..df84db0b 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt @@ -104,15 +104,17 @@ class PrimitiveValueWriteTest { @Test fun arrayWriteTest() { + val innerArray = TomlArray( + listOf( + TomlDouble(3.14) + ) + ) + val array = TomlArray( listOf( TomlLong(1L), TomlBasicString("string" as Any), - TomlArray( - listOf( - TomlDouble(3.14) - ) - ) + innerArray ) ) @@ -125,6 +127,9 @@ class PrimitiveValueWriteTest { // Multiline + innerArray.multiline = true + array.multiline = true + testTomlValue( array, """ @@ -135,8 +140,7 @@ class PrimitiveValueWriteTest { 3.14 ] ] - """.trimIndent(), - multiline = true + """.trimIndent() ) } } @@ -144,15 +148,14 @@ class PrimitiveValueWriteTest { fun testTomlValue( value: TomlValue, expectedString: String, - config: TomlOutputConfig = TomlOutputConfig(), - multiline: Boolean = false + config: TomlOutputConfig = TomlOutputConfig() ) { assertEquals( expectedString, actual = buildString { val emitter = TomlStringEmitter(this, config) - value.write(emitter, config, multiline) + value.write(emitter, config) } ) } From d310246de3dca74248b13a9ccb37e2b8a0d94d58 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Sun, 29 Jan 2023 17:38:40 -0700 Subject: [PATCH 05/11] Remove int grouping support TODO --- .../com/akuleshov7/ktoml/writers/IntegerRepresentation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt index 0a17522b..7e682b03 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt @@ -5,7 +5,7 @@ package com.akuleshov7.ktoml.writers * * @property BINARY A binary number prefixed with `0b`. * @property DECIMAL A decimal number. - * @property GROUPED A grouped decimal number, such as `1_000_000`. Todo: Add support. + * @property GROUPED A grouped decimal number, such as `1_000_000`. * @property HEX A hexadecimal number prefixed with `0x`. * @property OCTAL An octal number prefixed with `0o`. */ From 9128db87bfb06c22f75a14cb7c24454b95f607d4 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Mon, 30 Jan 2023 14:56:39 -0700 Subject: [PATCH 06/11] Fix negative integer representation --- .../akuleshov7/ktoml/writers/TomlEmitter.kt | 72 ++++++++++--------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt index 30b96573..b6808331 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt @@ -198,38 +198,46 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { * @return this instance */ @Suppress("MAGIC_NUMBER") - public fun emitValue(integer: Long, representation: IntegerRepresentation = DECIMAL): TomlEmitter = - when (representation) { - DECIMAL -> emit(integer.toString()) - HEX -> - emit("0x") - .emit(integer.toString(16)) - BINARY -> - emit("0b") - .emit(integer.toString(2)) - OCTAL -> - emit("0o") - .emit(integer.toString(8)) - GROUPED -> - emit( - if (integer < 1000) { - // No grouping needed. - integer.toString() - } else { - buildList { - var cur = integer - - do { - val group = cur % 1_000 - cur /= 1_000 - this += group - } while (cur > 0) - }.reversed().joinToString(separator = "_") { - it.toString().padStart(3, '0') - }.trimStart('0') - } - ) - } + public fun emitValue(integer: Long, representation: IntegerRepresentation = DECIMAL): TomlEmitter { + var value = if (integer < 0) { + emit('-') + -integer + } else { + integer + } + + return when (representation) { + DECIMAL -> emit(value.toString()) + HEX -> + emit("0x") + .emit(value.toString(16)) + + BINARY -> + emit("0b") + .emit(value.toString(2)) + + OCTAL -> + emit("0o") + .emit(value.toString(8)) + + GROUPED -> + emit( + if (value < 1000) { + // No grouping needed. + value.toString() + } else { + buildList { + do { + this += value % 1_000 + value /= 1_000 + } while (value > 0) + }.reversed().joinToString(separator = "_") { + it.toString().padStart(3, '0') + }.trimStart('0') + } + ) + } + } /** * Emits a floating-point value. From e87e74c7778e23096cce0e95b87d9284ce5db313 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Mon, 30 Jan 2023 15:03:45 -0700 Subject: [PATCH 07/11] Uncomment and add tests for int representation emission --- .../com/akuleshov7/ktoml/writers/ValueWriteTest.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt index df84db0b..261c9d83 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt @@ -50,7 +50,6 @@ class PrimitiveValueWriteTest { testTomlValue(TomlBasicString("""hello\u0000\\\Uffffffff world""" as Any), """"hello\u0000\\\Uffffffff world"""") } - @Suppress("COMMENTED_CODE") @Test fun integerWriteTest() { // Decimal @@ -58,13 +57,20 @@ class PrimitiveValueWriteTest { testTomlValue(TomlLong(-1234567L), "-1234567") // Hex - //testTomlValue(TomlLong(0xdeadc0de, IntegerRepresentation.HEX), "0xdeadc0de") + testTomlValue(TomlLong(0xdeadc0deL, IntegerRepresentation.HEX), "0xdeadc0de") + testTomlValue(TomlLong(-0xdeadc0deL, IntegerRepresentation.HEX), "-0xdeadc0de") // Binary - //testTomlValue(TomlLong(0b10000000, IntegerRepresentation.BINARY), "0b10000000") + testTomlValue(TomlLong(0b10000000L, IntegerRepresentation.BINARY), "0b10000000") + testTomlValue(TomlLong(-0b10000000L, IntegerRepresentation.BINARY), "-0b10000000") // Octal - //testTomlValue(TomlLong(0x1FF, IntegerRepresentation.OCTAL), "0o777") + testTomlValue(TomlLong(0x1FFL, IntegerRepresentation.OCTAL), "0o777") + testTomlValue(TomlLong(-0x1FFL, IntegerRepresentation.OCTAL), "-0o777") + + // Grouped + testTomlValue(TomlLong(1234567L, IntegerRepresentation.GROUPED), "1_234_567") + testTomlValue(TomlLong(-1234567L, IntegerRepresentation.GROUPED), "-1_234_567") } @Test From 276a27e0547c4dab9b9c14877b20036cd794cc0c Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Mon, 30 Jan 2023 18:04:14 -0700 Subject: [PATCH 08/11] Simplify integer emission --- .../ktoml/writers/IntegerRepresentation.kt | 38 ++++++++++--- .../akuleshov7/ktoml/writers/TomlEmitter.kt | 56 +++++++------------ 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt index 7e682b03..4512991e 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/IntegerRepresentation.kt @@ -3,17 +3,37 @@ package com.akuleshov7.ktoml.writers /** * How a TOML integer should be represented during encoding. * - * @property BINARY A binary number prefixed with `0b`. - * @property DECIMAL A decimal number. - * @property GROUPED A grouped decimal number, such as `1_000_000`. - * @property HEX A hexadecimal number prefixed with `0x`. - * @property OCTAL An octal number prefixed with `0o`. + * @property prefix The prefix, if any, signalling this representation in TOML. + * @property radix The radix or base number of the representation. */ -public enum class IntegerRepresentation { - BINARY, +@Suppress("MAGIC_NUMBER") +public enum class IntegerRepresentation( + public val prefix: String = "", + public val radix: Int = 10, +) { + /** + * A binary number prefixed with `0b`. + */ + BINARY("0b", 2), + + /** + * A decimal number. + */ DECIMAL, + + /** + * A grouped decimal number, such as `1_000_000`. + */ GROUPED, - HEX, - OCTAL, + + /** + * A hexadecimal number prefixed with `0x`. + */ + HEX("0x", 16), + + /** + * An octal number prefixed with `0o`. + */ + OCTAL("0o", 8), ; } diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt index b6808331..afa003ca 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt @@ -199,44 +199,28 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { */ @Suppress("MAGIC_NUMBER") public fun emitValue(integer: Long, representation: IntegerRepresentation = DECIMAL): TomlEmitter { - var value = if (integer < 0) { - emit('-') - -integer - } else { - integer - } + var value = integer - return when (representation) { - DECIMAL -> emit(value.toString()) - HEX -> - emit("0x") - .emit(value.toString(16)) - - BINARY -> - emit("0b") - .emit(value.toString(2)) - - OCTAL -> - emit("0o") - .emit(value.toString(8)) - - GROUPED -> - emit( - if (value < 1000) { - // No grouping needed. - value.toString() - } else { - buildList { - do { - this += value % 1_000 - value /= 1_000 - } while (value > 0) - }.reversed().joinToString(separator = "_") { - it.toString().padStart(3, '0') - }.trimStart('0') - } - ) + if (value < 0) { + return emit('-').emitValue(-value, representation) } + + return emit(representation.prefix).emit( + when (representation) { + GROUPED -> + buildList { + while (value > 999) { + val group = value % 1_000 + this += group.toString().padStart(3, '0') + value /= 1_000 + } + + this += value.toString() + }.reversed().joinToString(separator = "_") + else -> + value.toString(representation.radix) + } + ) } /** From d36a0cf04bf7c489b1a632384703c13478166a0e Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Tue, 31 Jan 2023 04:04:27 -0700 Subject: [PATCH 09/11] Fix Long.MIN_VALUE case --- .../com/akuleshov7/ktoml/writers/TomlEmitter.kt | 14 ++++++++------ .../com/akuleshov7/ktoml/writers/ValueWriteTest.kt | 5 +++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt index afa003ca..0114fc2d 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt @@ -4,6 +4,7 @@ import com.akuleshov7.ktoml.TomlOutputConfig import com.akuleshov7.ktoml.utils.bareKeyRegex import com.akuleshov7.ktoml.utils.literalKeyCandidateRegex import com.akuleshov7.ktoml.writers.IntegerRepresentation.* +import kotlin.math.abs import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -200,25 +201,26 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { @Suppress("MAGIC_NUMBER") public fun emitValue(integer: Long, representation: IntegerRepresentation = DECIMAL): TomlEmitter { var value = integer + val isNeg = value < 0 - if (value < 0) { - return emit('-').emitValue(-value, representation) + if (isNeg) { + emit('-') } return emit(representation.prefix).emit( when (representation) { GROUPED -> buildList { - while (value > 999) { + while (isNeg && value < -999 || !isNeg && value > 999) { val group = value % 1_000 - this += group.toString().padStart(3, '0') + this += abs(group).toString().padStart(3, '0') value /= 1_000 } - this += value.toString() + this += abs(value).toString() }.reversed().joinToString(separator = "_") else -> - value.toString(representation.radix) + value.toString(representation.radix).trimStart('-') } ) } diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt index 261c9d83..2f86aaf6 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/writers/ValueWriteTest.kt @@ -55,22 +55,27 @@ class PrimitiveValueWriteTest { // Decimal testTomlValue(TomlLong(1234567L), "1234567") testTomlValue(TomlLong(-1234567L), "-1234567") + testTomlValue(TomlLong(Long.MIN_VALUE), "${Long.MIN_VALUE}") // Hex testTomlValue(TomlLong(0xdeadc0deL, IntegerRepresentation.HEX), "0xdeadc0de") testTomlValue(TomlLong(-0xdeadc0deL, IntegerRepresentation.HEX), "-0xdeadc0de") + testTomlValue(TomlLong(Long.MIN_VALUE, IntegerRepresentation.HEX), "-0x" + Long.MIN_VALUE.toString(16).trimStart('-')) // Binary testTomlValue(TomlLong(0b10000000L, IntegerRepresentation.BINARY), "0b10000000") testTomlValue(TomlLong(-0b10000000L, IntegerRepresentation.BINARY), "-0b10000000") + testTomlValue(TomlLong(Long.MIN_VALUE, IntegerRepresentation.BINARY), "-0b" + Long.MIN_VALUE.toString(2).trimStart('-')) // Octal testTomlValue(TomlLong(0x1FFL, IntegerRepresentation.OCTAL), "0o777") testTomlValue(TomlLong(-0x1FFL, IntegerRepresentation.OCTAL), "-0o777") + testTomlValue(TomlLong(Long.MIN_VALUE, IntegerRepresentation.OCTAL), "-0o" + Long.MIN_VALUE.toString(8).trimStart('-')) // Grouped testTomlValue(TomlLong(1234567L, IntegerRepresentation.GROUPED), "1_234_567") testTomlValue(TomlLong(-1234567L, IntegerRepresentation.GROUPED), "-1_234_567") + testTomlValue(TomlLong(Long.MIN_VALUE, IntegerRepresentation.GROUPED), "-9_223_372_036_854_775_808") } @Test From d483f17faf5c4f7d2485ed6c3fa42e11fc11b180 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Tue, 31 Jan 2023 16:49:04 -0700 Subject: [PATCH 10/11] Add groupSize parameter and change grouping to chunked string --- .../akuleshov7/ktoml/writers/TomlEmitter.kt | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt index 0114fc2d..3ec3152b 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/writers/TomlEmitter.kt @@ -4,7 +4,6 @@ import com.akuleshov7.ktoml.TomlOutputConfig import com.akuleshov7.ktoml.utils.bareKeyRegex import com.akuleshov7.ktoml.utils.literalKeyCandidateRegex import com.akuleshov7.ktoml.writers.IntegerRepresentation.* -import kotlin.math.abs import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -196,33 +195,35 @@ public abstract class TomlEmitter(config: TomlOutputConfig) { * * @param integer * @param representation How the integer will be represented in TOML. + * @param groupSize The digit group size, or less than `1` for no grouping. For + * example, a group size of `3` emits `1_000_000`, `4` emits `0b1111_1111`, etc. * @return this instance */ - @Suppress("MAGIC_NUMBER") - public fun emitValue(integer: Long, representation: IntegerRepresentation = DECIMAL): TomlEmitter { - var value = integer - val isNeg = value < 0 + @Suppress("SAY_NO_TO_VAR") + public fun emitValue( + integer: Long, + representation: IntegerRepresentation = DECIMAL, + groupSize: Int = 0 + ): TomlEmitter { + // Todo: Add groupSize to the annotation and AST and remove GROUPED. + if (representation == GROUPED) { + return emitValue(integer, representation = DECIMAL, groupSize = 3) + } - if (isNeg) { + if (integer < 0) { emit('-') } - return emit(representation.prefix).emit( - when (representation) { - GROUPED -> - buildList { - while (isNeg && value < -999 || !isNeg && value > 999) { - val group = value % 1_000 - this += abs(group).toString().padStart(3, '0') - value /= 1_000 - } - - this += abs(value).toString() - }.reversed().joinToString(separator = "_") - else -> - value.toString(representation.radix).trimStart('-') - } - ) + var digits = integer.toString(representation.radix).trimStart('-') + + if (groupSize > 0) { + digits = (digits as CharSequence).reversed() + .chunked(groupSize, CharSequence::reversed) + .asReversed() + .joinToString(separator = "_") + } + + return emit(representation.prefix).emit(digits) } /** From fc768637c6c66ed412ec4334a6cdc19bf7ee3499 Mon Sep 17 00:00:00 2001 From: NightEule5 <24661563+NightEule5@users.noreply.github.com> Date: Fri, 17 Feb 2023 19:06:17 -0700 Subject: [PATCH 11/11] Clean up after merge --- .../akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt index 71f50063..1d46beff 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/tree/nodes/pairs/values/TomlBasicString.kt @@ -2,7 +2,6 @@ package com.akuleshov7.ktoml.tree.nodes.pairs.values import com.akuleshov7.ktoml.TomlOutputConfig import com.akuleshov7.ktoml.exceptions.ParseException -import com.akuleshov7.ktoml.exceptions.TomlWritingException import com.akuleshov7.ktoml.parsers.convertLineEndingBackslash import com.akuleshov7.ktoml.parsers.getCountOfOccurrencesOfSubstring import com.akuleshov7.ktoml.parsers.trimMultilineQuotes