diff --git a/NAMESPACE b/NAMESPACE index acd9602a..90eec637 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,6 +20,7 @@ export(regex) export(str_c) export(str_conv) export(str_count) +export(str_dedent) export(str_detect) export(str_dup) export(str_ends) diff --git a/R/remove.R b/R/remove.R index a943cede..567965e6 100644 --- a/R/remove.R +++ b/R/remove.R @@ -19,3 +19,65 @@ str_remove <- function(string, pattern) { str_remove_all <- function(string, pattern) { str_replace_all(string, pattern, "") } + + +#' Remove common leading indentation from strings +#' +#' @description +#' `str_dedent()` is designed to make it possible to correctly indent multiline +#' strings inside of function calls, while generating the desired amount of +#' whitespace in the output. +#' +#' It does this by removing the common leading indentation from each line +#' (ignoring lines only containing whitespace), and removing the first line, +#' if it only contains whitespace. +#' +#' It is inspired by Python's +#' [`textwrap.dedent()`](https://docs.python.org/3/library/textwrap.html#textwrap.dedent). +#' +#' @inheritParams str_detect +#' @return A character vector the same length as `string` +#' @export +#' @examples +#' str_dedent(" +#' Hello +#' World +#' ") +#' +#' f <- function() { +#' str_dedent(" +#' Line 1 +#' Line 2 +#' Line 3 +#' ") +#' } +#' cat(str_dedent(f())) +str_dedent <- function(string) { + check_character(string) + + lines <- str_split(string, fixed("\n")) + map_chr(lines, str_dedent_1) +} + +str_dedent_1 <- function(lines, trim_empty_ends = TRUE) { + if (length(lines) <= 1) { + return(lines) + } + + ws <- str_length(str_extract(lines, "^ *")) + only_ws <- ws == str_length(lines) + + # Drop the first line if it's completely blank + if (only_ws[1]) { + lines <- lines[-1] + ws <- ws[-1] + only_ws <- only_ws[-1] + } + + if (all(only_ws)) { + dedented <- lines + } else { + dedented <- str_sub(lines, start = min(ws[!only_ws]) + 1) + } + paste0(dedented, collapse = "\n") +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 77b0971b..514985bb 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -21,6 +21,7 @@ reference: - subtitle: String contents: - str_count + - str_dedent - str_detect - str_escape - str_extract diff --git a/man/str_dedent.Rd b/man/str_dedent.Rd new file mode 100644 index 00000000..94aef998 --- /dev/null +++ b/man/str_dedent.Rd @@ -0,0 +1,42 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/remove.R +\name{str_dedent} +\alias{str_dedent} +\title{Remove common leading indentation from strings} +\usage{ +str_dedent(string) +} +\arguments{ +\item{string}{Input vector. Either a character vector, or something +coercible to one.} +} +\value{ +A character vector the same length as \code{string} +} +\description{ +\code{str_dedent()} is designed to make it possible to correctly indent multiline +strings inside of function calls, while generating the desired amount of +whitespace in the output. + +It does this by removing the common leading indentation from each line +(ignoring lines only containing whitespace), and removing the first line, +if it only contains whitespace. + +It is inspired by Python's +\href{https://docs.python.org/3/library/textwrap.html#textwrap.dedent}{\code{textwrap.dedent()}}. +} +\examples{ +str_dedent(" + Hello + World +") + +f <- function() { + str_dedent(" + Line 1 + Line 2 + Line 3 + ") +} +cat(str_dedent(f())) +} diff --git a/tests/testthat/test-remove.R b/tests/testthat/test-remove.R index f8ea1b7c..00c0fe6a 100644 --- a/tests/testthat/test-remove.R +++ b/tests/testthat/test-remove.R @@ -7,3 +7,42 @@ test_that("str_remove() preserves names", { x <- c(C = "3", B = "2", A = "1") expect_equal(names(str_remove(x, "[0-9]")), names(x)) }) + +test_that("strips common ws", { + expect_equal(str_dedent(" Hello\n World"), "Hello\n World") + expect_equal(str_dedent(" Hello\n World"), " Hello\nWorld") +}) + +test_that("strips initial empty line", { + expect_equal(str_dedent("\n Hello\n World"), "Hello\n World") + + expect_equal(str_dedent("\n"), "") + expect_equal(str_dedent("\n\n"), "\n") +}) + +test_that("preserves final newline", { + expect_equal(str_dedent(" Hello\n World"), "Hello\nWorld") + expect_equal(str_dedent(" Hello\n World\n"), "Hello\nWorld\n") + + # fmt: skip + expect_equal( + str_dedent(" + Hello + World" + ), + "Hello\nWorld") + # fmt: skip + expect_equal( + str_dedent(" + Hello + World + "), + "Hello\nWorld\n") +}) + +test_that("special cases are idempotent", { + expect_equal(str_dedent(character()), character()) + expect_equal(str_dedent(""), "") + expect_equal(str_dedent("one line"), "one line") + expect_equal(NA_character_, NA_character_) +})