KT-8708 Function to strip leading whitespace (stripMargin, trimMargin, stripIndent?)

This commit is contained in:
Sergey Mashkov
2015-08-03 19:09:57 +03:00
parent 468919f5d9
commit 7afa40590d
2 changed files with 193 additions and 1 deletions
@@ -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
+113 -1
View File
@@ -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() })
}
}