diff --git a/stdlib-candidate/std-rfc/str/dedent/mod.nu b/stdlib-candidate/std-rfc/str/dedent/mod.nu new file mode 100644 index 0000000..fabc50f --- /dev/null +++ b/stdlib-candidate/std-rfc/str/dedent/mod.nu @@ -0,0 +1,93 @@ +# Removes common indent from a multi-line string based on the number of spaces on the last line. +# +# A.k.a. Unindent +# +# Example - Two leading spaces are removed from all lines: +# +# > let s = " +# Heading +# Indented Line +# Another Indented Line +# +# Another Heading +# " +# > $a | str dedent +# +# Heading +# Indented Line +# Another Indented Line +# +# Another Heading +export def main []: string -> string { + let string = $in + + if ($string | describe) != "string" { + let span = (view files | last) + error make { + msg: 'Requires multi-line string as pipeline input' + label: { + text: "err::pipeline_input" + span: { + start: $span.start + end: $span.end + } + } + } + } + + if ($string !~ '(?ms)^\s*\n') { + return (error make { + msg: 'First line must be empty' + }) + } + + if ($string !~ '(?ms)\n\s*$') { + return (error make { + msg: 'Last line must contain only whitespace indicating the dedent' + }) + } + + # Get number of spaces on the last line + let indent = $string + | str replace -r '(?ms).*\n( *)$' '$1' + | str length + + # Skip the first and last lines + let lines = ( + $string + | str replace -r '(?ms)^[^\n]*\n(.*)\n[^\n]*$' '$1' + # Use `split` instead of `lines`, since `lines` will + # drop legitimate trailing empty lines + | split row "\n" + | enumerate + | rename lineNumber text + ) + + let spaces = ('' | fill -c ' ' -w $indent) + + # Has to be done outside the replacement block or the error + # is converted to text. This is probably a Nushell bug, and + # this code can be recombined with the next iterator when + # the Nushell behavior is fixed. + for line in $lines { + if ($line.text !~ '^\s*$') and ($line.text | str index-of --range 0..($indent) $spaces) == -1 { + error make { + msg: $"Line ($line.lineNumber + 1) must be indented by ($indent) or more spaces." + } + } + } + + $lines + | each {|line| + # Don't operate on lines containing only whitespace + if ($line.text !~ '^\s*$') { + $line.text | str replace $spaces '' + } else { + $line.text + } + } + | to text + # Remove the trailing newline which indicated + # indent level + | str replace -r '(?ms)(.*)\n$' '$1' +} \ No newline at end of file diff --git a/stdlib-candidate/std-rfc/str/mod.nu b/stdlib-candidate/std-rfc/str/mod.nu index fbff9d9..9dbbcb2 100644 --- a/stdlib-candidate/std-rfc/str/mod.nu +++ b/stdlib-candidate/std-rfc/str/mod.nu @@ -1 +1,2 @@ export use xpend.nu * +export use dedent * diff --git a/stdlib-candidate/tests/mod.nu b/stdlib-candidate/tests/mod.nu index 34dc62f..42fc3a4 100644 --- a/stdlib-candidate/tests/mod.nu +++ b/stdlib-candidate/tests/mod.nu @@ -3,3 +3,4 @@ export module record.nu export module str_xpend.nu export module math.nu export module bench.nu +export module str_dedent.nu diff --git a/stdlib-candidate/tests/str_dedent.nu b/stdlib-candidate/tests/str_dedent.nu new file mode 100644 index 0000000..0f6c8ef --- /dev/null +++ b/stdlib-candidate/tests/str_dedent.nu @@ -0,0 +1,128 @@ +use std assert +use ../std-rfc str + +export def "test str dedent" [] { + + # Test 1: + # Should start with "Heading" in the first character position + # Should not end with a line-break + # The blank line has no extra spaces + assert equal ( + do { + let s = " + Heading + + one + two + " + $s | str dedent + } + ) "Heading\n\n one\n two" + + # Test 2: + # Same as #1, but the blank line has leftover whitespace + # indentation (16 spaces) which is left in the result + assert equal ( + do { + let s = " + Heading + + one + two + " + $s | str dedent + } + ) "Heading\n \n one\n two" + + # Test 3: + # Same, but with a single tab character on the "blank" line + assert equal ( + do { + let s = " + Heading +\t + one + two + " + $s | str dedent + } + ) "Heading\n\t\n one\n two" + + # Test 4: + # Ends with line-break + assert equal ( + do { + let s = " + Heading + + one + two + + " + $s | str dedent + } + ) "Heading\n\n one\n two\n" + + # Test 5: + # Identity - Returns the original string sans first and last empty lines + # No other whitespace should be removed + assert equal ( + do { + let s = "\n Identity \n" + $s | str dedent + } + ) " Identity " + + # Test 6: + # Error - Does not contain an empty first line + assert error {|| + let s = "Error" + $s | str dedent + } + + # Test 7: + # Error - Does not contain an empty last line + assert error {|| + let s = " + Error" + $s | str dedent + } + + # Test 8: + # Error - Line 1 does not have enough indentation + assert error {|| + let s = " + Line 1 + Line 2 + " + $s | str dedent + } + + # Test 8: + # Error - Line 2 does not have enough indentation + assert error {|| + let s = " + Line 1 + Line 2 + " + $s | str dedent + } + + # Test 9: + # Error - Line does not have enough indentation + assert error {|| + let s = " + Line + " + $s | str dedent + } + + # Test 10: + # "Hidden" whitespace on the first line is allowed + assert equal ( + do { + let s = " \t \n Identity \n" + $s | str dedent + } + ) " Identity " +} \ No newline at end of file