commit
4a83a17c14
@ -0,0 +1,735 @@
|
|||||||
|
-- The help text database and query interface.
|
||||||
|
--
|
||||||
|
-- Help text is read from the rendered text in hack/docs/docs/. If no rendered
|
||||||
|
-- text exists, it is read from the script sources (for scripts) or the string
|
||||||
|
-- passed to the PluginCommand initializer (for plugins).
|
||||||
|
--
|
||||||
|
-- There should be one help file for each plugin that contains a summary for the
|
||||||
|
-- plugin itself and help for all the commands that plugin provides (if any).
|
||||||
|
-- Each script should also have one documentation file.
|
||||||
|
--
|
||||||
|
-- The database is lazy-loaded when an API method is called. It rechecks its
|
||||||
|
-- help sources for updates if an API method has not been called in the last
|
||||||
|
-- 60 seconds.
|
||||||
|
|
||||||
|
local _ENV = mkmodule('helpdb')
|
||||||
|
|
||||||
|
local MAX_STALE_MS = 60000
|
||||||
|
|
||||||
|
-- paths
|
||||||
|
local RENDERED_PATH = 'hack/docs/docs/tools/'
|
||||||
|
local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt'
|
||||||
|
|
||||||
|
-- used when reading help text embedded in script sources
|
||||||
|
local SCRIPT_DOC_BEGIN = '[====['
|
||||||
|
local SCRIPT_DOC_END = ']====]'
|
||||||
|
local SCRIPT_DOC_BEGIN_RUBY = '=begin'
|
||||||
|
local SCRIPT_DOC_END_RUBY = '=end'
|
||||||
|
|
||||||
|
-- enums
|
||||||
|
local ENTRY_TYPES = {
|
||||||
|
BUILTIN='builtin',
|
||||||
|
PLUGIN='plugin',
|
||||||
|
COMMAND='command'
|
||||||
|
}
|
||||||
|
|
||||||
|
local HELP_SOURCES = {
|
||||||
|
RENDERED='rendered', -- from the installed, rendered help text
|
||||||
|
PLUGIN='plugin', -- from the plugin source code
|
||||||
|
SCRIPT='script', -- from the script source code
|
||||||
|
STUB='stub', -- from a generated stub
|
||||||
|
}
|
||||||
|
|
||||||
|
-- builtin command names, with aliases mapped to their canonical form
|
||||||
|
local BUILTINS = {
|
||||||
|
['?']='help',
|
||||||
|
alias=true,
|
||||||
|
clear='cls',
|
||||||
|
cls=true,
|
||||||
|
['devel/dump-rpc']=true,
|
||||||
|
die=true,
|
||||||
|
dir='ls',
|
||||||
|
disable=true,
|
||||||
|
enable=true,
|
||||||
|
fpause=true,
|
||||||
|
help=true,
|
||||||
|
hide=true,
|
||||||
|
keybinding=true,
|
||||||
|
['kill-lua']=true,
|
||||||
|
['load']=true,
|
||||||
|
ls=true,
|
||||||
|
man='help',
|
||||||
|
plug=true,
|
||||||
|
reload=true,
|
||||||
|
script=true,
|
||||||
|
['sc-script']=true,
|
||||||
|
show=true,
|
||||||
|
tags=true,
|
||||||
|
['type']=true,
|
||||||
|
unload=true,
|
||||||
|
}
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
-- data structures
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- help text database, keys are a subset of the entry database
|
||||||
|
-- entry name -> {
|
||||||
|
-- help_source (element of HELP_SOURCES),
|
||||||
|
-- short_help (string),
|
||||||
|
-- long_help (string),
|
||||||
|
-- tags (set),
|
||||||
|
-- source_timestamp (mtime, 0 for non-files),
|
||||||
|
-- source_path (string, nil for non-files)
|
||||||
|
-- }
|
||||||
|
local textdb = {}
|
||||||
|
|
||||||
|
-- entry database, points to text in textdb
|
||||||
|
-- entry name -> {
|
||||||
|
-- entry_types (set of ENTRY_TYPES),
|
||||||
|
-- short_help (string, if not nil then overrides short_help in text_entry),
|
||||||
|
-- text_entry (string)
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- entry_types is a set because plugin commands can also be the plugin names.
|
||||||
|
local entrydb = {}
|
||||||
|
|
||||||
|
|
||||||
|
-- tag name -> list of entry names
|
||||||
|
-- Tags defined in the TAG_DEFINITIONS file that have no associated db entries
|
||||||
|
-- will have an empty list.
|
||||||
|
local tag_index = {}
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
-- data ingestion
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
local function get_rendered_path(entry_name)
|
||||||
|
return RENDERED_PATH .. entry_name .. '.txt'
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_rendered_help(entry_name)
|
||||||
|
return dfhack.filesystem.mtime(get_rendered_path(entry_name)) ~= -1
|
||||||
|
end
|
||||||
|
|
||||||
|
local DEFAULT_HELP_TEMPLATE = [[
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]]
|
||||||
|
local function make_default_entry(entry_name, help_source, kwargs)
|
||||||
|
local default_long_help = DEFAULT_HELP_TEMPLATE:format(
|
||||||
|
entry_name, ('*'):rep(#entry_name))
|
||||||
|
return {
|
||||||
|
help_source=help_source,
|
||||||
|
short_help='No help available.',
|
||||||
|
long_help=default_long_help,
|
||||||
|
tags={},
|
||||||
|
source_timestamp=kwargs.source_timestamp or 0,
|
||||||
|
source_path=kwargs.source_path}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- updates the short_text, the long_text, and the tags in the given entry based
|
||||||
|
-- on the text returned from the iterator.
|
||||||
|
-- if defined, opts can have the following fields:
|
||||||
|
-- begin_marker (string that marks the beginning of the help text; all text
|
||||||
|
-- before this marker is ignored)
|
||||||
|
-- end_marker (string that marks the end of the help text; text will stop
|
||||||
|
-- being parsed after this marker is seen)
|
||||||
|
-- no_header (don't try to find the entity name at the top of the help text)
|
||||||
|
-- first_line_is_short_help (if set, then read the short help text from the
|
||||||
|
-- first commented line of the script instead of
|
||||||
|
-- using the first sentence of the long help text.
|
||||||
|
-- value is the comment character.)
|
||||||
|
local function update_entry(entry, iterator, opts)
|
||||||
|
opts = opts or {}
|
||||||
|
local lines, tags = {}, ''
|
||||||
|
local first_line_is_short_help = opts.first_line_is_short_help
|
||||||
|
local begin_marker_found,header_found = not opts.begin_marker,opts.no_header
|
||||||
|
local tags_found, short_help_found = false, opts.skip_short_help
|
||||||
|
local in_tags, in_short_help = false, false
|
||||||
|
for line in iterator do
|
||||||
|
if not short_help_found and first_line_is_short_help then
|
||||||
|
line = line:trim()
|
||||||
|
local _,_,text = line:find('^'..first_line_is_short_help..'%s*(.*)')
|
||||||
|
if not text then
|
||||||
|
-- if no first-line short help found, fall back to getting the
|
||||||
|
-- first sentence of the help text.
|
||||||
|
first_line_is_short_help = false
|
||||||
|
else
|
||||||
|
if not text:endswith('.') then
|
||||||
|
text = text .. '.'
|
||||||
|
end
|
||||||
|
entry.short_help = text
|
||||||
|
short_help_found = true
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not begin_marker_found then
|
||||||
|
local _, endpos = line:find(opts.begin_marker, 1, true)
|
||||||
|
if endpos == #line then
|
||||||
|
begin_marker_found = true
|
||||||
|
end
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
if opts.end_marker then
|
||||||
|
local _, endpos = line:find(opts.end_marker, 1, true)
|
||||||
|
if endpos == #line then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not header_found and line:find('%w') then
|
||||||
|
header_found = true
|
||||||
|
elseif in_tags then
|
||||||
|
if #line == 0 then
|
||||||
|
in_tags = false
|
||||||
|
else
|
||||||
|
tags = tags .. line
|
||||||
|
end
|
||||||
|
elseif not tags_found and line:find('^[*]*Tags:[*]*') then
|
||||||
|
_,_,tags = line:trim():find('[*]*Tags:[*]* *(.*)')
|
||||||
|
in_tags, tags_found = true, true
|
||||||
|
elseif not short_help_found and
|
||||||
|
line:find('^%w') then
|
||||||
|
if in_short_help then
|
||||||
|
entry.short_help = entry.short_help .. ' ' .. line
|
||||||
|
else
|
||||||
|
entry.short_help = line
|
||||||
|
end
|
||||||
|
local sentence_end = entry.short_help:find('.', 1, true)
|
||||||
|
if sentence_end then
|
||||||
|
entry.short_help = entry.short_help:sub(1, sentence_end)
|
||||||
|
short_help_found = true
|
||||||
|
else
|
||||||
|
in_short_help = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(lines, line)
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
entry.tags = {}
|
||||||
|
for _,tag in ipairs(tags:split('[ ,|]+')) do
|
||||||
|
if #tag > 0 and tag_index[tag] then
|
||||||
|
entry.tags[tag] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #lines > 0 then
|
||||||
|
entry.long_help = table.concat(lines, '\n')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create db entry based on parsing sphinx-rendered help text
|
||||||
|
local function make_rendered_entry(old_entry, entry_name, kwargs)
|
||||||
|
local source_path = get_rendered_path(entry_name)
|
||||||
|
local source_timestamp = dfhack.filesystem.mtime(source_path)
|
||||||
|
if old_entry and old_entry.help_source == HELP_SOURCES.RENDERED and
|
||||||
|
old_entry.source_timestamp >= source_timestamp then
|
||||||
|
-- we already have the latest info
|
||||||
|
return old_entry
|
||||||
|
end
|
||||||
|
kwargs.source_path, kwargs.source_timestamp = source_path, source_timestamp
|
||||||
|
local entry = make_default_entry(entry_name, HELP_SOURCES.RENDERED, kwargs)
|
||||||
|
local ok, lines = pcall(io.lines, source_path)
|
||||||
|
if not ok then
|
||||||
|
return entry
|
||||||
|
end
|
||||||
|
update_entry(entry, lines)
|
||||||
|
return entry
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create db entry based on the help text in the plugin source (used by
|
||||||
|
-- out-of-tree plugins)
|
||||||
|
local function make_plugin_entry(old_entry, entry_name, kwargs)
|
||||||
|
if old_entry and old_entry.source == HELP_SOURCES.PLUGIN then
|
||||||
|
-- we can't tell when a plugin is reloaded, so we can either choose to
|
||||||
|
-- always refresh or never refresh. let's go with never for now for
|
||||||
|
-- performance.
|
||||||
|
return old_entry
|
||||||
|
end
|
||||||
|
local entry = make_default_entry(entry_name, HELP_SOURCES.PLUGIN, kwargs)
|
||||||
|
local long_help = dfhack.internal.getCommandHelp(entry_name)
|
||||||
|
if long_help and #long_help:trim() > 0 then
|
||||||
|
update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true})
|
||||||
|
end
|
||||||
|
return entry
|
||||||
|
end
|
||||||
|
|
||||||
|
-- create db entry based on the help text in the script source (used by
|
||||||
|
-- out-of-tree scripts)
|
||||||
|
local function make_script_entry(old_entry, entry_name, kwargs)
|
||||||
|
local source_path = kwargs.source_path
|
||||||
|
local source_timestamp = dfhack.filesystem.mtime(source_path)
|
||||||
|
if old_entry and old_entry.source == HELP_SOURCES.SCRIPT and
|
||||||
|
old_entry.source_path == source_path and
|
||||||
|
old_entry.source_timestamp >= source_timestamp then
|
||||||
|
-- we already have the latest info
|
||||||
|
return old_entry
|
||||||
|
end
|
||||||
|
kwargs.source_timestamp, kwargs.entry_type = source_timestamp
|
||||||
|
local entry = make_default_entry(entry_name, HELP_SOURCES.SCRIPT, kwargs)
|
||||||
|
local ok, lines = pcall(io.lines, source_path)
|
||||||
|
if not ok then
|
||||||
|
return entry
|
||||||
|
end
|
||||||
|
local is_rb = source_path:endswith('.rb')
|
||||||
|
update_entry(entry, lines,
|
||||||
|
{begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN),
|
||||||
|
end_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_END),
|
||||||
|
first_line_is_short_help=(is_rb and '#' or '%-%-')})
|
||||||
|
return entry
|
||||||
|
end
|
||||||
|
|
||||||
|
-- updates the dbs (and associated tag index) with a new entry if the entry_name
|
||||||
|
-- doesn't already exist in the dbs.
|
||||||
|
local function update_db(old_db, entry_name, text_entry, help_source, kwargs)
|
||||||
|
if entrydb[entry_name] then
|
||||||
|
-- already in db (e.g. from a higher-priority script dir); skip
|
||||||
|
return
|
||||||
|
end
|
||||||
|
entrydb[entry_name] = {
|
||||||
|
entry_types=kwargs.entry_types,
|
||||||
|
short_help=kwargs.short_help,
|
||||||
|
text_entry=text_entry
|
||||||
|
}
|
||||||
|
if entry_name ~= text_entry then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local text_entry, old_entry = nil, old_db[entry_name]
|
||||||
|
if help_source == HELP_SOURCES.RENDERED then
|
||||||
|
text_entry = make_rendered_entry(old_entry, entry_name, kwargs)
|
||||||
|
elseif help_source == HELP_SOURCES.PLUGIN then
|
||||||
|
text_entry = make_plugin_entry(old_entry, entry_name, kwargs)
|
||||||
|
elseif help_source == HELP_SOURCES.SCRIPT then
|
||||||
|
text_entry = make_script_entry(old_entry, entry_name, kwargs)
|
||||||
|
elseif help_source == HELP_SOURCES.STUB then
|
||||||
|
text_entry = make_default_entry(entry_name, HELP_SOURCES.STUB, kwargs)
|
||||||
|
else
|
||||||
|
error('unhandled help source: ' .. help_source)
|
||||||
|
end
|
||||||
|
textdb[entry_name] = text_entry
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add the builtin commands to the db
|
||||||
|
local function scan_builtins(old_db)
|
||||||
|
local entry_types = {[ENTRY_TYPES.BUILTIN]=true, [ENTRY_TYPES.COMMAND]=true}
|
||||||
|
for builtin,canonical in pairs(BUILTINS) do
|
||||||
|
if canonical == true then canonical = builtin end
|
||||||
|
update_db(old_db, builtin, canonical,
|
||||||
|
has_rendered_help(canonical) and
|
||||||
|
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
|
||||||
|
{entry_types=entry_types})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- scan for enableable plugins and plugin-provided commands and add their help
|
||||||
|
-- to the db
|
||||||
|
local function scan_plugins(old_db)
|
||||||
|
local plugin_names = dfhack.internal.listPlugins()
|
||||||
|
for _,plugin in ipairs(plugin_names) do
|
||||||
|
local commands = dfhack.internal.listCommands(plugin)
|
||||||
|
local includes_plugin = false
|
||||||
|
for _,command in ipairs(commands) do
|
||||||
|
local kwargs = {entry_types={[ENTRY_TYPES.COMMAND]=true}}
|
||||||
|
if command == plugin then
|
||||||
|
kwargs.entry_types[ENTRY_TYPES.PLUGIN]=true
|
||||||
|
includes_plugin = true
|
||||||
|
end
|
||||||
|
kwargs.short_help = dfhack.internal.getCommandDescription(command)
|
||||||
|
update_db(old_db, command, plugin,
|
||||||
|
has_rendered_help(plugin) and
|
||||||
|
HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN,
|
||||||
|
kwargs)
|
||||||
|
end
|
||||||
|
if not includes_plugin then
|
||||||
|
update_db(old_db, plugin, plugin,
|
||||||
|
has_rendered_help(plugin) and
|
||||||
|
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
|
||||||
|
{entry_types={[ENTRY_TYPES.PLUGIN]=true}})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- scan for scripts and add their help to the db
|
||||||
|
local function scan_scripts(old_db)
|
||||||
|
local entry_types = {[ENTRY_TYPES.COMMAND]=true}
|
||||||
|
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
|
||||||
|
local files = dfhack.filesystem.listdir_recursive(
|
||||||
|
script_path, nil, false)
|
||||||
|
if not files then goto skip_path end
|
||||||
|
for _,f in ipairs(files) do
|
||||||
|
if f.isdir or
|
||||||
|
(not f.path:endswith('.lua') and not f.path:endswith('.rb')) or
|
||||||
|
f.path:startswith('test/') or
|
||||||
|
f.path:startswith('internal/') then
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
local dot_index = f.path:find('%.[^.]*$')
|
||||||
|
local entry_name = f.path:sub(1, dot_index - 1)
|
||||||
|
local source_path = script_path .. '/' .. f.path
|
||||||
|
update_db(old_db, entry_name, entry_name,
|
||||||
|
has_rendered_help(entry_name) and
|
||||||
|
HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT,
|
||||||
|
{entry_types=entry_types, source_path=source_path})
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
::skip_path::
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- read tags and descriptions from the TAG_DEFINITIONS file and add them all
|
||||||
|
-- to tag_index, initizlizing each entry with an empty list.
|
||||||
|
local function initialize_tags()
|
||||||
|
local tag, desc, in_desc = nil, nil, false
|
||||||
|
local ok, lines = pcall(io.lines, TAG_DEFINITIONS)
|
||||||
|
if not ok then return end
|
||||||
|
for line in lines do
|
||||||
|
if in_desc then
|
||||||
|
line = line:trim()
|
||||||
|
if #line == 0 then
|
||||||
|
in_desc = false
|
||||||
|
goto continue
|
||||||
|
end
|
||||||
|
desc = desc .. ' ' .. line
|
||||||
|
tag_index[tag].description = desc
|
||||||
|
else
|
||||||
|
_,_,tag,desc = line:find('^%* (%w+): (.+)')
|
||||||
|
if not tag then goto continue end
|
||||||
|
tag_index[tag] = {description=desc}
|
||||||
|
in_desc = true
|
||||||
|
end
|
||||||
|
::continue::
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function index_tags()
|
||||||
|
for entry_name,entry in pairs(entrydb) do
|
||||||
|
for tag in pairs(textdb[entry.text_entry].tags) do
|
||||||
|
-- ignore unknown tags
|
||||||
|
if tag_index[tag] then
|
||||||
|
table.insert(tag_index[tag], entry_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ensures the db is up to date by scanning all help sources. does not do
|
||||||
|
-- anything if it has already been run within the last MAX_STALE_MS milliseconds
|
||||||
|
local last_refresh_ms = 0
|
||||||
|
local function ensure_db()
|
||||||
|
local now_ms = dfhack.getTickCount()
|
||||||
|
if now_ms - last_refresh_ms <= MAX_STALE_MS then return end
|
||||||
|
last_refresh_ms = now_ms
|
||||||
|
|
||||||
|
local old_db = textdb
|
||||||
|
textdb, entrydb, tag_index = {}, {}, {}
|
||||||
|
|
||||||
|
initialize_tags()
|
||||||
|
scan_builtins(old_db)
|
||||||
|
scan_plugins(old_db)
|
||||||
|
scan_scripts(old_db)
|
||||||
|
index_tags()
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
-- get API
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- converts strings into single-element lists containing that string
|
||||||
|
local function normalize_string_list(l)
|
||||||
|
if not l or #l == 0 then return nil end
|
||||||
|
if type(l) == 'string' then
|
||||||
|
return {l}
|
||||||
|
end
|
||||||
|
return l
|
||||||
|
end
|
||||||
|
|
||||||
|
local function has_keys(str, dict)
|
||||||
|
if not str or #str == 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
for _,s in ipairs(normalize_string_list(str)) do
|
||||||
|
if not dict[s] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns whether the given string (or list of strings) is an entry (are all
|
||||||
|
-- entries) in the db
|
||||||
|
function is_entry(str)
|
||||||
|
ensure_db()
|
||||||
|
return has_keys(str, entrydb)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function get_db_property(entry_name, property)
|
||||||
|
ensure_db()
|
||||||
|
if not entrydb[entry_name] then
|
||||||
|
error(('helpdb entry not found: "%s"'):format(entry_name))
|
||||||
|
end
|
||||||
|
return entrydb[entry_name][property] or
|
||||||
|
textdb[entrydb[entry_name].text_entry][property]
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_entry_types(entry)
|
||||||
|
return get_db_property(entry, 'entry_types')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns the ~54 char summary blurb associated with the entry
|
||||||
|
function get_entry_short_help(entry)
|
||||||
|
return get_db_property(entry, 'short_help')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns the full help documentation associated with the entry
|
||||||
|
function get_entry_long_help(entry)
|
||||||
|
return get_db_property(entry, 'long_help')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns the set of tags associated with the entry
|
||||||
|
function get_entry_tags(entry)
|
||||||
|
return get_db_property(entry, 'tags')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns whether the given string (or list of strings) matches a tag name
|
||||||
|
function is_tag(str)
|
||||||
|
ensure_db()
|
||||||
|
return has_keys(str, tag_index)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function set_to_sorted_list(set)
|
||||||
|
local list = {}
|
||||||
|
for item in pairs(set) do
|
||||||
|
table.insert(list, item)
|
||||||
|
end
|
||||||
|
table.sort(list)
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns the defined tags in alphabetical order
|
||||||
|
function get_tags()
|
||||||
|
ensure_db()
|
||||||
|
return set_to_sorted_list(tag_index)
|
||||||
|
end
|
||||||
|
|
||||||
|
function get_tag_data(tag)
|
||||||
|
ensure_db()
|
||||||
|
if not tag_index[tag] then
|
||||||
|
error(('helpdb tag not found: "%s"'):format(tag))
|
||||||
|
end
|
||||||
|
return tag_index[tag]
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
-- search API
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- returns a list of path elements in reverse order
|
||||||
|
local function chunk_for_sorting(str)
|
||||||
|
local parts = str:split('/')
|
||||||
|
local chunks = {}
|
||||||
|
for i=1,#parts do
|
||||||
|
chunks[#parts - i + 1] = parts[i]
|
||||||
|
end
|
||||||
|
return chunks
|
||||||
|
end
|
||||||
|
|
||||||
|
-- sorts by last path component, then by parent path components.
|
||||||
|
-- something comes before nothing.
|
||||||
|
-- e.g. gui/autofarm comes immediately before autofarm
|
||||||
|
function sort_by_basename(a, b)
|
||||||
|
local a = chunk_for_sorting(a)
|
||||||
|
local b = chunk_for_sorting(b)
|
||||||
|
local i = 1
|
||||||
|
while a[i] do
|
||||||
|
if not b[i] then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if a[i] ~= b[i] then
|
||||||
|
return a[i] < b[i]
|
||||||
|
end
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function matches(entry_name, filter)
|
||||||
|
if filter.tag then
|
||||||
|
local matched = false
|
||||||
|
local tags = get_db_property(entry_name, 'tags')
|
||||||
|
for _,tag in ipairs(filter.tag) do
|
||||||
|
if tags[tag] then
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not matched then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if filter.entry_type then
|
||||||
|
local matched = false
|
||||||
|
local etypes = get_db_property(entry_name, 'entry_types')
|
||||||
|
for _,etype in ipairs(filter.entry_type) do
|
||||||
|
if etypes[etype] then
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not matched then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if filter.str then
|
||||||
|
local matched = false
|
||||||
|
for _,str in ipairs(filter.str) do
|
||||||
|
if entry_name:find(str, 1, true) then
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not matched then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- normalizes the lists in the filter and returns nil if no filter elements are
|
||||||
|
-- populated
|
||||||
|
local function normalize_filter(f)
|
||||||
|
if not f then return nil end
|
||||||
|
local filter = {}
|
||||||
|
filter.str = normalize_string_list(f.str)
|
||||||
|
filter.tag = normalize_string_list(f.tag)
|
||||||
|
filter.entry_type = normalize_string_list(f.entry_type)
|
||||||
|
if not filter.str and not filter.tag and not filter.entry_type then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return filter
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns a list of entry names, alphabetized by their last path component,
|
||||||
|
-- with populated path components coming before null path components (e.g.
|
||||||
|
-- autobutcher will immediately follow gui/autobutcher).
|
||||||
|
-- the optional include and exclude filter params are maps with the following
|
||||||
|
-- elements:
|
||||||
|
-- str - if a string, filters by the given substring. if a table of strings,
|
||||||
|
-- includes entry names that match any of the given substrings.
|
||||||
|
-- tag - if a string, filters by the given tag name. if a table of strings,
|
||||||
|
-- includes entries that match any of the given tags.
|
||||||
|
-- entry_type - if a string, matches entries of the given type. if a table of
|
||||||
|
-- strings, includes entries that match any of the given types. valid
|
||||||
|
-- types are: "builtin", "plugin", "command". note that many plugin
|
||||||
|
-- commands have the same name as the plugin, so those entries will
|
||||||
|
-- match both "plugin" and "command" types.
|
||||||
|
function search_entries(include, exclude)
|
||||||
|
ensure_db()
|
||||||
|
include = normalize_filter(include)
|
||||||
|
exclude = normalize_filter(exclude)
|
||||||
|
local entries = {}
|
||||||
|
for entry in pairs(entrydb) do
|
||||||
|
if (not include or matches(entry, include)) and
|
||||||
|
(not exclude or not matches(entry, exclude)) then
|
||||||
|
table.insert(entries, entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(entries, sort_by_basename)
|
||||||
|
return entries
|
||||||
|
end
|
||||||
|
|
||||||
|
-- returns a list of all commands. used by Core's autocomplete functionality.
|
||||||
|
function get_commands()
|
||||||
|
local include = {entry_type=ENTRY_TYPES.COMMAND}
|
||||||
|
return search_entries(include)
|
||||||
|
end
|
||||||
|
|
||||||
|
function is_builtin(command)
|
||||||
|
return is_entry(command) and get_entry_types(command)[ENTRY_TYPES.BUILTIN]
|
||||||
|
end
|
||||||
|
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
-- print API (outputs to console)
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- implements the 'help' builtin command
|
||||||
|
function help(entry)
|
||||||
|
ensure_db()
|
||||||
|
if not entrydb[entry] then
|
||||||
|
dfhack.printerr(('No help entry found for "%s"'):format(entry))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
print(get_entry_long_help(entry))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- prints col1text (width 21), a one space gap, and col2 (width 58)
|
||||||
|
-- if col1text is longer than 21 characters, col2text is printed starting on the
|
||||||
|
-- next line. if col2text is longer than 58 characters, it is wrapped. col2text
|
||||||
|
-- lines on lines below the col1text output are indented by one space further
|
||||||
|
-- than the col2text on the first line.
|
||||||
|
local COL1WIDTH, COL2WIDTH = 20, 58
|
||||||
|
local function print_columns(col1text, col2text)
|
||||||
|
col2text = col2text:wrap(COL2WIDTH)
|
||||||
|
local wrapped_col2 = {}
|
||||||
|
for line in col2text:gmatch('[^'..NEWLINE..']*') do
|
||||||
|
table.insert(wrapped_col2, line)
|
||||||
|
end
|
||||||
|
local col2_start_line = 1
|
||||||
|
if #col1text > COL1WIDTH then
|
||||||
|
print(col1text)
|
||||||
|
else
|
||||||
|
print(('%-'..COL1WIDTH..'s %s'):format(col1text, wrapped_col2[1]))
|
||||||
|
col2_start_line = 2
|
||||||
|
end
|
||||||
|
for i=col2_start_line,#wrapped_col2 do
|
||||||
|
print(('%'..COL1WIDTH..'s %s'):format(' ', wrapped_col2[i]))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- implements the 'tags' builtin command
|
||||||
|
function tags()
|
||||||
|
local tags = get_tags()
|
||||||
|
for _,tag in ipairs(tags) do
|
||||||
|
print_columns(tag, get_tag_data(tag).description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- prints the requested entries to the console. include and exclude filters are
|
||||||
|
-- defined as in search_entries() above.
|
||||||
|
local function list_entries(skip_tags, include, exclude)
|
||||||
|
local entries = search_entries(include, exclude)
|
||||||
|
for _,entry in ipairs(entries) do
|
||||||
|
print_columns(entry, get_entry_short_help(entry))
|
||||||
|
if not skip_tags then
|
||||||
|
local tags = set_to_sorted_list(get_entry_tags(entry))
|
||||||
|
if #tags > 0 then
|
||||||
|
print((' tags: %s'):format(table.concat(tags, ', ')))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #entries == 0 then
|
||||||
|
print('No matches.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- wraps the list_entries() API to provide a more convenient interface for Core
|
||||||
|
-- to implement the 'ls' builtin command.
|
||||||
|
-- filter_str - if a tag name, will filter by that tag. otherwise, will filter
|
||||||
|
-- as a substring
|
||||||
|
-- skip_tags - whether to skip printing tag info
|
||||||
|
-- show_dev_commands - if true, will include scripts in the modtools/ and
|
||||||
|
-- devel/ directories. otherwise those scripts will be
|
||||||
|
-- excluded
|
||||||
|
function ls(filter_str, skip_tags, show_dev_commands)
|
||||||
|
local include = {entry_type={ENTRY_TYPES.COMMAND}}
|
||||||
|
if is_tag(filter_str) then
|
||||||
|
include.tag = filter_str
|
||||||
|
else
|
||||||
|
include.str = filter_str
|
||||||
|
end
|
||||||
|
list_entries(skip_tags, include,
|
||||||
|
show_dev_commands and {} or {tag='dev'})
|
||||||
|
end
|
||||||
|
|
||||||
|
return _ENV
|
@ -0,0 +1,648 @@
|
|||||||
|
local h = require('helpdb')
|
||||||
|
|
||||||
|
local mock_plugin_db = {
|
||||||
|
hascommands={
|
||||||
|
boxbinders={description="Box your binders.", help=[[Box your binders.
|
||||||
|
This command will help you box your binders.]]},
|
||||||
|
bindboxers={description="Bind your boxers.", help=[[Bind your boxers.
|
||||||
|
This command will help you bind your boxers.]]}
|
||||||
|
},
|
||||||
|
samename={
|
||||||
|
samename={description="Samename.", help=[[Samename.
|
||||||
|
This command has the same name as its host plugin.]]}
|
||||||
|
},
|
||||||
|
nocommand={},
|
||||||
|
nodocs_hascommands={
|
||||||
|
nodoc_command={description="cpp description.", help=[[cpp description.
|
||||||
|
Rest of help.]]}
|
||||||
|
},
|
||||||
|
nodocs_samename={
|
||||||
|
nodocs_samename={description="Nodocs samename.", help=[[Nodocs samename.
|
||||||
|
This command has the same name as its host plugin but no rst docs.]]}
|
||||||
|
},
|
||||||
|
nodocs_nocommand={},
|
||||||
|
}
|
||||||
|
|
||||||
|
local mock_command_db = {}
|
||||||
|
for k,v in pairs(mock_plugin_db) do
|
||||||
|
for c,d in pairs(v) do
|
||||||
|
mock_command_db[c] = d
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local mock_script_db = {
|
||||||
|
basic=true,
|
||||||
|
['subdir/scriptname']=true,
|
||||||
|
inscript_docs=true,
|
||||||
|
inscript_short_only=true,
|
||||||
|
nodocs_script=true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local files = {
|
||||||
|
['hack/docs/docs/Tags.txt']=[[
|
||||||
|
* fort: Tools that are useful while in fort mode.
|
||||||
|
|
||||||
|
* armok: Tools that give you complete control over
|
||||||
|
an aspect of the game or provide access to
|
||||||
|
information that the game intentionally keeps
|
||||||
|
hidden.
|
||||||
|
|
||||||
|
* map: Tools that interact with the game map.
|
||||||
|
|
||||||
|
* units: Tools that interact with units.
|
||||||
|
|
||||||
|
* nomembers: Nothing is tagged with this.
|
||||||
|
]],
|
||||||
|
['hack/docs/docs/tools/hascommands.txt']=[[
|
||||||
|
hascommands
|
||||||
|
***********
|
||||||
|
|
||||||
|
**Tags:** fort | armok | units
|
||||||
|
|
||||||
|
Documented a plugin that
|
||||||
|
has commands.
|
||||||
|
|
||||||
|
**Command:** "boxbinders"
|
||||||
|
|
||||||
|
Documented boxbinders.
|
||||||
|
|
||||||
|
**Command:** "bindboxers"
|
||||||
|
|
||||||
|
Documented bindboxers.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]],
|
||||||
|
['hack/docs/docs/tools/samename.txt']=[[
|
||||||
|
samename
|
||||||
|
********
|
||||||
|
|
||||||
|
**Tags:** fort | armok
|
||||||
|
| units
|
||||||
|
|
||||||
|
**Command:** "samename"
|
||||||
|
|
||||||
|
Documented samename.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]],
|
||||||
|
['hack/docs/docs/tools/nocommand.txt']=[[
|
||||||
|
nocommand
|
||||||
|
*********
|
||||||
|
|
||||||
|
**Tags:** fort | armok |
|
||||||
|
units
|
||||||
|
|
||||||
|
Documented nocommand.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]],
|
||||||
|
['hack/docs/docs/tools/basic.txt']=[[
|
||||||
|
basic
|
||||||
|
*****
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "basic"
|
||||||
|
|
||||||
|
Documented basic.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]],
|
||||||
|
['hack/docs/docs/tools/subdir/scriptname.txt']=[[
|
||||||
|
subdir/scriptname
|
||||||
|
*****************
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "subdir/scriptname"
|
||||||
|
|
||||||
|
Documented subdir/scriptname.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]],
|
||||||
|
['scripts/scriptpath/basic.lua']=[[
|
||||||
|
-- in-file short description for basic
|
||||||
|
-- [====[
|
||||||
|
basic
|
||||||
|
=====
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "basic"
|
||||||
|
|
||||||
|
in-file basic.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]====]
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['scripts/scriptpath/subdir/scriptname.lua']=[[
|
||||||
|
-- in-file short description for scriptname
|
||||||
|
-- [====[
|
||||||
|
subdir/scriptname
|
||||||
|
=================
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "subdir/scriptname"
|
||||||
|
|
||||||
|
in-file scriptname.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]====]
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['scripts/scriptpath/inscript_docs.lua']=[[
|
||||||
|
-- in-file short description for inscript_docs
|
||||||
|
-- [====[
|
||||||
|
inscript_docs
|
||||||
|
=============
|
||||||
|
|
||||||
|
**Tags:** map | badtag
|
||||||
|
|
||||||
|
**Command:** "inscript_docs"
|
||||||
|
|
||||||
|
in-file inscript_docs.
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]====]
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['scripts/scriptpath/nodocs_script.lua']=[[
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['scripts/scriptpath/inscript_short_only.lua']=[[
|
||||||
|
-- inscript short desc.
|
||||||
|
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['other/scriptpath/basic.lua']=[[
|
||||||
|
-- in-file short description for basic (other)
|
||||||
|
-- [====[
|
||||||
|
basic
|
||||||
|
=====
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "basic"
|
||||||
|
|
||||||
|
in-file basic (other).
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]====]
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['other/scriptpath/subdir/scriptname.lua']=[[
|
||||||
|
-- in-file short description for scriptname (other)
|
||||||
|
-- [====[
|
||||||
|
subdir/scriptname
|
||||||
|
=================
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "subdir/scriptname"
|
||||||
|
|
||||||
|
in-file scriptname (other).
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]====]
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
['other/scriptpath/inscript_docs.lua']=[[
|
||||||
|
-- in-file short description for inscript_docs (other)
|
||||||
|
-- [====[
|
||||||
|
inscript_docs
|
||||||
|
=============
|
||||||
|
|
||||||
|
**Tags:** map
|
||||||
|
|
||||||
|
**Command:** "inscript_docs"
|
||||||
|
|
||||||
|
in-file inscript_docs (other).
|
||||||
|
|
||||||
|
Documented full help.
|
||||||
|
]====]
|
||||||
|
script contents
|
||||||
|
]],
|
||||||
|
}
|
||||||
|
|
||||||
|
local function mock_getCommandHelp(command)
|
||||||
|
if mock_command_db[command] then
|
||||||
|
return mock_command_db[command].help
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_listPlugins()
|
||||||
|
local list = {}
|
||||||
|
for k in pairs(mock_plugin_db) do
|
||||||
|
table.insert(list, k)
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_listCommands(plugin)
|
||||||
|
local list = {}
|
||||||
|
for k,v in pairs(mock_plugin_db) do
|
||||||
|
if k == plugin then
|
||||||
|
for c in pairs(v) do
|
||||||
|
table.insert(list, c)
|
||||||
|
end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_getCommandDescription(command)
|
||||||
|
if mock_command_db[command] then
|
||||||
|
return mock_command_db[command].description
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_getScriptPaths()
|
||||||
|
return {'scripts/scriptpath', 'other/scriptpath'}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_mtime(path)
|
||||||
|
if files[path] then return 1 end
|
||||||
|
return -1
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_listdir_recursive(script_path)
|
||||||
|
local list = {}
|
||||||
|
for s in pairs(mock_script_db) do
|
||||||
|
table.insert(list, {isdir=false, path=s..'.lua'})
|
||||||
|
end
|
||||||
|
return list
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_getTickCount()
|
||||||
|
return 100000
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mock_pcall(fn, fname)
|
||||||
|
if fn ~= io.lines then error('unexpected fn for pcall') end
|
||||||
|
if not files[fname] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true, files[fname]:gmatch('([^\n]*)\n?')
|
||||||
|
end
|
||||||
|
|
||||||
|
config.wrapper = function(test_fn)
|
||||||
|
mock.patch({
|
||||||
|
{h.dfhack.internal, 'getCommandHelp', mock_getCommandHelp},
|
||||||
|
{h.dfhack.internal, 'listPlugins', mock_listPlugins},
|
||||||
|
{h.dfhack.internal, 'listCommands', mock_listCommands},
|
||||||
|
{h.dfhack.internal, 'getCommandDescription', mock_getCommandDescription},
|
||||||
|
{h.dfhack.internal, 'getScriptPaths', mock_getScriptPaths},
|
||||||
|
{h.dfhack.filesystem, 'mtime', mock_mtime},
|
||||||
|
{h.dfhack.filesystem, 'listdir_recursive', mock_listdir_recursive},
|
||||||
|
{h.dfhack, 'getTickCount', mock_getTickCount},
|
||||||
|
{h, 'pcall', mock_pcall},
|
||||||
|
}, test_fn)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.is_entry()
|
||||||
|
expect.true_(h.is_entry('ls'),
|
||||||
|
'builtin commands get an entry')
|
||||||
|
expect.true_(h.is_entry('hascommands'),
|
||||||
|
'plugins whose names do not match their commands get an entry')
|
||||||
|
expect.true_(h.is_entry('boxbinders'),
|
||||||
|
'commands whose name does not match the host plugin get an entry')
|
||||||
|
expect.true_(h.is_entry('samename'),
|
||||||
|
'plugins that have a command with the same name get one entry')
|
||||||
|
expect.true_(h.is_entry('nocommand'),
|
||||||
|
'plugins that do not have commands get an entry')
|
||||||
|
expect.true_(h.is_entry('basic'),
|
||||||
|
'scripts in the script path get an entry')
|
||||||
|
expect.true_(h.is_entry('subdir/scriptname'),
|
||||||
|
'scripts in subdirs of a script path get an entry')
|
||||||
|
|
||||||
|
expect.true_(h.is_entry('nodocs_hascommands'),
|
||||||
|
'plugins whose names do not match their commands get an entry')
|
||||||
|
expect.true_(h.is_entry('nodoc_command'),
|
||||||
|
'commands whose name does not match the host plugin get an entry')
|
||||||
|
expect.true_(h.is_entry('nodocs_samename'),
|
||||||
|
'plugins that have a command with the same name get one entry')
|
||||||
|
expect.true_(h.is_entry('nodocs_nocommand'),
|
||||||
|
'plugins that do not have commands get an entry')
|
||||||
|
expect.true_(h.is_entry('inscript_docs'),
|
||||||
|
'scripts in the script path get an entry')
|
||||||
|
expect.true_(h.is_entry('nodocs_script'),
|
||||||
|
'scripts in the script path get an entry')
|
||||||
|
expect.true_(h.is_entry('inscript_short_only'),
|
||||||
|
'scripts in the script path get an entry')
|
||||||
|
|
||||||
|
expect.false_(h.is_entry(nil),
|
||||||
|
'nil is not an entry')
|
||||||
|
expect.false_(h.is_entry(''),
|
||||||
|
'blank is not an entry')
|
||||||
|
expect.false_(h.is_entry('notanentryname'),
|
||||||
|
'strings that are neither plugin names nor command names do not get an entry')
|
||||||
|
|
||||||
|
expect.true_(h.is_entry({'hascommands', 'boxbinders', 'nocommand'}),
|
||||||
|
'list of valid entries')
|
||||||
|
expect.false_(h.is_entry({'hascommands', 'notanentryname'}),
|
||||||
|
'list contains an element that is not an entry')
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_entry_types()
|
||||||
|
expect.table_eq({builtin=true, command=true}, h.get_entry_types('ls'))
|
||||||
|
|
||||||
|
expect.table_eq({plugin=true}, h.get_entry_types('hascommands'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('boxbinders'))
|
||||||
|
expect.table_eq({plugin=true, command=true}, h.get_entry_types('samename'))
|
||||||
|
expect.table_eq({plugin=true}, h.get_entry_types('nocommand'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('basic'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('subdir/scriptname'))
|
||||||
|
|
||||||
|
expect.table_eq({plugin=true}, h.get_entry_types('nodocs_hascommands'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('nodoc_command'))
|
||||||
|
expect.table_eq({plugin=true, command=true}, h.get_entry_types('nodocs_samename'))
|
||||||
|
expect.table_eq({plugin=true}, h.get_entry_types('nodocs_nocommand'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('nodocs_script'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('inscript_docs'))
|
||||||
|
expect.table_eq({command=true}, h.get_entry_types('inscript_short_only'))
|
||||||
|
|
||||||
|
expect.error_match('entry not found', function()
|
||||||
|
h.get_entry_types('notanentry') end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_entry_short_help()
|
||||||
|
expect.eq('No help available.', h.get_entry_short_help('ls'),
|
||||||
|
'no docs for builtin fn result in default short description')
|
||||||
|
|
||||||
|
expect.eq('Documented a plugin that has commands.', h.get_entry_short_help('hascommands'))
|
||||||
|
expect.eq('Box your binders.', h.get_entry_short_help('boxbinders'),
|
||||||
|
'should get short help from command description')
|
||||||
|
expect.eq('Samename.', h.get_entry_short_help('samename'),
|
||||||
|
'should get short help from command description')
|
||||||
|
expect.eq('Documented nocommand.', h.get_entry_short_help('nocommand'))
|
||||||
|
expect.eq('Documented basic.', h.get_entry_short_help('basic'))
|
||||||
|
expect.eq('Documented subdir/scriptname.', h.get_entry_short_help('subdir/scriptname'))
|
||||||
|
|
||||||
|
expect.eq('No help available.', h.get_entry_short_help('nodocs_hascommands'))
|
||||||
|
expect.eq('cpp description.', h.get_entry_short_help('nodoc_command'),
|
||||||
|
'should get short help from command description')
|
||||||
|
expect.eq('Nodocs samename.', h.get_entry_short_help('nodocs_samename'),
|
||||||
|
'should get short help from command description')
|
||||||
|
expect.eq('No help available.', h.get_entry_short_help('nodocs_nocommand'))
|
||||||
|
expect.eq('in-file short description for inscript_docs.',
|
||||||
|
h.get_entry_short_help('inscript_docs'),
|
||||||
|
'should get short help from header comment')
|
||||||
|
expect.eq('No help available.',
|
||||||
|
h.get_entry_short_help('nodocs_script'))
|
||||||
|
expect.eq('inscript short desc.',
|
||||||
|
h.get_entry_short_help('inscript_short_only'),
|
||||||
|
'should get short help from header comment')
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_entry_long_help()
|
||||||
|
-- long help for plugins/commands that have doc files should match the
|
||||||
|
-- contents of those files exactly
|
||||||
|
expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
|
||||||
|
h.get_entry_long_help('hascommands'))
|
||||||
|
expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
|
||||||
|
h.get_entry_long_help('boxbinders'))
|
||||||
|
expect.eq(files['hack/docs/docs/tools/samename.txt'],
|
||||||
|
h.get_entry_long_help('samename'))
|
||||||
|
expect.eq(files['hack/docs/docs/tools/nocommand.txt'],
|
||||||
|
h.get_entry_long_help('nocommand'))
|
||||||
|
expect.eq(files['hack/docs/docs/tools/basic.txt'],
|
||||||
|
h.get_entry_long_help('basic'))
|
||||||
|
expect.eq(files['hack/docs/docs/tools/subdir/scriptname.txt'],
|
||||||
|
h.get_entry_long_help('subdir/scriptname'))
|
||||||
|
|
||||||
|
-- plugins/commands that have no doc files get the default template
|
||||||
|
expect.eq([[ls
|
||||||
|
**
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]], h.get_entry_long_help('ls'))
|
||||||
|
expect.eq([[nodocs_hascommands
|
||||||
|
******************
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]], h.get_entry_long_help('nodocs_hascommands'))
|
||||||
|
expect.eq([[nodocs_hascommands
|
||||||
|
******************
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]], h.get_entry_long_help('nodoc_command'))
|
||||||
|
expect.eq([[Nodocs samename.
|
||||||
|
This command has the same name as its host plugin but no rst docs.]], h.get_entry_long_help('nodocs_samename'))
|
||||||
|
expect.eq([[nodocs_nocommand
|
||||||
|
****************
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]], h.get_entry_long_help('nodocs_nocommand'))
|
||||||
|
expect.eq([[nodocs_script
|
||||||
|
*************
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]], h.get_entry_long_help('nodocs_script'))
|
||||||
|
expect.eq([[inscript_short_only
|
||||||
|
*******************
|
||||||
|
|
||||||
|
No help available.
|
||||||
|
]], h.get_entry_long_help('inscript_short_only'))
|
||||||
|
|
||||||
|
-- scripts that have no doc files get the docs from the script lua source
|
||||||
|
expect.eq([[inscript_docs
|
||||||
|
=============
|
||||||
|
|
||||||
|
**Tags:** map | badtag
|
||||||
|
|
||||||
|
**Command:** "inscript_docs"
|
||||||
|
|
||||||
|
in-file inscript_docs.
|
||||||
|
|
||||||
|
Documented full help.]], h.get_entry_long_help('inscript_docs'))
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_entry_tags()
|
||||||
|
expect.table_eq({fort=true, armok=true, units=true},
|
||||||
|
h.get_entry_tags('hascommands'))
|
||||||
|
expect.table_eq({fort=true, armok=true, units=true},
|
||||||
|
h.get_entry_tags('samename'))
|
||||||
|
expect.table_eq({fort=true, armok=true, units=true},
|
||||||
|
h.get_entry_tags('nocommand'))
|
||||||
|
expect.table_eq({map=true}, h.get_entry_tags('basic'))
|
||||||
|
expect.table_eq({map=true}, h.get_entry_tags('inscript_docs'),
|
||||||
|
'bad tags should get filtered out')
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.is_tag()
|
||||||
|
-- see tags defined in the Tags.txt files entry above
|
||||||
|
expect.true_(h.is_tag('map'))
|
||||||
|
expect.true_(h.is_tag({'map', 'armok'}))
|
||||||
|
|
||||||
|
expect.false_(h.is_tag(nil))
|
||||||
|
expect.false_(h.is_tag(''))
|
||||||
|
expect.false_(h.is_tag('not_tag'))
|
||||||
|
expect.false_(h.is_tag({'map', 'not_tag', 'armok'}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_tags()
|
||||||
|
expect.table_eq({'armok', 'fort', 'map', 'nomembers', 'units'},
|
||||||
|
h.get_tags())
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_tag_data()
|
||||||
|
local tag_data = h.get_tag_data('armok')
|
||||||
|
table.sort(tag_data)
|
||||||
|
expect.table_eq({description='Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden.',
|
||||||
|
'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
|
||||||
|
tag_data,
|
||||||
|
'multi-line descriptions should get joined into a single line.')
|
||||||
|
|
||||||
|
tag_data = h.get_tag_data('fort')
|
||||||
|
table.sort(tag_data)
|
||||||
|
expect.table_eq({description='Tools that are useful while in fort mode.',
|
||||||
|
'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
|
||||||
|
tag_data)
|
||||||
|
|
||||||
|
tag_data = h.get_tag_data('units')
|
||||||
|
table.sort(tag_data)
|
||||||
|
expect.table_eq({description='Tools that interact with units.',
|
||||||
|
'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
|
||||||
|
tag_data)
|
||||||
|
|
||||||
|
tag_data = h.get_tag_data('map')
|
||||||
|
table.sort(tag_data)
|
||||||
|
expect.table_eq({description='Tools that interact with the game map.',
|
||||||
|
'basic', 'inscript_docs', 'subdir/scriptname'},
|
||||||
|
tag_data)
|
||||||
|
|
||||||
|
expect.table_eq({description='Nothing is tagged with this.'}, h.get_tag_data('nomembers'))
|
||||||
|
|
||||||
|
expect.error_match('tag not found',
|
||||||
|
function() h.get_tag_data('notatag') end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.search_entries()
|
||||||
|
-- all entries, in alphabetical order by last path component
|
||||||
|
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
|
||||||
|
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable',
|
||||||
|
'fpause', 'hascommands', 'help', 'hide', 'inscript_docs',
|
||||||
|
'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man',
|
||||||
|
'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand',
|
||||||
|
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
|
||||||
|
'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
|
||||||
|
'unload'}
|
||||||
|
table.sort(expected, h.sort_by_basename)
|
||||||
|
expect.table_eq(expected, h.search_entries())
|
||||||
|
expect.table_eq(expected, h.search_entries({}))
|
||||||
|
expect.table_eq(expected, h.search_entries(nil, {}))
|
||||||
|
expect.table_eq(expected, h.search_entries({}, {}))
|
||||||
|
|
||||||
|
expected = {'inscript_docs', 'subdir/scriptname'}
|
||||||
|
expect.table_eq(expected, h.search_entries({tag='map', str='script'}))
|
||||||
|
|
||||||
|
expected = {'script', 'sc-script'}
|
||||||
|
table.sort(expected, h.sort_by_basename)
|
||||||
|
expect.table_eq(expected, h.search_entries({str='script',
|
||||||
|
entry_type='builtin'}))
|
||||||
|
|
||||||
|
expected = {'inscript_docs', 'inscript_short_only','nodocs_script',
|
||||||
|
'subdir/scriptname'}
|
||||||
|
expect.table_eq(expected, h.search_entries({str='script'},
|
||||||
|
{entry_type='builtin'}))
|
||||||
|
|
||||||
|
expected = {'bindboxers', 'boxbinders'}
|
||||||
|
expect.table_eq(expected, h.search_entries({str='box'}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.get_commands()
|
||||||
|
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
|
||||||
|
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable',
|
||||||
|
'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only',
|
||||||
|
'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command',
|
||||||
|
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
|
||||||
|
'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
|
||||||
|
'unload'}
|
||||||
|
table.sort(expected, h.sort_by_basename)
|
||||||
|
expect.table_eq(expected, h.get_commands())
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.is_builtin()
|
||||||
|
expect.true_(h.is_builtin('ls'))
|
||||||
|
expect.false_(h.is_builtin('basic'))
|
||||||
|
expect.false_(h.is_builtin('notanentry'))
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.help()
|
||||||
|
expect.printerr_match('No help entry found', function() h.help('blarg') end)
|
||||||
|
|
||||||
|
local mock_print = mock.func()
|
||||||
|
mock.patch(h, 'print', mock_print, function()
|
||||||
|
h.help('nocommand')
|
||||||
|
expect.eq(1, mock_print.call_count)
|
||||||
|
expect.eq(files['hack/docs/docs/tools/nocommand.txt'],
|
||||||
|
mock_print.call_args[1][1])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.tags()
|
||||||
|
local mock_print = mock.func()
|
||||||
|
mock.patch(h, 'print', mock_print, function()
|
||||||
|
h.tags()
|
||||||
|
expect.eq(7, mock_print.call_count)
|
||||||
|
expect.eq('armok Tools that give you complete control over an aspect of the',
|
||||||
|
mock_print.call_args[1][1])
|
||||||
|
expect.eq(' game or provide access to information that the game',
|
||||||
|
mock_print.call_args[2][1])
|
||||||
|
expect.eq(' intentionally keeps hidden.',
|
||||||
|
mock_print.call_args[3][1])
|
||||||
|
expect.eq('fort Tools that are useful while in fort mode.',
|
||||||
|
mock_print.call_args[4][1])
|
||||||
|
expect.eq('map Tools that interact with the game map.',
|
||||||
|
mock_print.call_args[5][1])
|
||||||
|
expect.eq('nomembers Nothing is tagged with this.',
|
||||||
|
mock_print.call_args[6][1])
|
||||||
|
expect.eq('units Tools that interact with units.',
|
||||||
|
mock_print.call_args[7][1])
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function test.ls()
|
||||||
|
local mock_print = mock.func()
|
||||||
|
mock.patch(h, 'print', mock_print, function()
|
||||||
|
h.ls('doc') -- interpreted as a string
|
||||||
|
expect.eq(5, mock_print.call_count)
|
||||||
|
expect.eq('inscript_docs in-file short description for inscript_docs.',
|
||||||
|
mock_print.call_args[1][1])
|
||||||
|
expect.eq(' tags: map', mock_print.call_args[2][1])
|
||||||
|
expect.eq('nodoc_command cpp description.',
|
||||||
|
mock_print.call_args[3][1])
|
||||||
|
expect.eq('nodocs_samename Nodocs samename.',
|
||||||
|
mock_print.call_args[4][1])
|
||||||
|
expect.eq('nodocs_script No help available.',
|
||||||
|
mock_print.call_args[5][1])
|
||||||
|
end)
|
||||||
|
|
||||||
|
mock_print = mock.func()
|
||||||
|
mock.patch(h, 'print', mock_print, function()
|
||||||
|
h.ls('armok') -- interpreted as a tag
|
||||||
|
expect.eq(6, mock_print.call_count)
|
||||||
|
expect.eq('bindboxers Bind your boxers.',
|
||||||
|
mock_print.call_args[1][1])
|
||||||
|
expect.eq(' tags: armok, fort, units',
|
||||||
|
mock_print.call_args[2][1])
|
||||||
|
expect.eq('boxbinders Box your binders.',
|
||||||
|
mock_print.call_args[3][1])
|
||||||
|
expect.eq(' tags: armok, fort, units',
|
||||||
|
mock_print.call_args[4][1])
|
||||||
|
expect.eq('samename Samename.',
|
||||||
|
mock_print.call_args[5][1])
|
||||||
|
expect.eq(' tags: armok, fort, units',
|
||||||
|
mock_print.call_args[6][1])
|
||||||
|
end)
|
||||||
|
|
||||||
|
mock_print = mock.func()
|
||||||
|
mock.patch(h, 'print', mock_print, function()
|
||||||
|
h.ls('not a match')
|
||||||
|
expect.eq(1, mock_print.call_count)
|
||||||
|
expect.eq('No matches.', mock_print.call_args[1][1])
|
||||||
|
end)
|
||||||
|
end
|
Loading…
Reference in New Issue