diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt index 7e801eec..8715c2fb 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/StringUtils.kt @@ -6,36 +6,6 @@ package com.akuleshov7.ktoml.parsers import com.akuleshov7.ktoml.exceptions.ParseException -/** - * Takes only the text before a comment, searching for a comment after the specified - * [startIndex]. - * - * @param startIndex The index to start the comment search from. - * @return The text before a comment, i.e. - * ```kotlin - * "a = 0 # Comment".takeBeforeComment() == "a = 0" - * ``` - */ -internal fun String.takeBeforeComment(startIndex: Int) = - when (val hashIndex = indexOf('#', startIndex)) { - -1 -> trim() - else -> take(hashIndex).trim() - } - -/** - * Trims a comment of any text before it and its hash token. - * - * @return The comment text, i.e. - * ```kotlin - * "a = 0 # Comment".trimComment() == "Comment" - * ``` - */ -internal fun String.trimComment() = - when (val hashIndex = indexOf('#')) { - -1 -> "" - else -> drop(hashIndex + 1).trim() - } - /** * Splitting dot-separated string to the list of tokens: * a.b.c -> [a, b, c]; a."b.c".d -> [a, "b.c", d]; @@ -120,6 +90,44 @@ internal fun String.trimBrackets(): String = trimSymbols(this, "[", "]") */ internal fun String.trimDoubleBrackets(): String = trimSymbols(this, "[[", "]]") +/** + * Takes only the text before a comment + * + * @param allowEscapedQuotesInLiteralStrings value from TomlInputConfig + * @return The text before a comment, i.e. + * ```kotlin + * "a = 0 # Comment".takeBeforeComment() == "a = 0" + * ``` + */ +internal fun String.takeBeforeComment(allowEscapedQuotesInLiteralStrings: Boolean): String { + val commentStartIndex = getCommentStartIndex(allowEscapedQuotesInLiteralStrings) + + return if (commentStartIndex == -1) { + this.trim() + } else { + this.substring(0, commentStartIndex).trim() + } +} + +/** + * Trims a comment of any text before it and its hash token. + * + * @param allowEscapedQuotesInLiteralStrings value from TomlInputConfig + * @return The comment text, i.e. + * ```kotlin + * "a = 0 # Comment".trimComment() == "Comment" + * ``` + */ +internal fun String.trimComment(allowEscapedQuotesInLiteralStrings: Boolean): String { + val commentStartIndex = getCommentStartIndex(allowEscapedQuotesInLiteralStrings) + + return if (commentStartIndex == -1) { + "" + } else { + drop(commentStartIndex + 1).trim() + } +} + private fun String.validateSpaces(lineNo: Int, fullKey: String) { if (this.trim().count { it == ' ' } > 0 && this.isNotQuoted()) { throw ParseException( @@ -167,6 +175,41 @@ private fun String.validateSymbols(lineNo: Int) { } } +private fun String.getCommentStartIndex(allowEscapedQuotesInLiteralStrings: Boolean): Int { + val isEscapingDisabled = if (allowEscapedQuotesInLiteralStrings) { + // escaping is disabled when the config option is true AND we have a literal string + val firstQuoteLetter = this.firstOrNull { it == '\"' || it == '\'' } + firstQuoteLetter == '\'' + } else { + false + } + + val chars = if (!isEscapingDisabled) { + this.replace("\\\"", "__") + .replace("\\\'", "__") + } else { + this + }.toCharArray() + var currentQuoteChar: Char? = null + + chars.forEachIndexed { idx, symbol -> + // take hash index if it's not enclosed in quotation marks + if (symbol == '#' && currentQuoteChar == null) { + return idx + } + + if (symbol == '\"' || symbol == '\'') { + if (currentQuoteChar == null) { + currentQuoteChar = symbol + } else if (currentQuoteChar == symbol) { + currentQuoteChar = null + } + } + } + + return -1 +} + private fun Char.isLetterOrDigit() = CharRange('A', 'Z').contains(this) || CharRange('a', 'z').contains(this) || CharRange('0', '9').contains(this) diff --git a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt index 735ea30e..8c8820c3 100644 --- a/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt +++ b/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/parsers/TomlParser.kt @@ -54,10 +54,10 @@ public value class TomlParser(private val config: TomlInputConfig) { // comments and empty lines can easily be ignored in the TomlTree, but we cannot filter them out in mutableTomlLines // because we need to calculate and save lineNo if (line.isComment()) { - comments += line.trimComment() + comments += line.trimComment(config.allowEscapedQuotesInLiteralStrings) } else if (!line.isEmptyLine()) { // Parse the inline comment if any - val inlineComment = line.trimComment() + val inlineComment = line.trimComment(config.allowEscapedQuotesInLiteralStrings) if (line.isTableNode()) { if (line.isArrayOfTables()) { 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 9982bf39..30d998eb 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 @@ -83,16 +83,7 @@ public fun String.parseList(lineNo: Int, config: TomlInputConfig): TomlArray = T * @throws ParseException */ public fun String.splitKeyValue(lineNo: Int, config: TomlInputConfig = TomlInputConfig()): Pair { - // finding the index of the last quote, if no quotes are found, then use the length of the string - val closingQuoteIndex = listOf( - this.lastIndexOf("\""), - this.lastIndexOf("\'"), - this.lastIndexOf("\"\"\"") - ).filterNot { it == -1 }.maxOrNull() ?: 0 - - // removing the commented part of the string - // search starts goes from the closingQuoteIndex to the end of the string - val pair = takeBeforeComment(closingQuoteIndex) + val pair = takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) // searching for an equals sign that should be placed main part of the string (not in the comment) val firstEqualsSign = pair.indexOfFirst { it == '=' } 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 96ac8f01..94091d70 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 @@ -51,9 +51,10 @@ public class TomlArrayOfTables( throw ParseException("Invalid Array of Tables provided: $content." + " It has missing closing brackets: ']]'", lineNo) } - // getting the content inside brackets ([a.b] -> a.b) - val sectionFromContent = content.takeBeforeComment(lastIndexOfBrace).trimDoubleBrackets() + val sectionFromContent = content + .takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) + .trimDoubleBrackets() .trim() if (sectionFromContent.isBlank()) { 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 c61e3e8b..86ae86e5 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 @@ -50,9 +50,10 @@ public class TomlTablePrimitive( throw ParseException("Invalid Tables provided: $content." + " It has missing closing bracket: ']'", lineNo) } - // getting the content inside brackets ([a.b] -> a.b) - val sectionFromContent = content.takeBeforeComment(lastIndexOfBrace).trim().trimBrackets() + val sectionFromContent = content + .takeBeforeComment(config.allowEscapedQuotesInLiteralStrings) + .trimBrackets() .trim() if (sectionFromContent.isBlank()) { diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt new file mode 100644 index 00000000..2248fa90 --- /dev/null +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/StringUtilsTest.kt @@ -0,0 +1,50 @@ +package com.akuleshov7.ktoml.parsers + +import kotlin.test.Test +import kotlin.test.assertEquals + +class StringUtilsTest { + + @Test + fun testForTakeBeforeComment() { + var lineWithoutComment = "test_key = \"test_value\" # \" some comment".takeBeforeComment(false) + assertEquals("test_key = \"test_value\"", lineWithoutComment) + + lineWithoutComment = "key = \"\"\"value\"\"\" # \"".takeBeforeComment(false) + assertEquals("key = \"\"\"value\"\"\"", lineWithoutComment) + + lineWithoutComment = "key = 123 # \"\"\"abc".takeBeforeComment(false) + assertEquals("key = 123", lineWithoutComment) + + lineWithoutComment = "key = \"ab\\\"#cdef\"#123".takeBeforeComment(false) + assertEquals("key = \"ab\\\"#cdef\"", lineWithoutComment) + + lineWithoutComment = " \t#123".takeBeforeComment(false) + assertEquals("", lineWithoutComment) + + lineWithoutComment = "key = \"ab\'c\" # ".takeBeforeComment(false) + assertEquals("key = \"ab\'c\"", lineWithoutComment) + + lineWithoutComment = """ + a = 'C:\some\path\' #\abc + """.trimIndent().takeBeforeComment(true) + assertEquals("""a = 'C:\some\path\'""", lineWithoutComment) + } + + @Test + fun testForTrimComment() { + var comment = "a = \"here#hash\" # my comment".trimComment(false) + assertEquals("my comment", comment) + + comment = "a = \"here#\\\"hash\" # my comment".trimComment(false) + assertEquals("my comment", comment) + + comment = " # my comment".trimComment(false) + assertEquals("my comment", comment) + + comment = """ + a = 'C:\some\path\' #\abc + """.trimIndent().trimComment(true) + assertEquals("\\abc", comment) + } +} \ No newline at end of file diff --git a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/ValueParserTest.kt b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/ValueParserTest.kt index e635ee90..c7313c2d 100644 --- a/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/ValueParserTest.kt +++ b/ktoml-core/src/commonTest/kotlin/com/akuleshov7/ktoml/parsers/ValueParserTest.kt @@ -112,6 +112,12 @@ class ValueParserTest { assertEquals("1 = 2", TomlKeyValuePrimitive(pairTest, 0).value.content) } + @Test + fun symbolsAfterComment() { + val keyValue = "test_key = \"test_value\" # \" some comment".splitKeyValue(0) + assertEquals("test_value", TomlKeyValuePrimitive(keyValue, 0).value.content) + } + @Test fun parsingIssueValue() { assertFailsWith { " = false".splitKeyValue(0) }