From 4a16087a2128df5ec3cbd702f6f6ec64ba7a1b82 Mon Sep 17 00:00:00 2001 From: Ilya Matveev Date: Tue, 25 Apr 2017 15:38:57 +0700 Subject: [PATCH] stdlib: Add indentation extensions for strings --- .../stdlib/text/StringTest/trimIndent.kt | 7 +- runtime/src/main/kotlin/kotlin/text/Indent.kt | 108 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 runtime/src/main/kotlin/kotlin/text/Indent.kt diff --git a/backend.native/tests/external/stdlib/text/StringTest/trimIndent.kt b/backend.native/tests/external/stdlib/text/StringTest/trimIndent.kt index 240b38bcda8..fd09f9f799b 100644 --- a/backend.native/tests/external/stdlib/text/StringTest/trimIndent.kt +++ b/backend.native/tests/external/stdlib/text/StringTest/trimIndent.kt @@ -60,7 +60,12 @@ ${" "} """.trimIndent() assertEquals(23, deindented.lines().size) - val indents = deindented.lines().map { "^\\s*".toRegex().find(it)!!.value.length } + val indents = deindented.lines().map { + var i = 0 + while (i < it.length && it[i].isWhitespace()) + i++ + return@map i + } assertEquals(0, indents.min()) assertEquals(42, indents.max()) assertEquals(1, deindented.lines().count { it.isEmpty() }) diff --git a/runtime/src/main/kotlin/kotlin/text/Indent.kt b/runtime/src/main/kotlin/kotlin/text/Indent.kt new file mode 100644 index 00000000000..743cc0998cd --- /dev/null +++ b/runtime/src/main/kotlin/kotlin/text/Indent.kt @@ -0,0 +1,108 @@ +package kotlin.text + +/** + * Trims leading whitespace characters followed by [marginPrefix] from every line of a source string and removes + * the first and the last lines if they are blank (notice difference blank vs empty). + * + * Doesn't affect a line if it doesn't contain [marginPrefix] except the first and the last blank lines. + * + * Doesn't preserve the original line endings. + * + * @param marginPrefix non-blank string, which is used as a margin delimiter. Default is `|` (pipe character). + * + * @sample samples.text.Strings.trimMargin + * @see trimIndent + * @see kotlin.text.isWhitespace + */ +public fun String.trimMargin(marginPrefix: String = "|"): String = + replaceIndentByMargin("", marginPrefix) + +/** + * Detects indent by [marginPrefix] as it does [trimMargin] and replace it with [newIndent]. + * + * @param marginPrefix non-blank string, which is used as a margin delimiter. Default is `|` (pipe character). + */ +public fun String.replaceIndentByMargin(newIndent: String = "", marginPrefix: String = "|"): String { + require(marginPrefix.isNotBlank()) { "marginPrefix must be non-blank string." } + val lines = lines() + + return lines.reindent(length + newIndent.length * lines.size, getIndentFunction(newIndent), { line -> + val firstNonWhitespaceIndex = line.indexOfFirst { !it.isWhitespace() } + + when { + firstNonWhitespaceIndex == -1 -> null + line.startsWith(marginPrefix, firstNonWhitespaceIndex) -> line.substring(firstNonWhitespaceIndex + marginPrefix.length) + else -> null + } + }) +} + +/** + * Detects a common minimal indent of all the input lines, removes it from every line and also removes the first and the last + * lines if they are blank (notice difference blank vs empty). + * + * Note that blank lines do not affect the detected indent level. + * + * In case if there are non-blank lines with no leading whitespace characters (no indent at all) then the + * common indent is 0, and therefore this function doesn't change the indentation. + * + * Doesn't preserve the original line endings. + * + * @sample samples.text.Strings.trimIndent + * @see trimMargin + * @see kotlin.text.isBlank + */ +public fun String.trimIndent(): String = replaceIndent("") + +/** + * Detects a common minimal indent like it does [trimIndent] and replaces it with the specified [newIndent]. + */ +public fun String.replaceIndent(newIndent: String = ""): String { + val lines = lines() + + val minCommonIndent = lines + .filter(String::isNotBlank) + .map(String::indentWidth) + .min() ?: 0 + + return lines.reindent(length + newIndent.length * lines.size, getIndentFunction(newIndent), { line -> line.drop(minCommonIndent) }) +} + +/** + * Prepends [indent] to every line of the original string. + * + * Doesn't preserve the original line endings. + */ +public fun String.prependIndent(indent: String = " "): String = + lineSequence() + .map { + when { + it.isBlank() -> { + when { + it.length < indent.length -> indent + else -> it + } + } + else -> indent + it + } + } + .joinToString("\n") + +private fun String.indentWidth(): Int = indexOfFirst { !it.isWhitespace() }.let { if (it == -1) length else it } + +private fun getIndentFunction(indent: String) = when { + indent.isEmpty() -> { line: String -> line } + else -> { line: String -> indent + line } +} + +private inline fun List.reindent(resultSizeEstimate: Int, indentAddFunction: (String) -> String, indentCutFunction: (String) -> String?): String { + val lastIndex = lastIndex + return mapIndexedNotNull { index, value -> + if ((index == 0 || index == lastIndex) && value.isBlank()) + null + else + indentCutFunction(value)?.let(indentAddFunction) ?: value + } + .joinTo(StringBuilder(resultSizeEstimate), "\n") + .toString() +}