KT-8708 Function to strip leading whitespace (stripMargin, trimMargin, stripIndent?)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package kotlin
|
||||
|
||||
import java.lang
|
||||
import java.util.NoSuchElementException
|
||||
import kotlin.platform.platformName
|
||||
import kotlin.text.MatchResult
|
||||
@@ -791,6 +792,85 @@ public fun String.contains(seq: CharSequence, ignoreCase: Boolean = false): Bool
|
||||
public fun String.contains(char: Char, ignoreCase: Boolean = false): Boolean =
|
||||
indexOf(char, ignoreCase = ignoreCase) >= 0
|
||||
|
||||
private fun getIndentFunction(indent: String) = when {
|
||||
indent.isEmpty() -> { line: String -> line }
|
||||
else -> { line: String -> "$indent$line" }
|
||||
}
|
||||
|
||||
private inline fun List<String>.reindent(length: Int, indentAddFunction: (String) -> String, indentCutFunction: (String) -> String) = lastIndex.let { lastLineIndex ->
|
||||
withIndex()
|
||||
.dropWhile { it.index == 0 && it.value.isBlank() }
|
||||
.takeWhile { it.index != lastLineIndex || it.value.isNotBlank() }
|
||||
.map { indentCutFunction(it.value) }
|
||||
.map(indentAddFunction)
|
||||
.joinTo(StringBuilder(length), "\n")
|
||||
.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims leading whitespace characters followed by [marginPrefix] from every line of a source string and removes
|
||||
* first and last lines if they are blank (notice difference blank vs empty).
|
||||
* Do not preserves original line endings
|
||||
*
|
||||
* Example
|
||||
* ```kotlin
|
||||
* assertEquals("ABC\n123\n456", """ABC
|
||||
* |123
|
||||
* |456""".trimMargin())
|
||||
* ```
|
||||
*
|
||||
* @param marginPrefix characters to be used as a margin delimiter. Default is `|` (pipe character)
|
||||
* @return the trimmed String
|
||||
* @see kotlin.isWhitespace
|
||||
* @since M13
|
||||
*/
|
||||
public fun String.trimMargin(marginPrefix: String = "|", newIndent: String = ""): String {
|
||||
require(marginPrefix.isNotBlank()) { "marginPrefix should be non blank string but it is '$marginPrefix'" }
|
||||
|
||||
return lines().reindent(length(), getIndentFunction(newIndent)) { line ->
|
||||
val firstNonWhitespaceIndex = line.indexOfFirst { !it.isWhitespace() }
|
||||
|
||||
when {
|
||||
firstNonWhitespaceIndex == -1 -> line
|
||||
line.startsWith(marginPrefix, firstNonWhitespaceIndex) -> line.substring(firstNonWhitespaceIndex + marginPrefix.length())
|
||||
else -> line
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects a common minimal indent of all the input lines, removes it from every line and also removes first and last
|
||||
* lines if they are blank (notice difference blank vs empty).
|
||||
* Note that blank lines do not affect detected indent level.
|
||||
* Please keep in mind that if there are non-blank lines with no leading whitespace characters (no indent at all) then the
|
||||
* common indent is 0 so this function may do nothing so it is recommended to keep first line empty (will be dropped).
|
||||
*
|
||||
* Do not preserves original line endings
|
||||
*
|
||||
* Example
|
||||
* ```kotlin
|
||||
* assertEquals("ABC\n123\n456", """
|
||||
* ABC
|
||||
* 123
|
||||
* 456""".trimMargin())
|
||||
* ```
|
||||
*
|
||||
* @return deindented String
|
||||
* @see kotlin.isBlank
|
||||
* @since M13
|
||||
*/
|
||||
public fun String.trimIndent(newIndent: String = ""): String {
|
||||
val lines = lines()
|
||||
|
||||
val minCommonIndent = lines
|
||||
.filter { it.isNotBlank() }
|
||||
.map { it.indentWidth() }
|
||||
.min() ?: 0
|
||||
|
||||
return lines.reindent(length(), getIndentFunction(newIndent)) { line -> line.drop(minCommonIndent) }
|
||||
}
|
||||
|
||||
private fun String.indentWidth(): Int = indexOfFirst { !it.isWhitespace() }.let { if (it == -1) length() else it }
|
||||
|
||||
// rangesDelimitedBy
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package test.text
|
||||
|
||||
import kotlin.test.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fails
|
||||
import org.junit.Test as test
|
||||
|
||||
// could not be local inside isEmptyAndBlank, because non-toplevel declarations is not yet supported for JS
|
||||
@@ -555,5 +558,114 @@ class StringTest {
|
||||
assertEquals("-test", "test".replaceFirst("", "-"))
|
||||
}
|
||||
|
||||
test fun trimMargin() {
|
||||
// WARNING
|
||||
// DO NOT REFORMAT AS TESTS MAY FAIL DUE TO INDENTATION CHANGE
|
||||
|
||||
assertEquals("ABC\n123\n456", """ABC
|
||||
|123
|
||||
|456""".trimMargin())
|
||||
|
||||
assertEquals(" ABC\n 123\n 456", """ABC
|
||||
|123
|
||||
|456""".trimMargin(newIndent = " "))
|
||||
|
||||
assertEquals("ABC \n123\n456", """ABC${" "}
|
||||
|123
|
||||
|456""".trimMargin())
|
||||
|
||||
assertEquals(" ABC\n123\n456", """ ABC
|
||||
>>123
|
||||
${"\t"}>>456""".trimMargin(">>"))
|
||||
|
||||
assertEquals("", "".trimMargin())
|
||||
|
||||
assertEquals("", """
|
||||
""".trimMargin())
|
||||
|
||||
assertEquals("", """
|
||||
|""".trimMargin())
|
||||
|
||||
assertEquals("", """
|
||||
|
|
||||
""".trimMargin())
|
||||
|
||||
assertEquals(" a", """
|
||||
| a
|
||||
""".trimMargin())
|
||||
|
||||
assertEquals(" a", """
|
||||
| a""".trimMargin())
|
||||
|
||||
assertEquals(" a", """ | a
|
||||
""".trimMargin())
|
||||
|
||||
assertEquals(" a", """ | a""".trimMargin())
|
||||
|
||||
assertEquals("\u0000|ABC", "${"\u0000"}|ABC".trimMargin())
|
||||
}
|
||||
|
||||
test fun trimIndent() {
|
||||
// WARNING
|
||||
// DO NOT REFORMAT AS TESTS MAY FAIL DUE TO INDENTATION CHANGE
|
||||
|
||||
assertEquals("123", """
|
||||
123
|
||||
""".trimIndent())
|
||||
|
||||
assertEquals("123\n 456", """
|
||||
123
|
||||
456
|
||||
""".trimIndent())
|
||||
|
||||
assertEquals(" 123\n456", """
|
||||
123
|
||||
456
|
||||
""".trimIndent())
|
||||
|
||||
assertEquals(" 123\n 456", """
|
||||
123
|
||||
456
|
||||
""".trimIndent(newIndent = " "))
|
||||
|
||||
assertEquals(" 123\n456", """
|
||||
123
|
||||
456""".trimIndent())
|
||||
|
||||
assertEquals(" ", """
|
||||
${" "}
|
||||
""".trimIndent())
|
||||
|
||||
val deindented = """
|
||||
,.
|
||||
,. _ oo. `88P
|
||||
]88b ,o. d88. ]88b '
|
||||
888 _ Y888o888 d88P _ _
|
||||
888 ,888 `Y88888o_ ,888 d88b d88._____
|
||||
888,888P ,oooooo. ;888888b.]88P 888' d888888888p
|
||||
888888P d88888888. J88b'YPP ]88b ,888 d888P'''888.
|
||||
8888P' ]88P `888 d88[ d88P ]88b 888' Y88b
|
||||
8888p ]88b 888 888 d88[ 888 888. `888
|
||||
,88888b 888[ 888 888. d88[ 888. Y88b Y88[
|
||||
d88PY88b `888L,d88P Y88b Y88b ]88b `888 888'
|
||||
888 Y88b Y88888P 888. 888. 888. Y88b `88P
|
||||
d88P 888 `'P' Y888. `888. `88P `Y8P '
|
||||
Y8P' ' `YP Y8P' '
|
||||
|
||||
____ dXp _ _ _________
|
||||
ddXXXXXp XXP ,XX dXb Yo.XXXXXX ,oooooo.
|
||||
X'L_oXXP XX' XX[ dXb dXb YPPPPXXX'
|
||||
XYXXXXX ]XX dXb dXb dX8Xooooo dXXP
|
||||
XXb`YYXXo. YXXo_ dXP dXP YXb'''''' ,XXP'
|
||||
`XX `YYXb `YXXXXP XX[ ]XX ,XX'
|
||||
YXb YXb `'' XXXXooL `XX._____ `XXXXXXXXooooo.
|
||||
`XP ' '''''' YPXXXXXX' ''''''`''YPPP
|
||||
""".trimIndent()
|
||||
|
||||
assertEquals(23, deindented.lines().size())
|
||||
val indents = deindented.lines().map { "^\\s*".toRegex().match(it)!!.value.length() }
|
||||
assertEquals(0, indents.min())
|
||||
assertEquals(42, indents.max())
|
||||
assertEquals(1, deindented.lines().count { it.isEmpty() })
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user