using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CommandLine.Infrastructure;
namespace CommandLine.Text
{
///
/// A utility class to word-wrap and indent blocks of text
///
public class TextWrapper
{
private string[] lines;
public TextWrapper(string input)
{
//start by splitting at newlines and then reinserting the newline as a separate word
//Note that on the input side, we can't assume the line-break style at run time so we have to
//be able to handle both. We can't use Environment.NewLine because that changes at
//_runtime_ and may not match the line-break style that was compiled in
lines = input
.Replace("\r","")
.Split(new[] {'\n'}, StringSplitOptions.None);
}
///
/// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation
///
/// The number of characters we can use for text
///
/// This method attempts to wrap text without breaking words
/// For example, if columnWidth is 10 , the input
/// "a string for wrapping 01234567890123"
/// would return
/// "a string
/// "for
/// "wrapping
/// "0123456789
/// "0123"
///
/// this
public TextWrapper WordWrap(int columnWidth)
{
//ensure we always use at least 1 column even if the client has told us there's no space available
columnWidth = Math.Max(1, columnWidth);
lines= lines
.SelectMany(line => WordWrapLine(line, columnWidth))
.ToArray();
return this;
}
///
/// Indent all lines in the TextWrapper by the desired number of spaces
///
/// The number of spaces to indent by
/// this
public TextWrapper Indent(int numberOfSpaces)
{
lines = lines
.Select(line => numberOfSpaces.Spaces() + line)
.ToArray();
return this;
}
///
/// Returns the current state of the TextWrapper as a string
///
///
public string ToText()
{
//return the whole thing as a single string
return string.Join(Environment.NewLine,lines);
}
///
/// Convenience method to wraps and indent a string in a single operation
///
/// The string to operate on
/// The number of spaces to indent by
/// The width of the column used for wrapping
///
/// The string is wrapped _then_ indented so the columnWidth is the width of the
/// usable text block, and does NOT include the indentLevel.
///
/// the processed string
public static string WrapAndIndentText(string input, int indentLevel,int columnWidth)
{
return new TextWrapper(input)
.WordWrap(columnWidth)
.Indent(indentLevel)
.ToText();
}
private string [] WordWrapLine(string line,int columnWidth)
{
//create a list of individual lines generated from the supplied line
//When handling sub-indentation we must always reserve at least one column for text!
var unindentedLine = line.TrimStart();
var currentIndentLevel = Math.Min(line.Length - unindentedLine.Length,columnWidth-1) ;
columnWidth -= currentIndentLevel;
return unindentedLine.Split(' ')
.Aggregate(
new List(),
(lineList, word) => AddWordToLastLineOrCreateNewLineIfNecessary(lineList, word, columnWidth)
)
.Select(builder => currentIndentLevel.Spaces()+builder.ToString().TrimEnd())
.ToArray();
}
///
/// When presented with a word, either append to the last line in the list or start a new line
///
/// A list of StringBuilders containing results so far
/// The individual word to append
/// The usable text space
///
/// The 'word' can actually be an empty string. It's important to keep these -
/// empty strings allow us to preserve indentation and extra spaces within a line.
///
/// The same list as is passed in
private static List AddWordToLastLineOrCreateNewLineIfNecessary(List lines, string word,int columnWidth)
{
//The current indentation level is based on the previous line but we need to be careful
var previousLine = lines.LastOrDefault()?.ToString() ??string.Empty;
var wouldWrap = !lines.Any() || (word.Length>0 && previousLine.Length + word.Length > columnWidth);
if (!wouldWrap)
{
//The usual case is we just append the 'word' and a space to the current line
//Note that trailing spaces will get removed later when we turn the line list
//into a single string
lines.Last().Append(word + ' ');
}
else
{
//The 'while' here is to take account of the possibility of someone providing a word
//which just can't fit in the current column. In that case we just split it at the
//column end.
//That's a rare case though - most of the time we'll succeed in a single pass without
//having to split
//Note that we always do at least one pass even if the 'word' is empty in order to
//honour sub-indentation and extra spaces within strings
do
{
var availableCharacters = Math.Min(columnWidth, word.Length);
var segmentToAdd = LeftString(word,availableCharacters) + ' ';
lines.Add(new StringBuilder(segmentToAdd));
word = RightString(word,availableCharacters);
} while (word.Length > 0);
}
return lines;
}
///
/// Return the right part of a string in a way that compensates for Substring's deficiencies
///
private static string RightString(string str,int n)
{
return (n >= str.Length || str.Length==0)
? string.Empty
: str.Substring(n);
}
///
/// Return the left part of a string in a way that compensates for Substring's deficiencies
///
private static string LeftString(string str,int n)
{
return (n >= str.Length || str.Length==0)
? str
: str.Substring(0,n);
}
}
}