@@ -21,6 +21,18 @@ public class HelpText
2121 {
2222 private const int BuilderCapacity = 128 ;
2323 private const int DefaultMaximumLength = 80 ; // default console width
24+ /// <summary>
25+ /// The number of spaces between an option and its associated help text
26+ /// </summary>
27+ private const int OptionToHelpTextSeparatorWidth = 4 ;
28+ /// <summary>
29+ /// The width of the option prefix (either "--" or " "
30+ /// </summary>
31+ private const int OptionPrefixWidth = 2 ;
32+ /// <summary>
33+ /// The total amount of extra space that needs to accounted for when indenting Option help text
34+ /// </summary>
35+ private const int TotalOptionPadding = OptionToHelpTextSeparatorWidth + OptionPrefixWidth ;
2436 private readonly StringBuilder preOptionsHelp ;
2537 private readonly StringBuilder postOptionsHelp ;
2638 private readonly SentenceBuilder sentenceBuilder ;
@@ -608,7 +620,7 @@ public static IEnumerable<string> RenderUsageTextAsLines<T>(ParserResult<T> pars
608620 var styles = example . GetFormatStylesOrDefault ( ) ;
609621 foreach ( var s in styles )
610622 {
611- var commandLine = new StringBuilder ( 2 . Spaces ( ) )
623+ var commandLine = new StringBuilder ( OptionPrefixWidth . Spaces ( ) )
612624 . Append ( appAlias )
613625 . Append ( ' ' )
614626 . Append ( Parser . Default . FormatCommandLine ( example . Sample ,
@@ -645,7 +657,7 @@ public override string ToString()
645657 . ToString ( ) ;
646658 }
647659
648- internal static void AddLine ( StringBuilder builder , string value , int maximumLength )
660+ internal static void AddLine ( StringBuilder builder , string value , int maximumLength )
649661 {
650662 if ( builder == null )
651663 {
@@ -665,37 +677,7 @@ internal static void AddLine(StringBuilder builder, string value, int maximumLen
665677 value = value . TrimEnd ( ) ;
666678
667679 builder . AppendWhen ( builder . Length > 0 , Environment . NewLine ) ;
668- do
669- {
670- var wordBuffer = 0 ;
671- var words = value . Split ( ' ' ) ;
672- for ( var i = 0 ; i < words . Length ; i ++ )
673- {
674- if ( words [ i ] . Length < ( maximumLength - wordBuffer ) )
675- {
676- builder . Append ( words [ i ] ) ;
677- wordBuffer += words [ i ] . Length ;
678- if ( ( maximumLength - wordBuffer ) > 1 && i != words . Length - 1 )
679- {
680- builder . Append ( " " ) ;
681- wordBuffer ++ ;
682- }
683- }
684- else if ( words [ i ] . Length >= maximumLength && wordBuffer == 0 )
685- {
686- builder . Append ( words [ i ] . Substring ( 0 , maximumLength ) ) ;
687- wordBuffer = maximumLength ;
688- break ;
689- }
690- else
691- break ;
692- }
693- value = value . Substring ( Math . Min ( wordBuffer , value . Length ) ) ;
694- builder . AppendWhen ( value . Length > 0 , Environment . NewLine ) ;
695- }
696- while ( value . Length > maximumLength ) ;
697-
698- builder . Append ( value ) ;
680+ builder . Append ( WrapAndIndentText ( value , 0 , maximumLength ) ) ;
699681 }
700682
701683 private IEnumerable < Specification > GetSpecificationsFromType ( Type type )
@@ -748,7 +730,7 @@ private IEnumerable<Specification> AdaptVerbsToSpecifications(IEnumerable<Type>
748730 return optionSpecs ;
749731 }
750732
751- private HelpText AddOptionsImpl (
733+ private HelpText AddOptionsImpl (
752734 IEnumerable < Specification > specifications ,
753735 string requiredWord ,
754736 int maximumLength )
@@ -757,7 +739,7 @@ private HelpText AddOptionsImpl(
757739
758740 optionsHelp = new StringBuilder ( BuilderCapacity ) ;
759741
760- var remainingSpace = maximumLength - ( maxLength + 6 ) ;
742+ var remainingSpace = maximumLength - ( maxLength + TotalOptionPadding ) ;
761743
762744 specifications . ForEach (
763745 option =>
@@ -809,7 +791,7 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe
809791
810792 optionsHelp
811793 . Append ( name . Length < maxLength ? name . ToString ( ) . PadRight ( maxLength ) : name . ToString ( ) )
812- . Append ( " " ) ;
794+ . Append ( OptionToHelpTextSeparatorWidth . Spaces ( ) ) ;
813795
814796 var optionHelpText = specification . HelpText ;
815797
@@ -821,44 +803,13 @@ private HelpText AddOption(string requiredWord, int maxLength, Specification spe
821803
822804 if ( specification . Required )
823805 optionHelpText = "{0} " . FormatInvariant ( requiredWord ) + optionHelpText ;
824-
825- if ( ! string . IsNullOrEmpty ( optionHelpText ) )
826- {
827- do
828- {
829- var wordBuffer = 0 ;
830- var words = optionHelpText . Split ( ' ' ) ;
831- for ( var i = 0 ; i < words . Length ; i ++ )
832- {
833- if ( words [ i ] . Length < ( widthOfHelpText - wordBuffer ) )
834- {
835- optionsHelp . Append ( words [ i ] ) ;
836- wordBuffer += words [ i ] . Length ;
837- if ( ( widthOfHelpText - wordBuffer ) > 1 && i != words . Length - 1 )
838- {
839- optionsHelp . Append ( " " ) ;
840- wordBuffer ++ ;
841- }
842- }
843- else if ( words [ i ] . Length >= widthOfHelpText && wordBuffer == 0 )
844- {
845- optionsHelp . Append ( words [ i ] . Substring ( 0 , widthOfHelpText ) ) ;
846- wordBuffer = widthOfHelpText ;
847- break ;
848- }
849- else
850- break ;
851- }
852-
853- optionHelpText = optionHelpText . Substring ( Math . Min ( wordBuffer , optionHelpText . Length ) ) . Trim ( ) ;
854- optionsHelp . AppendWhen ( optionHelpText . Length > 0 , Environment . NewLine ,
855- new string ( ' ' , maxLength + 6 ) ) ;
856- }
857- while ( optionHelpText . Length > widthOfHelpText ) ;
858- }
859-
806+
807+ //note that we need to indent trim the start of the string because it's going to be
808+ //appended to an existing line that is as long as the indent-level
809+ var indented = WrapAndIndentText ( optionHelpText , maxLength + TotalOptionPadding , widthOfHelpText ) . TrimStart ( ) ;
810+
860811 optionsHelp
861- . Append ( optionHelpText )
812+ . Append ( indented )
862813 . Append ( Environment . NewLine )
863814 . AppendWhen ( additionalNewLineAfterOption , Environment . NewLine ) ;
864815
@@ -944,13 +895,13 @@ private int GetMaxOptionLength(OptionSpecification spec)
944895 {
945896 specLength += spec . LongName . Length ;
946897 if ( AddDashesToOption )
947- specLength += 2 ;
898+ specLength += OptionPrefixWidth ;
948899
949900 specLength += metaLength ;
950901 }
951902
952903 if ( hasShort && hasLong )
953- specLength += 2 ; // ", "
904+ specLength += OptionPrefixWidth ;
954905
955906 return specLength ;
956907 }
@@ -997,5 +948,107 @@ private static string FormatDefaultValue<T>(T value)
997948 ? builder . ToString ( 0 , builder . Length - 1 )
998949 : string . Empty ;
999950 }
951+
952+ /// <summary>
953+ /// Splits a string into a words and performs wrapping while also preserving line-breaks and sub-indentation
954+ /// </summary>
955+ /// <param name="input">The string to wrap</param>
956+ /// <param name="indentLevel">The amount of padding at the start of each string</param>
957+ /// <param name="columnWidth">The number of characters we can use for text</param>
958+ /// <remarks>
959+ /// The use of "width" is slightly confusing in other methods. In this method, the columnWidth
960+ /// parameter is the number of characters we can use for text regardless of the indent level.
961+ /// For example, if columnWidth is 10 and indentLevel is 2, the input
962+ /// "a string for wrapping 01234567890123"
963+ /// would return
964+ /// " a string" + newline +
965+ /// " for" + newline +
966+ /// " wrapping" + newline +
967+ /// " 0123456789" + newline +
968+ /// " 0123"
969+ /// </remarks>
970+ /// <returns>A string that has been word-wrapped with padding on each line to indent it</returns>
971+ private static string WrapAndIndentText ( string input , int indentLevel , int columnWidth )
972+ {
973+ //start by splitting at newlines and then reinserting the newline as a separate word
974+ var lines = input . Split ( new [ ] { Environment . NewLine } , StringSplitOptions . None ) ;
975+ var lineCount = lines . Length ;
976+
977+ var tokens = lines
978+ . Zip ( new string [ lineCount ] , ( a , _ ) => new string [ ] { a , Environment . NewLine } )
979+ . SelectMany ( linePair=> linePair )
980+ . Take ( lineCount * 2 - 1 ) ;
981+
982+ //split into words
983+ var words = tokens
984+ . SelectMany ( l=> l . Split ( ' ' ) ) ;
985+
986+ //create a list of individual indented lines
987+ var wrappedLines = words
988+ . Aggregate < string , List < StringBuilder > > (
989+ new List < StringBuilder > ( ) ,
990+ ( lineList , word ) => AddWordToLastLineOrCreateNewLineIfNecessary ( lineList , word , columnWidth )
991+ )
992+ . Select ( builder => indentLevel . Spaces ( ) + builder . ToString ( ) . TrimEnd ( ) ) ;
993+
994+ //return the whole thing as a single string
995+ return string . Join ( Environment . NewLine , wrappedLines ) ;
996+ }
997+
998+ /// <summary>
999+ /// When presented with a word, either append to the last line in the list or start a new line
1000+ /// </summary>
1001+ /// <param name="lines">A list of stringbuilders containing results so far</param>
1002+ /// <param name="word">The individual word to append</param>
1003+ /// <param name="columnWidth">The usable text space</param>
1004+ /// <remarks>
1005+ /// The 'word' can actually be an empty string or a linefeed. It's important to keep these -
1006+ /// empty strings allow us to preserve indentation and extra spaces within a line and linefeeds
1007+ /// allow us to honour the users formatting wishes when the pass in multi-line helptext.
1008+ /// </remarks>
1009+ /// <returns>The same list as is passed in</returns>
1010+ private static List < StringBuilder > AddWordToLastLineOrCreateNewLineIfNecessary ( List < StringBuilder > lines , string word , int columnWidth )
1011+ {
1012+ if ( word == Environment . NewLine )
1013+ {
1014+ //A newline token just means advance to the next line.
1015+ lines . Add ( new StringBuilder ( ) ) ;
1016+ return lines ;
1017+ }
1018+ //The current indentLevel is based on the previous line.
1019+ var previousLine = lines . LastOrDefault ( ) ? . ToString ( ) ?? string . Empty ;
1020+ var currentIndentLevel = previousLine . Length - previousLine . TrimStart ( ) . Length ;
1021+
1022+ var wouldWrap = ! lines . Any ( ) || previousLine . Length + word . Length > columnWidth ;
1023+
1024+ if ( ! wouldWrap )
1025+ {
1026+ //The usual case is we just append the 'word' and a space to the current line
1027+ //Note that trailing spaces will get removed later when we turn the line list
1028+ //into a single string
1029+ lines . Last ( ) . Append ( word + ' ' ) ;
1030+ }
1031+ else
1032+ {
1033+ //The 'while' here is to take account of the possibility of someone providing a word
1034+ //which just can't fit in the current column. In that case we just split it at the
1035+ //column end.
1036+ //That's a rare case though - most of the time we'll succeed in a single pass without
1037+ //having to split
1038+ while ( word . Length > 0 )
1039+ {
1040+ var availableCharacters = Math . Min ( columnWidth - currentIndentLevel , word . Length ) ;
1041+
1042+ var segmentToAdd = currentIndentLevel . Spaces ( ) +
1043+ word . Substring ( 0 , availableCharacters ) + ' ' ;
1044+
1045+ lines . Add ( new StringBuilder ( segmentToAdd ) ) ;
1046+ word = word . Substring ( availableCharacters ) ;
1047+ }
1048+ }
1049+ return lines ;
1050+ }
1051+
1052+
10001053 }
1001- }
1054+ }
0 commit comments