Add help_max_width property for help description autowrapping.

This commit is contained in:
Peter Melnichenko
2018-04-08 19:03:25 +03:00
parent 8a3faf3a3e
commit 488a8e59a0
3 changed files with 177 additions and 0 deletions

View File

@@ -20,6 +20,8 @@
usage string autogeneration. usage string autogeneration.
* Added `help_usage_margin` and `help_description_margin` properties * Added `help_usage_margin` and `help_description_margin` properties
for configuring help string autogeneration. for configuring help string autogeneration.
* Added `help_max_width` property. If set, descriptions in help string
are automatically wrapped to fit into given number of columns.
### Improvements ### Improvements

View File

@@ -496,4 +496,102 @@ Options:
That needs documenting. That needs documenting.
-h, --help Show this help message and exit.]], parser:get_help()) -h, --help Show this help message and exit.]], parser:get_help())
end) end)
describe("autowrap", function()
it("automatically wraps descriptions to match given max width", function()
local parser = Parser "foo"
:help_max_width(80)
parser:option "-f --foo"
:description("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " ..
"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation " ..
"ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit " ..
"in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " ..
"non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
parser:option "-b --bar"
:description "See above."
assert.equal([[
Usage: foo [-f <foo>] [-b <bar>] [-h]
Options:
-f <foo>, Lorem ipsum dolor sit amet, consectetur adipiscing
--foo <foo> elit, sed do eiusmod tempor incididunt ut labore et
dolore magna aliqua. Ut enim ad minim veniam, quis
nostrud exercitation ullamco laboris nisi ut aliquip ex
ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu
fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
-b <bar>, See above.
--bar <bar>
-h, --help Show this help message and exit.]], parser:get_help())
end)
it("preserves existing line breaks", function()
local parser = Parser "foo"
:help_max_width(80)
parser:option "-f --foo"
:description("This is a long line, it should be broken down into several lines. " .. [[
It just keeps going and going.
This should always be a new line.
Another one.
]])
parser:option "-b --bar"
assert.equal([[
Usage: foo [-f <foo>] [-b <bar>] [-h]
Options:
-f <foo>, This is a long line, it should be broken down into
--foo <foo> several lines. It just keeps going and going.
This should always be a new line.
Another one.
-b <bar>,
--bar <bar>
-h, --help Show this help message and exit.]], parser:get_help())
end)
it("preserves indentation", function()
local parser = Parser "foo"
:help_max_width(80)
parser:option "-f --foo"
:description("This is a long line, it should be broken down into several lines.\n" ..
" This paragraph is indented with three spaces, so when it gets broken down into several lines, " ..
"they will be, too.\n\n" ..
" That was an empty line there, preserve it.")
assert.equal([[
Usage: foo [-f <foo>] [-h]
Options:
-f <foo>, This is a long line, it should be broken down into
--foo <foo> several lines.
This paragraph is indented with three spaces, so
when it gets broken down into several lines, they
will be, too.
That was an empty line there, preserve it.
-h, --help Show this help message and exit.]], parser:get_help())
end)
it("preserves multiple spaces between words", function()
local parser = Parser "foo"
:help_max_width(80)
parser:option "-f --foo"
:description("This is a long line with two spaces between words, it should be broken down.")
assert.equal([[
Usage: foo [-f <foo>] [-h]
Options:
-f <foo>, This is a long line with two spaces between
--foo <foo> words, it should be broken down.
-h, --help Show this help message and exit.]], parser:get_help())
end)
end)
end) end)

View File

@@ -251,6 +251,7 @@ local Parser = class({
typechecked("usage_max_width", "number"), typechecked("usage_max_width", "number"),
typechecked("help_usage_margin", "number"), typechecked("help_usage_margin", "number"),
typechecked("help_description_margin", "number"), typechecked("help_description_margin", "number"),
typechecked("help_max_width", "number"),
add_help add_help
}) })
@@ -273,6 +274,7 @@ local Command = class({
typechecked("usage_max_width", "number"), typechecked("usage_max_width", "number"),
typechecked("help_usage_margin", "number"), typechecked("help_usage_margin", "number"),
typechecked("help_description_margin", "number"), typechecked("help_description_margin", "number"),
typechecked("help_max_width", "number"),
typechecked("hidden", "boolean"), typechecked("hidden", "boolean"),
add_help add_help
}, Parser) }, Parser)
@@ -800,6 +802,75 @@ local function split_lines(s)
return lines return lines
end end
local function autowrap_line(line, max_length)
-- Algorithm for splitting lines is simple and greedy.
local result_lines = {}
-- Preserve original indentation of the line, put this at the beginning of each result line.
local indentation = line:match("^( *)")
-- Parts of the last line being assembled.
local line_parts = {}
-- Length of the current line.
local line_length = 0
-- Index of the next character to consider.
local index = 1
while true do
local word_start, word_finish, word = line:find("([^ ]+)", index)
if not word_start then
-- Ignore trailing spaces, if any.
break
end
local preceding_spaces = line:sub(index, word_start - 1)
index = word_finish + 1
if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then
-- Either this is the very first word or it fits as an addition to the current line, add it.
table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation.
table.insert(line_parts, word)
line_length = line_length + #preceding_spaces + #word
else
-- Does not fit, finish current line and put the word into a new one.
table.insert(result_lines, table.concat(line_parts))
line_parts = {indentation, word}
line_length = #indentation + #word
end
end
if #line_parts > 0 then
table.insert(result_lines, table.concat(line_parts))
end
if #result_lines == 0 then
-- Preserve empty lines.
result_lines[1] = ""
end
return result_lines
end
-- Automatically wraps lines within given array,
-- attempting to limit line length to `max_length`.
-- Existing line splits are preserved.
local function autowrap(lines, max_length)
local result_lines = {}
for _, line in ipairs(lines) do
local autowrapped_lines = autowrap_line(line, max_length)
for _, autowrapped_line in ipairs(autowrapped_lines) do
table.insert(result_lines, autowrapped_line)
end
end
return result_lines
end
function Parser:_get_element_help(element) function Parser:_get_element_help(element)
local label_lines = element:_get_label_lines() local label_lines = element:_get_label_lines()
local description_lines = split_lines(element:_get_description()) local description_lines = split_lines(element:_get_description())
@@ -815,6 +886,12 @@ function Parser:_get_element_help(element)
local description_margin_len = self:_inherit_property("help_description_margin", 25) local description_margin_len = self:_inherit_property("help_description_margin", 25)
local description_margin = (" "):rep(description_margin_len) local description_margin = (" "):rep(description_margin_len)
local help_max_width = self:_inherit_property("help_max_width")
if help_max_width then
local description_max_width = math.max(help_max_width - description_margin_len, 10)
description_lines = autowrap(description_lines, description_max_width)
end
if #label_lines[1] >= (description_margin_len - usage_margin_len) then if #label_lines[1] >= (description_margin_len - usage_margin_len) then
for _, label_line in ipairs(label_lines) do for _, label_line in ipairs(label_lines) do