-
Notifications
You must be signed in to change notification settings - Fork 480
Expand file tree
/
Copy pathTextWrapper.cs
More file actions
179 lines (164 loc) · 7.41 KB
/
TextWrapper.cs
File metadata and controls
179 lines (164 loc) · 7.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CommandLine.Infrastructure;
namespace CommandLine.Text
{
/// <summary>
/// A utility class to word-wrap and indent blocks of text
/// </summary>
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);
}
/// <summary>
/// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation
/// </summary>
/// <param name="columnWidth">The number of characters we can use for text</param>
/// <remarks>
/// 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"
/// </remarks>
/// <returns>this</returns>
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;
}
/// <summary>
/// Indent all lines in the TextWrapper by the desired number of spaces
/// </summary>
/// <param name="numberOfSpaces">The number of spaces to indent by</param>
/// <returns>this</returns>
public TextWrapper Indent(int numberOfSpaces)
{
lines = lines
.Select(line => numberOfSpaces.Spaces() + line)
.ToArray();
return this;
}
/// <summary>
/// Returns the current state of the TextWrapper as a string
/// </summary>
/// <returns></returns>
public string ToText()
{
//return the whole thing as a single string
return string.Join(Environment.NewLine,lines);
}
/// <summary>
/// Convenience method to wraps and indent a string in a single operation
/// </summary>
/// <param name="input">The string to operate on</param>
/// <param name="indentLevel">The number of spaces to indent by</param>
/// <param name="columnWidth">The width of the column used for wrapping</param>
/// <remarks>
/// The string is wrapped _then_ indented so the columnWidth is the width of the
/// usable text block, and does NOT include the indentLevel.
/// </remarks>
/// <returns>the processed string</returns>
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<StringBuilder>(),
(lineList, word) => AddWordToLastLineOrCreateNewLineIfNecessary(lineList, word, columnWidth)
)
.Select(builder => currentIndentLevel.Spaces()+builder.ToString().TrimEnd())
.ToArray();
}
/// <summary>
/// When presented with a word, either append to the last line in the list or start a new line
/// </summary>
/// <param name="lines">A list of StringBuilders containing results so far</param>
/// <param name="word">The individual word to append</param>
/// <param name="columnWidth">The usable text space</param>
/// <remarks>
/// 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.
/// </remarks>
/// <returns>The same list as is passed in</returns>
private static List<StringBuilder> AddWordToLastLineOrCreateNewLineIfNecessary(List<StringBuilder> 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;
}
/// <summary>
/// Return the right part of a string in a way that compensates for Substring's deficiencies
/// </summary>
private static string RightString(string str,int n)
{
return (n >= str.Length || str.Length==0)
? string.Empty
: str.Substring(n);
}
/// <summary>
/// Return the left part of a string in a way that compensates for Substring's deficiencies
/// </summary>
private static string LeftString(string str,int n)
{
return (n >= str.Length || str.Length==0)
? str
: str.Substring(0,n);
}
}
}