Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support string and array multiline, and integer representation encoding #183

Merged
merged 13 commits into from
Feb 22, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -112,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)
}
)
}
Expand Down Expand Up @@ -203,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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<TomlNode>,
config: TomlOutputConfig,
multiline: Boolean
config: TomlOutputConfig
) {
val last = children.lastIndex

Expand All @@ -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)
Expand All @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<TomlKeyValue>.write(emitter, config, multiline)
config: TomlOutputConfig
): Unit = super<TomlKeyValue>.write(emitter, config)
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ public class TomlKeyValuePrimitive(

public override fun write(
emitter: TomlEmitter,
config: TomlOutputConfig,
multiline: Boolean
): Unit = super<TomlKeyValue>.write(emitter, config, multiline)
config: TomlOutputConfig
): Unit = super<TomlKeyValue>.write(emitter, config)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -59,14 +61,13 @@ public class TomlArray internal constructor(
@Suppress("UNCHECKED_CAST")
public override fun write(
emitter: TomlEmitter,
config: TomlOutputConfig,
multiline: Boolean
config: TomlOutputConfig
) {
emitter.startArray()

val content = (content as List<Any>).map {
if (it is List<*>) {
TomlArray(it)
TomlArray(it, multiline)
} else {
it as TomlValue
}
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,9 +13,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,
Expand All @@ -25,19 +26,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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ internal constructor(

override fun write(
emitter: TomlEmitter,
config: TomlOutputConfig,
multiline: Boolean
config: TomlOutputConfig
) {
emitter.emitValue(content as Double)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import com.akuleshov7.ktoml.parsers.getCountOfOccurrencesOfSubstring
import com.akuleshov7.ktoml.parsers.trimMultilineLiteralQuotes
import com.akuleshov7.ktoml.parsers.trimSingleQuotes
import com.akuleshov7.ktoml.utils.controlCharacterRegex
import com.akuleshov7.ktoml.utils.multilineControlCharacterRegex
import com.akuleshov7.ktoml.writers.TomlEmitter

/**
* Toml AST Node for a representation of literal string values: key = 'value' (with single quotes and no escaped symbols)
* 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,
Expand All @@ -42,19 +45,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
)
Expand Down Expand Up @@ -92,8 +88,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 ->
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regular expressions affect performance and in some cases can lead to exceptions in case of very long strings...
May it will be better to use something like string.any(setOfillegalCharacters::contains) instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, admittedly regexes aren't needed here. I assume you want a separate issue for this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I will make a separate one

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" +
Expand Down
Loading