1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.cli.help;
18
19 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.Collection;
23 import java.util.Collections;
24 import java.util.HashSet;
25 import java.util.LinkedList;
26 import java.util.List;
27 import java.util.Queue;
28 import java.util.Set;
29
30
31
32
33
34
35 public class TextHelpAppendable extends FilterHelpAppendable {
36
37
38 public static final int DEFAULT_WIDTH = 74;
39
40
41 public static final int DEFAULT_LEFT_PAD = 1;
42
43
44 public static final int DEFAULT_INDENT = 3;
45
46
47 public static final int DEFAULT_LIST_INDENT = 7;
48
49
50 private static final String BLANK_LINE = "";
51
52
53
54 private static final Set<Character> BREAK_CHAR_SET = Collections.unmodifiableSet(new HashSet<>(Arrays.asList('\t', '\n', '\f', '\r',
55 (char) Character.LINE_SEPARATOR,
56 (char) Character.PARAGRAPH_SEPARATOR,
57 '\u000b',
58 '\u001c',
59 '\u001d',
60 '\u001e',
61 '\u001f'
62 )));
63
64
65
66
67
68
69
70
71
72
73
74
75 public static int indexOfWrap(final CharSequence text, final int width, final int startPos) {
76 if (width < 1) {
77 throw new IllegalArgumentException("Width must be greater than 0");
78 }
79
80
81 int limit = Math.min(startPos + width, text.length());
82 for (int idx = startPos; idx < limit; idx++) {
83 if (BREAK_CHAR_SET.contains(text.charAt(idx))) {
84 return idx;
85 }
86 }
87 if (startPos + width >= text.length()) {
88 return text.length();
89 }
90
91 limit = Math.min(startPos + width, text.length() - 1);
92 int pos;
93
94 for (pos = limit; pos >= startPos; --pos) {
95 if (Util.isWhitespace(text.charAt(pos))) {
96 break;
97 }
98 }
99
100 return pos > startPos ? pos : limit - 1;
101 }
102
103
104
105
106
107
108 protected static TextHelpAppendable systemOut() {
109 return new TextHelpAppendable(System.out);
110 }
111
112
113 private final TextStyle.Builder textStyleBuilder;
114
115
116
117
118
119
120
121 public TextHelpAppendable(final Appendable output) {
122 super(output);
123
124 textStyleBuilder = TextStyle.builder()
125 .setMaxWidth(DEFAULT_WIDTH)
126 .setLeftPad(DEFAULT_LEFT_PAD)
127 .setIndent(DEFAULT_INDENT);
128
129 }
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 protected TableDefinition adjustTableFormat(final TableDefinition table) {
150 final List<TextStyle.Builder> styleBuilders = new ArrayList<>();
151 for (int i = 0; i < table.columnTextStyles().size(); i++) {
152 final TextStyle style = table.columnTextStyles().get(i);
153 final TextStyle.Builder builder = TextStyle.builder().setTextStyle(style);
154 styleBuilders.add(builder);
155 final String header = table.headers().get(i);
156
157 if (style.getMaxWidth() < header.length() || style.getMaxWidth() == TextStyle.UNSET_MAX_WIDTH) {
158 builder.setMaxWidth(header.length());
159 }
160 if (style.getMinWidth() < header.length()) {
161 builder.setMinWidth(header.length());
162 }
163 for (final List<String> row : table.rows()) {
164 final String cell = row.get(i);
165 if (cell.length() > builder.getMaxWidth()) {
166 builder.setMaxWidth(cell.length());
167 }
168 }
169 }
170
171 int calcWidth = 0;
172 int adjustedMaxWidth = textStyleBuilder.getMaxWidth();
173 for (final TextStyle.Builder builder : styleBuilders) {
174 adjustedMaxWidth -= builder.getLeftPad();
175 if (builder.isScalable()) {
176 calcWidth += builder.getMaxWidth();
177 } else {
178 adjustedMaxWidth -= builder.getMaxWidth();
179 }
180 }
181
182 if (calcWidth > adjustedMaxWidth) {
183 final double fraction = adjustedMaxWidth * 1.0 / calcWidth;
184 for (int i = 0; i < styleBuilders.size(); i++) {
185 final TextStyle.Builder builder = styleBuilders.get(i);
186 if (builder.isScalable()) {
187
188 styleBuilders.set(i, resize(builder, fraction));
189 }
190 }
191 }
192
193 final List<TextStyle> styles = new ArrayList<>();
194
195 styleBuilders.forEach(builder -> styles.add(builder.get()));
196 return TableDefinition.from(table.caption(), styles, table.headers(), table.rows());
197 }
198
199 @Override
200 public void appendHeader(final int level, final CharSequence text) throws IOException {
201 if (!Util.isEmpty(text)) {
202 if (level < 1) {
203 throw new IllegalArgumentException("level must be at least 1");
204 }
205 final char[] fillChars = { '=', '%', '+', '_' };
206 final int idx = Math.min(level, fillChars.length) - 1;
207 final TextStyle style = textStyleBuilder.get();
208 final Queue<String> queue = makeColumnQueue(text, style);
209 queue.add(Util.repeatSpace(style.getLeftPad()) + Util.repeat(Math.min(text.length(), style.getMaxWidth()), fillChars[idx]));
210 queue.add(BLANK_LINE);
211 printQueue(queue);
212 }
213 }
214
215 @Override
216 public void appendList(final boolean ordered, final Collection<CharSequence> list) throws IOException {
217 if (list != null && !list.isEmpty()) {
218 final TextStyle.Builder builder = TextStyle.builder().setLeftPad(textStyleBuilder.getLeftPad()).setIndent(DEFAULT_LIST_INDENT);
219 int i = 1;
220 for (final CharSequence line : list) {
221 final String entry = ordered ? String.format(" %s. %s", i++, Util.defaultValue(line, BLANK_LINE))
222 : String.format(" * %s", Util.defaultValue(line, BLANK_LINE));
223 builder.setMaxWidth(Math.min(textStyleBuilder.getMaxWidth(), entry.length()));
224 printQueue(makeColumnQueue(entry, builder.get()));
225 }
226 output.append(System.lineSeparator());
227 }
228 }
229
230 @Override
231 public void appendParagraph(final CharSequence paragraph) throws IOException {
232 if (!Util.isEmpty(paragraph)) {
233 final Queue<String> queue = makeColumnQueue(paragraph, textStyleBuilder.get());
234 queue.add(BLANK_LINE);
235 printQueue(queue);
236 }
237 }
238
239 @Override
240 public void appendTable(final TableDefinition rawTable) throws IOException {
241 final TableDefinition table = adjustTableFormat(rawTable);
242
243 appendParagraph(table.caption());
244 final List<TextStyle> headerStyles = new ArrayList<>();
245 table.columnTextStyles().forEach(style -> headerStyles.add(TextStyle.builder().setTextStyle(style).setAlignment(TextStyle.Alignment.CENTER).get()));
246 writeColumnQueues(makeColumnQueues(table.headers(), headerStyles), headerStyles);
247 for (final List<String> row : table.rows()) {
248 writeColumnQueues(makeColumnQueues(row, table.columnTextStyles()), table.columnTextStyles());
249 }
250 output.append(System.lineSeparator());
251 }
252
253 @Override
254 public void appendTitle(final CharSequence title) throws IOException {
255 if (!Util.isEmpty(title)) {
256 final TextStyle style = textStyleBuilder.get();
257 final Queue<String> queue = makeColumnQueue(title, style);
258 queue.add(Util.repeatSpace(style.getLeftPad()) + Util.repeat(Math.min(title.length(), style.getMaxWidth()), '#'));
259 queue.add(BLANK_LINE);
260 printQueue(queue);
261 }
262 }
263
264
265
266
267
268
269 public int getIndent() {
270 return textStyleBuilder.getIndent();
271 }
272
273
274
275
276
277
278 public int getLeftPad() {
279 return textStyleBuilder.getLeftPad();
280 }
281
282
283
284
285
286
287 public int getMaxWidth() {
288 return textStyleBuilder.getMaxWidth();
289 }
290
291
292
293
294
295
296 public TextStyle.Builder getTextStyleBuilder() {
297 return textStyleBuilder;
298 }
299
300
301
302
303
304
305
306
307 protected Queue<String> makeColumnQueue(final CharSequence columnData, final TextStyle style) {
308 final String lpad = Util.repeatSpace(style.getLeftPad());
309 final String indent = Util.repeatSpace(style.getIndent());
310 final Queue<String> result = new LinkedList<>();
311 int wrapPos = 0;
312 int lastPos;
313 final int wrappedMaxWidth = style.getMaxWidth() - indent.length();
314 while (wrapPos < columnData.length()) {
315 final int workingWidth = wrapPos == 0 ? style.getMaxWidth() : wrappedMaxWidth;
316 lastPos = indexOfWrap(columnData, workingWidth, wrapPos);
317 final CharSequence working = columnData.subSequence(wrapPos, lastPos);
318 result.add(lpad + style.pad(wrapPos > 0, working));
319 wrapPos = Util.indexOfNonWhitespace(columnData, lastPos);
320 wrapPos = wrapPos == -1 ? lastPos + 1 : wrapPos;
321 }
322 return result;
323 }
324
325
326
327
328
329
330
331
332
333 protected List<Queue<String>> makeColumnQueues(final List<String> columnData, final List<TextStyle> styles) {
334 final List<Queue<String>> result = new ArrayList<>();
335 for (int i = 0; i < columnData.size(); i++) {
336 result.add(makeColumnQueue(columnData.get(i), styles.get(i)));
337 }
338 return result;
339 }
340
341
342
343
344
345
346
347 private void printQueue(final Queue<String> queue) throws IOException {
348 for (final String s : queue) {
349 appendFormat("%s%n", Util.rtrim(s));
350 }
351 }
352
353
354
355
356
357
358
359 public void printWrapped(final String text) throws IOException {
360 printQueue(makeColumnQueue(text, this.textStyleBuilder.get()));
361 }
362
363
364
365
366
367
368
369
370 public void printWrapped(final String text, final TextStyle style) throws IOException {
371 printQueue(makeColumnQueue(text, style));
372 }
373
374
375
376
377
378
379
380
381 private int resize(final int orig, final double fraction) {
382 return (int) (orig * fraction);
383 }
384
385
386
387
388
389
390
391
392 protected TextStyle.Builder resize(final TextStyle.Builder builder, final double fraction) {
393 final double indentFrac = builder.getIndent() * 1.0 / builder.getMaxWidth();
394 builder.setMaxWidth(Math.max(resize(builder.getMaxWidth(), fraction), builder.getMinWidth()));
395 final int maxAdjust = builder.getMaxWidth() / 3;
396 int newIndent = builder.getMaxWidth() == 1 ? 0 : builder.getIndent();
397 if (newIndent > maxAdjust) {
398 newIndent = Math.min(resize(builder.getIndent(), indentFrac), maxAdjust);
399 }
400 builder.setIndent(newIndent);
401 return builder;
402 }
403
404
405
406
407
408
409 public void setIndent(final int indent) {
410 textStyleBuilder.setIndent(indent);
411 }
412
413
414
415
416
417
418 public void setLeftPad(final int leftPad) {
419 textStyleBuilder.setLeftPad(leftPad);
420 }
421
422
423
424
425
426
427 public void setMaxWidth(final int maxWidth) {
428 textStyleBuilder.setMaxWidth(maxWidth);
429 }
430
431
432
433
434
435
436
437
438
439 protected void writeColumnQueues(final List<Queue<String>> columnQueues, final List<TextStyle> styles) throws IOException {
440 boolean moreData = true;
441 final String lPad = Util.repeatSpace(textStyleBuilder.get().getLeftPad());
442 while (moreData) {
443 output.append(lPad);
444 moreData = false;
445 for (int i = 0; i < columnQueues.size(); i++) {
446 final TextStyle style = styles.get(i);
447 final Queue<String> columnQueue = columnQueues.get(i);
448 final String line = columnQueue.poll();
449 if (Util.isEmpty(line)) {
450 output.append(Util.repeatSpace(style.getMaxWidth() + style.getLeftPad()));
451 } else {
452 output.append(line);
453 }
454 moreData |= !columnQueue.isEmpty();
455 }
456 output.append(System.lineSeparator());
457 }
458 }
459 }