This repository was archived by the owner on Apr 4, 2024. It is now read-only.
forked from diffplug/selfie
-
Notifications
You must be signed in to change notification settings - Fork 1
This repository was archived by the owner on Apr 4, 2024. It is now read-only.
LiteralString #41
Copy link
Copy link
Closed
Description
From Epic: inline snapshot #8
Implementation:
selfie-python-wip/jvm/selfie-lib/src/commonMain/kotlin/com/diffplug/selfie/guts/Literals.kt
Lines 113 to 348 in a9c17e8
| private const val TRIPLE_QUOTE = "\"\"\"" | |
| private const val KOTLIN_DOLLAR = "\${'\$'}" | |
| private const val KOTLIN_DOLLARQUOTE = "\${'\"'}" | |
| internal object LiteralString : LiteralFormat<String>() { | |
| override fun encode( | |
| value: String, | |
| language: Language, | |
| encodingPolicy: EscapeLeadingWhitespace | |
| ): String = | |
| if (value.indexOf('\n') == -1) | |
| when (language) { | |
| Language.SCALA, // scala only does $ substitution for s" and f" strings | |
| Language.JAVA_PRE15, | |
| Language.GROOVY, | |
| Language.JAVA -> encodeSingleJava(value) | |
| Language.KOTLIN -> encodeSingleJavaWithDollars(value) | |
| } | |
| else | |
| when (language) { | |
| // TODO: support triple-quoted strings in scala | |
| // https://github.com/diffplug/selfie/issues/106 | |
| Language.SCALA, | |
| // TODO: support triple-quoted strings in groovy | |
| // https://github.com/diffplug/selfie/issues/105 | |
| Language.GROOVY, | |
| Language.JAVA_PRE15 -> encodeSingleJava(value) | |
| Language.JAVA -> encodeMultiJava(value, encodingPolicy) | |
| Language.KOTLIN -> encodeMultiKotlin(value, encodingPolicy) | |
| } | |
| override fun parse(str: String, language: Language): String = | |
| if (!str.startsWith(TRIPLE_QUOTE)) | |
| when (language) { | |
| Language.SCALA, | |
| Language.JAVA_PRE15, | |
| Language.JAVA -> parseSingleJava(str) | |
| Language.GROOVY, | |
| Language.KOTLIN -> parseSingleJavaWithDollars(str) | |
| } | |
| else | |
| when (language) { | |
| Language.SCALA -> | |
| throw UnsupportedOperationException( | |
| "Selfie doesn't support triple-quoted strings in Scala, yet - help wanted: https://github.com/diffplug/selfie/issues/106") | |
| Language.GROOVY -> | |
| throw UnsupportedOperationException( | |
| "Selfie doesn't support triple-quoted strings in Groovy, yet - help wanted: https://github.com/diffplug/selfie/issues/105") | |
| Language.JAVA_PRE15, | |
| Language.JAVA -> parseMultiJava(str) | |
| Language.KOTLIN -> parseMultiKotlin(str) | |
| } | |
| fun encodeSingleJava(value: String): String = encodeSingleJavaish(value, false) | |
| fun encodeSingleJavaWithDollars(value: String) = encodeSingleJavaish(value, true) | |
| private fun encodeSingleJavaish(value: String, escapeDollars: Boolean): String { | |
| val source = StringBuilder() | |
| source.append("\"") | |
| for (char in value) { | |
| when (char) { | |
| '\b' -> source.append("\\b") | |
| '\n' -> source.append("\\n") | |
| '\r' -> source.append("\\r") | |
| '\t' -> source.append("\\t") | |
| '\"' -> source.append("\\\"") | |
| '\\' -> source.append("\\\\") | |
| '$' -> if (escapeDollars) source.append(KOTLIN_DOLLAR) else source.append('$') | |
| else -> | |
| if (isControlChar(char)) { | |
| source.append("\\u") | |
| source.append(char.code.toString(16).padStart(4, '0')) | |
| } else { | |
| source.append(char) | |
| } | |
| } | |
| } | |
| source.append("\"") | |
| return source.toString() | |
| } | |
| private fun isControlChar(c: Char): Boolean { | |
| return c in '\u0000'..'\u001F' || c == '\u007F' | |
| } | |
| fun parseSingleJava(sourceWithQuotes: String) = parseSingleJavaish(sourceWithQuotes, false) | |
| fun parseSingleJavaWithDollars(sourceWithQuotes: String) = | |
| parseSingleJavaish(sourceWithQuotes, true) | |
| private fun parseSingleJavaish(sourceWithQuotes: String, removeDollars: Boolean): String { | |
| check(sourceWithQuotes.startsWith('"')) | |
| check(sourceWithQuotes.endsWith('"')) | |
| val source = sourceWithQuotes.substring(1, sourceWithQuotes.length - 1) | |
| val toUnescape = if (removeDollars) inlineDollars(source) else source | |
| return unescapeJava(toUnescape) | |
| } | |
| fun encodeMultiKotlin(arg: String, escapeLeadingWhitespace: EscapeLeadingWhitespace): String { | |
| val escapeDollars = arg.replace("$", KOTLIN_DOLLAR) | |
| val escapeTripleQuotes = | |
| escapeDollars.replace( | |
| TRIPLE_QUOTE, "$KOTLIN_DOLLARQUOTE$KOTLIN_DOLLARQUOTE$KOTLIN_DOLLARQUOTE") | |
| val protectWhitespace = | |
| escapeTripleQuotes.lines().joinToString("\n") { line -> | |
| val protectTrailingWhitespace = | |
| if (line.endsWith(" ")) { | |
| line.dropLast(1) + "\${' '}" | |
| } else if (line.endsWith("\t")) { | |
| line.dropLast(1) + "\${'\\t'}" | |
| } else line | |
| escapeLeadingWhitespace.escapeLine(protectTrailingWhitespace, "\${' '}", "\${'\\t'}") | |
| } | |
| return "$TRIPLE_QUOTE$protectWhitespace$TRIPLE_QUOTE" | |
| } | |
| fun encodeMultiJava(arg: String, escapeLeadingWhitespace: EscapeLeadingWhitespace): String { | |
| val escapeBackslashes = arg.replace("\\", "\\\\") | |
| val escapeTripleQuotes = escapeBackslashes.replace(TRIPLE_QUOTE, "\\\"\\\"\\\"") | |
| var protectWhitespace = | |
| escapeTripleQuotes.lines().joinToString("\n") { line -> | |
| val protectTrailingWhitespace = | |
| if (line.endsWith(" ")) { | |
| line.dropLast(1) + "\\s" | |
| } else if (line.endsWith("\t")) { | |
| line.dropLast(1) + "\\t" | |
| } else line | |
| escapeLeadingWhitespace.escapeLine(protectTrailingWhitespace, "\\s", "\\t") | |
| } | |
| val commonPrefix = | |
| protectWhitespace | |
| .lines() | |
| .mapNotNull { line -> | |
| if (line.isNotBlank()) line.takeWhile { it.isWhitespace() } else null | |
| } | |
| .minOrNull() ?: "" | |
| if (commonPrefix.isNotEmpty()) { | |
| val lines = protectWhitespace.lines() | |
| val last = lines.last() | |
| protectWhitespace = | |
| lines.joinToString("\n") { line -> | |
| if (line === last) { | |
| if (line.startsWith(" ")) "\\s${line.drop(1)}" | |
| else if (line.startsWith("\t")) "\\t${line.drop(1)}" | |
| else | |
| throw UnsupportedOperationException( | |
| "How did it end up with a common whitespace prefix?") | |
| } else line | |
| } | |
| } | |
| return "$TRIPLE_QUOTE\n$protectWhitespace$TRIPLE_QUOTE" | |
| } | |
| private val charLiteralRegex = """\$\{'(\\?.)'\}""".toRegex() | |
| private fun inlineDollars(source: String): String { | |
| if (source.indexOf('$') == -1) { | |
| return source | |
| } | |
| return charLiteralRegex.replace(source) { matchResult -> | |
| val charLiteral = matchResult.groupValues[1] | |
| when { | |
| charLiteral.length == 1 -> charLiteral | |
| charLiteral.length == 2 && charLiteral[0] == '\\' -> | |
| when (charLiteral[1]) { | |
| 't' -> "\t" | |
| 'b' -> "\b" | |
| 'n' -> "\n" | |
| 'r' -> "\r" | |
| '\'' -> "'" | |
| '\\' -> "\\" | |
| '$' -> "$" | |
| else -> charLiteral | |
| } | |
| else -> throw IllegalArgumentException("Unknown character literal $charLiteral") | |
| } | |
| } | |
| } | |
| private fun unescapeJava(source: String): String { | |
| val firstEscape = source.indexOf('\\') | |
| if (firstEscape == -1) { | |
| return source | |
| } | |
| val value = StringBuilder() | |
| value.append(source.substring(0, firstEscape)) | |
| var i = firstEscape | |
| while (i < source.length) { | |
| var c = source[i] | |
| if (c == '\\') { | |
| i++ | |
| c = source[i] | |
| when (c) { | |
| '\"' -> value.append('\"') | |
| '\\' -> value.append('\\') | |
| 'b' -> value.append('\b') | |
| 'f' -> value.append('\u000c') | |
| 'n' -> value.append('\n') | |
| 'r' -> value.append('\r') | |
| 's' -> value.append(' ') | |
| 't' -> value.append('\t') | |
| 'u' -> { | |
| val code = source.substring(i + 1, i + 5).toInt(16) | |
| value.append(code.toChar()) | |
| i += 4 | |
| } | |
| else -> throw IllegalArgumentException("Unknown escape sequence $c") | |
| } | |
| } else { | |
| value.append(c) | |
| } | |
| i++ | |
| } | |
| return value.toString() | |
| } | |
| fun parseMultiJava(sourceWithQuotes: String): String { | |
| check(sourceWithQuotes.startsWith("$TRIPLE_QUOTE\n")) | |
| check(sourceWithQuotes.endsWith(TRIPLE_QUOTE)) | |
| val source = | |
| sourceWithQuotes.substring( | |
| TRIPLE_QUOTE.length + 1, sourceWithQuotes.length - TRIPLE_QUOTE.length) | |
| val lines = source.lines() | |
| val commonPrefix = | |
| lines | |
| .mapNotNull { line -> | |
| if (line.isNotBlank()) line.takeWhile { it.isWhitespace() } else null | |
| } | |
| .minOrNull() ?: "" | |
| return lines.joinToString("\n") { line -> | |
| if (line.isBlank()) { | |
| "" | |
| } else { | |
| val removedPrefix = if (commonPrefix.isEmpty()) line else line.removePrefix(commonPrefix) | |
| val removeTrailingWhitespace = removedPrefix.trimEnd() | |
| val handleEscapeSequences = unescapeJava(removeTrailingWhitespace) | |
| handleEscapeSequences | |
| } | |
| } | |
| } | |
| fun parseMultiKotlin(sourceWithQuotes: String): String { | |
| check(sourceWithQuotes.startsWith(TRIPLE_QUOTE)) | |
| check(sourceWithQuotes.endsWith(TRIPLE_QUOTE)) | |
| val source = | |
| sourceWithQuotes.substring( | |
| TRIPLE_QUOTE.length, sourceWithQuotes.length - TRIPLE_QUOTE.length) | |
| return inlineDollars(source) | |
| } | |
| } |
Test:
selfie-python-wip/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt
Lines 21 to 102 in a9c17e8
| class LiteralStringTest { | |
| @Test | |
| fun encodeSingleJava() { | |
| encodeSingleJava("1", "'1'") | |
| encodeSingleJava("\\", "'\\\\'") | |
| encodeSingleJava("1\n\tABC", "'1\\n\\tABC'") | |
| } | |
| private fun encodeSingleJava(value: String, expected: String) { | |
| val actual = LiteralString.encodeSingleJava(value) | |
| actual shouldBe expected.replace("'", "\"") | |
| } | |
| @Test | |
| fun encodeSingleJavaWithDollars() { | |
| encodeSingleJavaWithDollars("1", "`1`") | |
| encodeSingleJavaWithDollars("\\", "`\\\\`") | |
| encodeSingleJavaWithDollars("$", "`s{'s'}`".replace('s', '$')) | |
| encodeSingleJavaWithDollars("1\n\tABC", "`1\\n\\tABC`") | |
| } | |
| private fun encodeSingleJavaWithDollars(value: String, expected: String) { | |
| val actual = LiteralString.encodeSingleJavaWithDollars(value) | |
| actual shouldBe expected.replace("`", "\"") | |
| } | |
| @Test | |
| fun encodeMultiJava() { | |
| encodeMultiJava("1", "'''\n1'''") | |
| encodeMultiJava("\\", "'''\n\\\\'''") | |
| encodeMultiJava(" leading\ntrailing ", "'''\n" + "\\s leading\n" + "trailing \\s'''") | |
| } | |
| private fun encodeMultiJava(value: String, expected: String) { | |
| val actual = LiteralString.encodeMultiJava(value, EscapeLeadingWhitespace.ALWAYS) | |
| actual shouldBe expected.replace("'", "\"") | |
| } | |
| private val KOTLIN_DOLLAR = "s{'s'}".replace('s', '$') | |
| @Test | |
| fun encodeMultiKotlin() { | |
| encodeMultiKotlin("1", "```1```") | |
| encodeMultiKotlin("$", "```$KOTLIN_DOLLAR```") | |
| } | |
| private fun encodeMultiKotlin(value: String, expected: String) { | |
| val actual = LiteralString.encodeMultiKotlin(value, EscapeLeadingWhitespace.ALWAYS) | |
| actual shouldBe expected.replace("`", "\"") | |
| } | |
| @Test | |
| fun parseSingleJava() { | |
| parseSingleJava("1", "1") | |
| parseSingleJava("\\\\", "\\") | |
| parseSingleJava("1\\n\\tABC", "1\n\tABC") | |
| } | |
| private fun parseSingleJava(value: String, expected: String) { | |
| val actual = LiteralString.parseSingleJava("\"${value.replace("'", "\"")}\"") | |
| actual shouldBe expected | |
| } | |
| @Test | |
| fun parseMultiJava() { | |
| parseMultiJava("\n123\nabc", "123\nabc") | |
| parseMultiJava("\n 123\n abc", "123\nabc") | |
| parseMultiJava("\n 123 \n abc\t", "123\nabc") | |
| parseMultiJava("\n 123 \n abc\t", "123\nabc") | |
| parseMultiJava("\n 123 \\s\n abc\t\\s", "123 \nabc\t ") | |
| } | |
| private fun parseMultiJava(value: String, expected: String) { | |
| val actual = LiteralString.parseMultiJava("\"\"\"${value.replace("'", "\"")}\"\"\"") | |
| actual shouldBe expected | |
| } | |
| @Test | |
| fun parseSingleJavaWithDollars() { | |
| parseSingleJavaWithDollars("1", "1") | |
| parseSingleJavaWithDollars("\\\\", "\\") | |
| parseSingleJavaWithDollars("s{'s'}".replace('s', '$'), "$") | |
| parseSingleJavaWithDollars("1\\n\\tABC", "1\n\tABC") | |
| } | |
| private fun parseSingleJavaWithDollars(value: String, expected: String) { | |
| val actual = LiteralString.parseSingleJavaWithDollars("\"${value}\"") | |
| actual shouldBe expected | |
| } | |
| } |
Metadata
Metadata
Assignees
Labels
No labels