|
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) |
|
} |
|
} |
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
Test:
selfie-python-wip/jvm/selfie-lib/src/commonTest/kotlin/com/diffplug/selfie/guts/LiteralStringTest.kt
Lines 21 to 102 in a9c17e8