mirror of https://github.com/jarro2783/cxxopts.git
feat: New wrapping algorithm, tests, and using wrapping on help text above options list
This commit is contained in:
parent
185cf9ef80
commit
edbfbad35b
|
|
@ -2210,6 +2210,141 @@ namespace {
|
|||
constexpr std::size_t OPTION_LONGEST = 30;
|
||||
constexpr std::size_t OPTION_DESC_GAP = 2;
|
||||
|
||||
|
||||
|
||||
String
|
||||
wrap_text
|
||||
(
|
||||
const String& text,
|
||||
std::size_t allowed,
|
||||
std::size_t start = 0 // spaces_to_append_at_newline
|
||||
)
|
||||
{
|
||||
if(allowed == 0) return String{};
|
||||
|
||||
String result;
|
||||
auto current = std::begin(text);
|
||||
using Iterator = decltype(current);
|
||||
|
||||
auto startLine = current;
|
||||
auto lastSpace = current;
|
||||
auto size = std::size_t{};
|
||||
|
||||
bool firstLine = true;
|
||||
const auto end = std::end(text);
|
||||
|
||||
// Loop invariants at the beginning of each iteration:
|
||||
// 1 - [std::begin(text), startLine) is already added to result
|
||||
// 2 - currentLine [startLine, current) is not added to result yet
|
||||
// 3 - size is the number of characters in [startLine, current)
|
||||
//
|
||||
// At every loop we try to include current in the currentLine.
|
||||
// If there is a need to start a new line, we do that first.
|
||||
|
||||
// Treat explicit newlines as whitespace for trimming and break detection.
|
||||
auto is_space = [](Iterator itr) -> bool {
|
||||
return *itr == ' ' || *itr == '\t' || *itr == '\n';
|
||||
};
|
||||
|
||||
// Ensure when calling begin <= end
|
||||
auto add_line = [&firstLine, &result, &is_space, &start](Iterator begin, Iterator end) {
|
||||
// begin == end means empty line
|
||||
// Handle newlines, clamping, everything here
|
||||
if(!firstLine) {
|
||||
stringAppend(result, 1, '\n');
|
||||
}
|
||||
|
||||
// Trim trailing spaces
|
||||
// Note: Its possible to be left with empty line after trim
|
||||
// So this trimming should be done before the next check.
|
||||
while(end != begin && is_space(std::prev(end))) {
|
||||
--end;
|
||||
}
|
||||
|
||||
// Actual Content
|
||||
if(begin != end) {
|
||||
// Clamp if not the first line
|
||||
if(!firstLine) stringAppend(result, start, ' ');
|
||||
stringAppend(result, begin, end);
|
||||
}
|
||||
|
||||
firstLine = false;
|
||||
};
|
||||
|
||||
// Make the line [itr, current]
|
||||
// It is assumed, as a special case for the below algorithm
|
||||
// that [itr, current] doesn't contains any space.
|
||||
// either its called with itr = std::next(current)
|
||||
// or with an itr <= current in case of word splitting
|
||||
auto reset_line_start = [&size, &startLine, &lastSpace, ¤t](Iterator itr) {
|
||||
startLine = itr;
|
||||
lastSpace = startLine;
|
||||
|
||||
size = std::distance(startLine, std::next(current));
|
||||
};
|
||||
|
||||
|
||||
for (; current != end; ++current)
|
||||
{
|
||||
const auto currentNext = std::next(current);
|
||||
|
||||
if(*current == '\n') {
|
||||
add_line(startLine, current);
|
||||
reset_line_start(currentNext);
|
||||
|
||||
// Last character is a newline. Hence there is another line to be added. An empty one
|
||||
// And we need to do that now as we don't be doing further iterations
|
||||
if(currentNext == end) {
|
||||
add_line(currentNext, currentNext);
|
||||
}
|
||||
|
||||
} else {
|
||||
size ++ ;
|
||||
if(is_space(current)) {
|
||||
lastSpace = current;
|
||||
}
|
||||
bool endHere = false;
|
||||
auto endLine = currentNext;
|
||||
|
||||
if(currentNext == end) {
|
||||
endHere = true;
|
||||
}
|
||||
else if(is_space(current) && size == 1) {
|
||||
// Ignore leading spaces
|
||||
reset_line_start(currentNext);
|
||||
}
|
||||
else if(size >= allowed && !is_space(currentNext)) {
|
||||
// Don't break. Think of cases 'abc \nxyz' with allowed=5
|
||||
// we will decide in the next iteration if needed
|
||||
//
|
||||
// Now we know currentNext is not a space:
|
||||
// - if there is no breakable whitespace, we have to split the word
|
||||
// - if the line ends in whitespace, split here; add_line() will trim it
|
||||
// - otherwise split from the last whitespace inside the line
|
||||
if(lastSpace != startLine && lastSpace != current)
|
||||
{
|
||||
endLine = std::next(lastSpace);
|
||||
}
|
||||
|
||||
// If the chosen break lands right before an explicit newline, let the
|
||||
// newline branch handle it instead of forcing an extra wrapped line.
|
||||
if(*endLine == '\n') {
|
||||
endHere = false;
|
||||
} else {
|
||||
endHere = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(endHere) {
|
||||
add_line(startLine, endLine);
|
||||
reset_line_start(endLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
String
|
||||
format_option
|
||||
(
|
||||
|
|
@ -2279,7 +2414,6 @@ format_description
|
|||
}
|
||||
}
|
||||
|
||||
String result;
|
||||
|
||||
if (tab_expansion)
|
||||
{
|
||||
|
|
@ -2307,83 +2441,10 @@ format_description
|
|||
desc = desc2;
|
||||
}
|
||||
|
||||
desc += " ";
|
||||
|
||||
auto current = std::begin(desc);
|
||||
auto previous = current;
|
||||
auto startLine = current;
|
||||
auto lastSpace = current;
|
||||
|
||||
auto size = std::size_t{};
|
||||
|
||||
bool appendNewLine;
|
||||
bool onlyWhiteSpace = true;
|
||||
|
||||
while (current != std::end(desc))
|
||||
{
|
||||
appendNewLine = false;
|
||||
if (*previous == ' ' || *previous == '\t')
|
||||
{
|
||||
lastSpace = current;
|
||||
}
|
||||
if (*current != ' ' && *current != '\t')
|
||||
{
|
||||
onlyWhiteSpace = false;
|
||||
}
|
||||
|
||||
while (*current == '\n')
|
||||
{
|
||||
previous = current;
|
||||
++current;
|
||||
appendNewLine = true;
|
||||
}
|
||||
|
||||
if (!appendNewLine && size >= allowed)
|
||||
{
|
||||
if (lastSpace != startLine)
|
||||
{
|
||||
current = lastSpace;
|
||||
previous = current;
|
||||
}
|
||||
appendNewLine = true;
|
||||
}
|
||||
|
||||
if (appendNewLine)
|
||||
{
|
||||
stringAppend(result, startLine, current);
|
||||
startLine = current;
|
||||
lastSpace = current;
|
||||
|
||||
if (*previous != '\n')
|
||||
{
|
||||
stringAppend(result, "\n");
|
||||
}
|
||||
|
||||
stringAppend(result, start, ' ');
|
||||
|
||||
if (*previous != '\n')
|
||||
{
|
||||
stringAppend(result, lastSpace, current);
|
||||
}
|
||||
|
||||
onlyWhiteSpace = true;
|
||||
size = 0;
|
||||
}
|
||||
|
||||
previous = current;
|
||||
++current;
|
||||
++size;
|
||||
}
|
||||
|
||||
//append whatever is left but ignore whitespace
|
||||
if (!onlyWhiteSpace)
|
||||
{
|
||||
stringAppend(result, startLine, previous);
|
||||
}
|
||||
|
||||
return result;
|
||||
return wrap_text(desc, allowed, start);
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
||||
|
||||
inline
|
||||
|
|
@ -2925,6 +2986,7 @@ Options::help_one_group(const std::string& g) const
|
|||
auto d = format_description(o, longest + OPTION_DESC_GAP, allowed, m_tab_expansion);
|
||||
|
||||
result += fiter->first;
|
||||
|
||||
if (stringLength(fiter->first) > longest)
|
||||
{
|
||||
result += '\n';
|
||||
|
|
@ -2996,6 +3058,8 @@ Options::help(const std::vector<std::string>& help_groups, bool print_usage) con
|
|||
|
||||
result += "\n\n";
|
||||
|
||||
result = wrap_text(result, m_width, 0);
|
||||
|
||||
if (help_groups.empty())
|
||||
{
|
||||
generate_all_groups_help(result);
|
||||
|
|
|
|||
340
test/options.cpp
340
test/options.cpp
|
|
@ -1578,6 +1578,120 @@ TEST_CASE("Help output wrapping", "[help]")
|
|||
std::vector<std::string> positionals;
|
||||
std::string expected;
|
||||
} tests[] = {
|
||||
{
|
||||
"Basic test",
|
||||
cxxopts::Options("prog_abc", "This is a sample program for snake jazz")
|
||||
.positional_help("Positional help")
|
||||
.custom_help("Custom help")
|
||||
.set_width(15),
|
||||
{{"o,opt", "Sample description"}},
|
||||
{"o"},
|
||||
"This is a\n"
|
||||
"sample program\n"
|
||||
"for snake jazz\n"
|
||||
"Usage:\n"
|
||||
"prog_abc Custom\n"
|
||||
"help Positional\n"
|
||||
"help\n"
|
||||
"\n"
|
||||
" -o, --opt Sample\n"
|
||||
" descriptio\n"
|
||||
" n\n"
|
||||
},
|
||||
{
|
||||
"Custom help manual newline",
|
||||
cxxopts::Options("prog")
|
||||
.custom_help("Custom\nHelp")
|
||||
.set_width(12),
|
||||
{{"o,opt", "desc"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog Custom\n"
|
||||
"Help\n"
|
||||
"\n"
|
||||
" -o, --opt desc\n"
|
||||
},
|
||||
{
|
||||
"Description spaces before explicit newline",
|
||||
cxxopts::Options("prog")
|
||||
.set_width(18),
|
||||
{{"o,opt", "alpha \nbeta"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog [OPTION...]\n"
|
||||
"\n"
|
||||
" -o, --opt alpha\n"
|
||||
" beta\n"
|
||||
},
|
||||
{
|
||||
"Description blank line is preserved",
|
||||
cxxopts::Options("prog")
|
||||
.set_width(18),
|
||||
{{"o,opt", "alpha\n\nbeta"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog [OPTION...]\n"
|
||||
"\n"
|
||||
" -o, --opt alpha\n"
|
||||
"\n"
|
||||
" beta\n"
|
||||
},
|
||||
{
|
||||
"Description trailing newline is preserved",
|
||||
cxxopts::Options("prog")
|
||||
.set_width(18),
|
||||
{{"o,opt", "alpha\n"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog [OPTION...]\n"
|
||||
"\n"
|
||||
" -o, --opt alpha\n"
|
||||
"\n"
|
||||
},
|
||||
{
|
||||
"Description leading newline is preserved",
|
||||
cxxopts::Options("prog")
|
||||
.set_width(18),
|
||||
{{"o,opt", "\nalpha"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog [OPTION...]\n"
|
||||
"\n"
|
||||
" -o, --opt \n"
|
||||
" alpha\n"
|
||||
},
|
||||
{
|
||||
"Custom help trailing newline is preserved",
|
||||
cxxopts::Options("prog")
|
||||
.custom_help("Custom\n")
|
||||
.set_width(12),
|
||||
{{"o,opt", "desc"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog Custom\n"
|
||||
"\n"
|
||||
"\n"
|
||||
" -o, --opt desc\n"
|
||||
},
|
||||
{
|
||||
"Tab expansion happens before description wrapping",
|
||||
cxxopts::Options("prog")
|
||||
.set_width(26)
|
||||
.set_tab_expansion(true),
|
||||
{{"o,opt", "a\tb"}},
|
||||
{},
|
||||
"\n"
|
||||
"Usage:\n"
|
||||
"prog [OPTION...]\n"
|
||||
"\n"
|
||||
" -o, --opt a b\n"
|
||||
},
|
||||
{
|
||||
"Long word does not drop trailing character",
|
||||
cxxopts::Options("prog")
|
||||
|
|
@ -1635,7 +1749,7 @@ TEST_CASE("Help output wrapping", "[help]")
|
|||
|
||||
for (auto& tc : tests)
|
||||
{
|
||||
SECTION(tc.name)
|
||||
SECTION(tc.name)
|
||||
{
|
||||
for (const auto& opt : tc.opts)
|
||||
{
|
||||
|
|
@ -1646,3 +1760,227 @@ TEST_CASE("Help output wrapping", "[help]")
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST_CASE("wrap_text", "[wrap_text]")
|
||||
{
|
||||
struct {
|
||||
std::string name;
|
||||
std::string text;
|
||||
std::size_t allowed;
|
||||
std::size_t start;
|
||||
std::string expected;
|
||||
} tests[] = {
|
||||
{
|
||||
"Plain Newline",
|
||||
"\n",
|
||||
3,
|
||||
3,
|
||||
"\n"
|
||||
},
|
||||
{
|
||||
"Manual newlines",
|
||||
"abc\ndef\nghi",
|
||||
3,
|
||||
0,
|
||||
"abc\n"
|
||||
"def\n"
|
||||
"ghi"
|
||||
},
|
||||
{
|
||||
"Basic wrap",
|
||||
"abc def ghi",
|
||||
3,
|
||||
0,
|
||||
"abc\n"
|
||||
"def\n"
|
||||
"ghi"
|
||||
},
|
||||
{
|
||||
"Word splitting",
|
||||
"abcdefghi",
|
||||
3,
|
||||
0,
|
||||
"abc\n"
|
||||
"def\n"
|
||||
"ghi"
|
||||
},
|
||||
|
||||
{
|
||||
"Clamping with manual newlines",
|
||||
"\na\n\nbcdef",
|
||||
3,
|
||||
3,
|
||||
"\n"
|
||||
" a\n"
|
||||
"\n"
|
||||
" bcd\n"
|
||||
" ef"
|
||||
},
|
||||
{
|
||||
"Trailing newline is preserved",
|
||||
"abc\n",
|
||||
3,
|
||||
3,
|
||||
"abc\n"
|
||||
},
|
||||
{
|
||||
"Trailing spaces after final newline preserve newline",
|
||||
"abc\n ",
|
||||
5,
|
||||
2,
|
||||
"abc\n"
|
||||
},
|
||||
{
|
||||
"Whitespace around final newline preserves blank line",
|
||||
" \n ",
|
||||
5,
|
||||
2,
|
||||
"\n"
|
||||
},
|
||||
{
|
||||
"Consecutive newlines stay consecutive",
|
||||
"a\n\nb",
|
||||
3,
|
||||
3,
|
||||
"a\n"
|
||||
"\n"
|
||||
" b"
|
||||
},
|
||||
{
|
||||
"Leading spaces do not create blank lines",
|
||||
" abc",
|
||||
3,
|
||||
3,
|
||||
"abc"
|
||||
},
|
||||
{
|
||||
"Exact fit with separating space stays on one line",
|
||||
"a b",
|
||||
3,
|
||||
0,
|
||||
"a b"
|
||||
},
|
||||
{
|
||||
"Exact fit with longer words stays on one line",
|
||||
"abc def",
|
||||
7,
|
||||
0,
|
||||
"abc def"
|
||||
},
|
||||
{
|
||||
"Exact fit with separating tab stays on one line",
|
||||
"a\tb",
|
||||
3,
|
||||
0,
|
||||
"a\tb"
|
||||
},
|
||||
{
|
||||
"Whitespace wrap keeps the trailing word",
|
||||
"abc def",
|
||||
4,
|
||||
2,
|
||||
"abc\n"
|
||||
" def"
|
||||
},
|
||||
{
|
||||
"Empty string stays empty",
|
||||
"",
|
||||
3,
|
||||
0,
|
||||
""
|
||||
},
|
||||
{
|
||||
"Whitespace only stays empty",
|
||||
" ",
|
||||
3,
|
||||
2,
|
||||
""
|
||||
},
|
||||
{
|
||||
"Exact fit word stays on one line",
|
||||
"abc",
|
||||
3,
|
||||
0,
|
||||
"abc"
|
||||
},
|
||||
{
|
||||
"Internal spaces are preserved when no wrap is needed",
|
||||
"a b",
|
||||
10,
|
||||
0,
|
||||
"a b"
|
||||
},
|
||||
{
|
||||
"Whitespace runs at wrap boundaries keep all words",
|
||||
"23423 23424 343",
|
||||
10,
|
||||
3,
|
||||
"23423\n"
|
||||
" 23424\n"
|
||||
" 343"
|
||||
},
|
||||
{
|
||||
"Wrapped line followed by newline",
|
||||
"abcd\n",
|
||||
3,
|
||||
2,
|
||||
"abc\n"
|
||||
" d\n"
|
||||
},
|
||||
{
|
||||
"Wrapped line followed by newline 2",
|
||||
"abcd\nef",
|
||||
3,
|
||||
2,
|
||||
"abc\n"
|
||||
" d\n"
|
||||
" ef"
|
||||
},
|
||||
{
|
||||
"Edge case of minimum width",
|
||||
"abc\n\nx y\n z",
|
||||
1,
|
||||
1,
|
||||
"a\n"
|
||||
" b\n"
|
||||
" c\n"
|
||||
"\n"
|
||||
" x\n"
|
||||
" y\n"
|
||||
" z"
|
||||
},
|
||||
{
|
||||
"0 allowed",
|
||||
"abc",
|
||||
0,
|
||||
1,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"trailing spaces before an explicit newline",
|
||||
"abc \nxyz",
|
||||
5,
|
||||
0,
|
||||
"abc\n"
|
||||
"xyz"
|
||||
},
|
||||
{
|
||||
"Consecutive trailing newlines are preserved",
|
||||
"a\n\n",
|
||||
3,
|
||||
2,
|
||||
"a\n"
|
||||
"\n"
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& tc : tests)
|
||||
{
|
||||
SECTION(tc.name)
|
||||
{
|
||||
CHECK(cxxopts::wrap_text(tc.text, tc.allowed, tc.start) == tc.expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue