diff --git a/std/string.d b/std/string.d index d4e2a1715bb..7c9b306f92c 100644 --- a/std/string.d +++ b/std/string.d @@ -60,7 +60,7 @@ module std.string; import core.exception : onRangeError; import core.vararg, core.stdc.stdlib, core.stdc.string, - std.ascii, std.conv, std.exception, std.format, std.functional, + std.algorithm, std.ascii, std.conv, std.exception, std.format, std.functional, std.metastrings, std.range, std.regex, std.traits, std.typetuple, std.uni, std.utf; @@ -3833,6 +3833,217 @@ unittest assert(wrap("u u") == "u u\n"); } +/****************************************** + * Removes indentation from a multi-line string or an array of single-line strings. + * + * This uniformly outdents the text as much as possible. + * Whitespace-only lines are always converted to blank lines. + * + * A StringException will be thrown if inconsistent indentation prevents + * the input from being outdented. + * + * Works at compile-time. + * + * Example: + * --- + * writeln(q{ + * import std.stdio; + * void main() { + * writeln("Hello"); + * } + * }.outdent()); + * --- + * + * Output: + * --- + * + * import std.stdio; + * void main() { + * writeln("Hello"); + * } + * + * --- + * + */ + +S outdent(S)(S str) if(isSomeString!S) +{ + return str.splitLines(KeepTerminator.yes).outdent().join(); +} + +/// ditto +S[] outdent(S)(S[] lines) if(isSomeString!S) +{ + if (lines.empty) + { + return null; + } + + static S leadingWhiteOf(S str) + { + return str[ 0 .. $-find!(not!(std.uni.isWhite))(str).length ]; + } + + S shortestIndent; + foreach (i, line; lines) + { + auto stripped = __ctfe? line.ctfe_strip() : line.strip(); + + if (stripped.empty) + { + lines[i] = line[line.chomp().length..$]; + } + else + { + auto indent = leadingWhiteOf(line); + + // Comparing number of code units instead of code points is OK here + // because this function throws upon inconsistent indentation. + if (shortestIndent is null || indent.length < shortestIndent.length) + { + if (indent.empty) return lines; + shortestIndent = indent; + } + } + } + + foreach (i; 0..lines.length) + { + auto stripped = __ctfe? lines[i].ctfe_strip() : lines[i].strip(); + if (stripped.empty) + { + // Do nothing + } + else if (lines[i].startsWith(shortestIndent)) + { + lines[i] = lines[i][shortestIndent.length..$]; + } + else + { + if (__ctfe) assert(false, "outdent: Inconsistent indentation"); + else throw new StringException("outdent: Inconsistent indentation"); + } + } + + return lines; +} + +// TODO: Remove this and use std.string.strip when retro() becomes ctfe-able. +private S ctfe_strip(S)(S str) if(isSomeString!(Unqual!S)) +{ + return str.stripLeft().ctfe_stripRight(); +} + +// TODO: Remove this and use std.string.strip when retro() becomes ctfe-able. +private S ctfe_stripRight(S)(S str) if(isSomeString!(Unqual!S)) +{ + size_t endIndex = 0; + size_t prevIndex = str.length; + + foreach_reverse (i, dchar ch; str) + { + if (!std.uni.isWhite(ch)) + { + endIndex = prevIndex; + break; + } + prevIndex = i; + } + + return str[0..endIndex]; +} + +version(unittest) +{ + template outdent_testStr(S) + { + enum S outdent_testStr = +" + \t\tX + \t\U00010143X + \t\t + + \t\t\tX +\t "; + } + + template outdent_expected(S) + { + enum S outdent_expected = +" +\tX +\U00010143X + + +\t\tX +"; + } +} + +unittest +{ + debug(string) printf("string.outdent.unittest\n"); + + static assert(ctfe_strip(" \tHi \r\n") == "Hi"); + static assert(ctfe_strip(" \tHi©\u2028 \r\n") == "Hi©"); + static assert(ctfe_strip("Hi") == "Hi"); + static assert(ctfe_strip(" \t \r\n") == ""); + static assert(ctfe_strip("") == ""); + + foreach (S; TypeTuple!(string, wstring, dstring)) + { + enum S blank = ""; + assert(blank.outdent() == blank); + static assert(blank.outdent() == blank); + + enum S testStr1 = " \n \t\n "; + enum S expected1 = "\n\n"; + assert(testStr1.outdent() == expected1); + static assert(testStr1.outdent() == expected1); + + assert(testStr1[0..$-1].outdent() == expected1); + static assert(testStr1[0..$-1].outdent() == expected1); + + enum S testStr2 = "a\n \t\nb"; + assert(testStr2.outdent() == testStr2); + static assert(testStr2.outdent() == testStr2); + + enum S testStr3 = +" + \t\tX + \t\U00010143X + \t\t + + \t\t\tX +\t "; + + enum S expected3 = +" +\tX +\U00010143X + + +\t\tX +"; + assert(testStr3.outdent() == expected3); + static assert(testStr3.outdent() == expected3); + + enum testStr4 = " X\r X\n X\r\n X\u2028 X\u2029 X"; + enum expected4 = "X\rX\nX\r\nX\u2028X\u2029X"; + assert(testStr4.outdent() == expected4); + static assert(testStr4.outdent() == expected4); + + enum testStr5 = testStr4[0..$-1]; + enum expected5 = expected4[0..$-1]; + assert(testStr5.outdent() == expected5); + static assert(testStr5.outdent() == expected5); + + enum testStr6 = " \r \n \r\n \u2028 \u2029"; + enum expected6 = "\r\n\r\n\u2028\u2029"; + assert(testStr6.outdent() == expected6); + static assert(testStr6.outdent() == expected6); + } +} private template softDeprec(string vers, string date, string oldFunc, string newFunc) {