Start update help command further

This commit is contained in:
Griatch 2021-04-29 17:14:18 +02:00
parent e301a1410f
commit 062aba2926

View file

@ -103,63 +103,6 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5):
) )
class CmdHelp(COMMAND_DEFAULT_CLASS):
"""
View help or a list of topics
Usage:
help <topic or command>
help list
help all
This will search for help on commands and other
topics related to the game.
"""
key = "help"
aliases = ["?"]
locks = "cmd:all()"
arg_regex = r"\s|$"
# this is a special cmdhandler flag that makes the cmdhandler also pack
# the current cmdset with the call to self.func().
return_cmdset = True
# Help messages are wrapped in an EvMore call (unless using the webclient
# with separate help popups) If you want to avoid this, simply add
# 'HELP_MORE = False' in your settings/conf/settings.py
help_more = HELP_MORE
# suggestion cutoff, between 0 and 1 (1 => perfect match)
suggestion_cutoff = 0.6
# number of suggestions (set to 0 to remove suggestions from help)
suggestion_maxnum = 5
def msg_help(self, text):
"""
messages text to the caller, adding an extra oob argument to indicate
that this is a help command result and could be rendered in a separate
help window
"""
if type(self).help_more:
usemore = True
if self.session and self.session.protocol_key in ("websocket", "ajax/comet",):
try:
options = self.account.db._saved_webclient_options
if options and options["helppopup"]:
usemore = False
except KeyError:
pass
if usemore:
evmore.msg(self.caller, text, session=self.session)
return
self.msg(text=(text, {"type": "help"}))
@staticmethod
def parse_entry_for_subcategories(entry): def parse_entry_for_subcategories(entry):
""" """
Parse a command docstring for special sub-category blocks: Parse a command docstring for special sub-category blocks:
@ -168,7 +111,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
entry (str): A help entry to parse entry (str): A help entry to parse
Returns: Returns:
dict: A mapping that splits the entry into subcategories. This dict: The dict is a mapping that splits the entry into subcategories. This
will always hold a key `None` for the main help entry and will always hold a key `None` for the main help entry and
zero or more keys holding the subcategories. Each is itself zero or more keys holding the subcategories. Each is itself
a dict with a key `None` for the main text of that subcategory a dict with a key `None` for the main text of that subcategory
@ -180,7 +123,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
''' '''
Main topic text Main topic text
# help-subcategories # HELP-SUBCATEGORIES
## foo ## foo
@ -231,11 +174,12 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
entries. entries.
""" """
topic, *subcategories = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1) topic, *subtopics = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1)
structure = {None: topic.strip()} structure = {None: topic.strip()}
subtopics_index = []
if subcategories: if subtopics:
subcategories = subcategories[0] subctopics = subtopics[0]
else: else:
return structure return structure
@ -243,12 +187,13 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
current_nesting = 0 current_nesting = 0
subtopic = None subtopic = None
for part in _RE_HELP_SUBTOPIC_SPLIT.split(subcategories): for part in _RE_HELP_SUBTOPIC_SPLIT.split(subtopics):
subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part) subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part)
if subtopic_match: if subtopic_match:
# a new sub(-sub..) category starts. # a new sub(-sub..) category starts.
mdict = subtopic_match.groupdict() mdict = subtopic_match.groupdict()
subtopic = mdict['name'].strip() subtopic = mdict['name'].strip()
subtopic_index.append(subtopic)
new_nesting = len(mdict['nesting']) - 1 new_nesting = len(mdict['nesting']) - 1
nestdiff = new_nesting - current_nesting nestdiff = new_nesting - current_nesting
if nestdiff < 0: if nestdiff < 0:
@ -273,52 +218,139 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
dct[key] = { dct[key] = {
None: part.strip() None: part.strip()
} }
return structure return structure, subtopic_index
class CmdHelp(COMMAND_DEFAULT_CLASS):
"""
View help or a list of topics
Usage:
help
help <topic, command or category>
help <topic> / <subtopic>
help <topic> / <subtopic> / <subsubtopic> ...
Use the help command alone to see an index of all help topics, organized by
category. Some long topics may offer additional sub-topics.
"""
key = "help"
aliases = ["?"]
locks = "cmd:all()"
arg_regex = r"\s|$"
# this is a special cmdhandler flag that makes the cmdhandler also pack
# the current cmdset with the call to self.func().
return_cmdset = True
# Help messages are wrapped in an EvMore call (unless using the webclient
# with separate help popups) If you want to avoid this, simply add
# 'HELP_MORE = False' in your settings/conf/settings.py
help_more = HELP_MORE
# suggestion cutoff, between 0 and 1 (1 => perfect match)
suggestion_cutoff = 0.6
# number of suggestions (set to 0 to remove suggestions from help)
suggestion_maxnum = 5
# separator between subtopics:
subtopic_separator_char = r"/"
def msg_help(self, text):
"""
messages text to the caller, adding an extra oob argument to indicate
that this is a help command result and could be rendered in a separate
help window
"""
if type(self).help_more:
usemore = True
if self.session and self.session.protocol_key in ("websocket", "ajax/comet",):
try:
options = self.account.db._saved_webclient_options
if options and options["helppopup"]:
usemore = False
except KeyError:
pass
if usemore:
evmore.msg(self.caller, text, session=self.session)
return
self.msg(text=(text, {"type": "help"}))
@staticmethod @staticmethod
def format_help_entry(title, help_text, aliases=None, suggested=None): def format_help_entry(topic="", help_text="", aliases=None, suggested=None,
subtopics=None):
""" """
This visually formats the help entry. This visually formats the help entry.
This method can be overriden to customize the way a help This method can be overriden to customize the way a help
entry is displayed. entry is displayed.
Args: Args:
title (str): the title of the help entry. title (str): The title of the help entry.
help_text (str): the text of the help entry. help_text (str): Text of the help entry.
aliases (list of str or None): the list of aliases. aliases (list): List of help-aliases (displayed in header).
suggested (list of str or None): suggested reading. suggested (list): Strings suggested reading (based on title).
subtopics (list): A list of strings - the subcategories to this entry.
Returns the formatted string, ready to be sent. Returns the formatted string, ready to be sent.
""" """
start = f"{_SEP}\n" start = f"{_SEP}\n"
title = f"|CHelp for |w{title}|n" if title else "" title = f"|CHelp for |w{topic}|n" if topic else ""
aliases = ( aliases = (
" |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n" for ali in aliases)) " |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n" for ali in aliases))
if aliases else "") if aliases else ""
)
help_text = ( help_text = (
f"\n{dedent(help_text.rstrip())}"if help_text else "") f"\n{dedent(help_text.rstrip())}" if help_text else ""
)
subtopics = (
"\nSubtopics (read with |whelp {} / subtopic): {}".format(
topic, "|C,|n ".join(f"|w{subtop}|n" for subtop in subtopics)
if subtopics else ""
)
)
suggested = ( suggested = (
"\n\n|CSuggested:|n {}".format( "\n\n|CSuggested:|n {}".format(
fill("|C,|n ".join(f"|w{sug}|n" for sug in suggested))) fill("|C,|n ".join(f"|w{sug}|n" for sug in suggested))
if suggested else "") )
if suggested else ""
)
end = f"\n{_SEP}" end = f"\n{_SEP}"
return "".join((start, title, aliases, help_text, suggested, end)) return "".join((start, title, aliases, help_text, subtopics, suggested, end))
def format_help_list(self, hdict_cmds, hdict_db): def format_help_index(self, cmd_help_dict=None, db_help_dict=None):
""" """
Output a category-ordered list. The input are the Output a category-ordered g for displaying the main help, grouped by
category.
Args:
cmd_help_dict (dict): A dict `{"category": [topic, topic, ...]}` for
command-based help.
db_help_dict (dict): A dict `{"category": [topic, topic], ...]}` for
database-based help.
Returns:
str: The help index organized into a grid.
The input are the
pre-loaded help files for commands and database-helpfiles pre-loaded help files for commands and database-helpfiles
respectively. You can override this method to return a respectively. You can override this method to return a
custom display of the list of commands and topics. custom display of the list of commands and topics.
""" """
category_clr = "|w" category_clr = "|w"
topic_clr = "|G" topic_clr = "|G"
width = self.client_width() width = self.client_width()
grid = [] grid = []
verbatim_elements = [] verbatim_elements = []
for category in sorted(set(list(hdict_cmds.keys()) + list(hdict_db.keys()))): for category in sorted(set(list(cmd_help_dict.keys()) + list(db_help_dict.keys()))):
category_str = f"-- {category.title()} " category_str = f"-- {category.title()} "
grid.append( grid.append(
@ -328,7 +360,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
) )
verbatim_elements.append(len(grid) - 1) verbatim_elements.append(len(grid) - 1)
entries = sorted(set(hdict_cmds.get(category, []) + hdict_db.get(category, []))) entries = sorted(set(cmd_help_dict.get(category, []) + db_help_dict.get(category, [])))
grid.extend(entries) grid.extend(entries)
gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements) gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements)
@ -379,70 +411,103 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
def parse(self): def parse(self):
""" """
input is a string containing the command or topic to match. input is a string containing the command or topic to match.
"""
self.original_args = self.args.strip()
self.args = self.args.strip().lower()
The allowed syntax is
::
help <topic>[/<subtopic>[/<subtopic>[/...]]]
The database/command query is always for `<topic>`, and any subtopics
is then parsed from there. If a `<topic>` has spaces in it, it is
always matched before assuming the space begins a subtopic.
"""
# parse the query
if self.args:
self.subtopics = [part.strip().lower()
for part in self.args.split(self.subtopic_separator_char)]
self.topic = self.subtopics.pop(0)
else:
self.topic = ""
self.subtopics = []
def func(self): def func(self):
""" """
Run the dynamic help entry creator. Run the dynamic help entry creator.
""" """
query, cmdset = self.args, self.cmdset
caller = self.caller caller = self.caller
query, subtopics, cmdset = self.topic, self.subtopics, self.cmdset
suggestion_cutoff = self.suggestion_cutoff suggestion_cutoff = self.suggestion_cutoff
suggestion_maxnum = self.suggestion_maxnum suggestion_maxnum = self.suggestion_maxnum
if not query:
query = "all"
# removing doublets in cmdset, caused by cmdhandler # removing doublets in cmdset, caused by cmdhandler
# having to allow doublet commands to manage exits etc. # having to allow doublet commands to manage exits etc.
cmdset.make_unique(caller) cmdset.make_unique(caller)
# retrieve all available commands and database topics # retrieve all available commands and database topics
all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)] all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)]
all_topics = [ all_db_topics = [
topic for topic in HelpEntry.objects.all() if topic.access(caller, "view", default=True) topic for topic in HelpEntry.objects.all() if topic.access(caller, "view", default=True)
] ]
all_categories = list( all_categories = list(set(
set(
[HelpCategory(cmd.help_category) for cmd in all_cmds] [HelpCategory(cmd.help_category) for cmd in all_cmds]
+ [HelpCategory(topic.help_category) for topic in all_topics] + [HelpCategory(topic.help_category) for topic in all_db_topics]
) )
) )
if query in ("list", "all"): if not query:
# we want to list all available help entries, grouped by category # list all available help entries, grouped by category. We want to
hdict_cmd = defaultdict(list) # build dictionaries {category: [topic, topic, ...], ...}
hdict_topic = defaultdict(list) cmd_help_dict = defaultdict(list)
# create the dictionaries {category:[topic, topic ...]} required by format_help_list db_help_dict = defaultdict(list)
# Filter commands that should be reached by the help # Filter commands that should be reached by the help
# system, but not be displayed in the table, or be displayed differently. # system, but not be displayed in the table, or be displayed differently.
for cmd in all_cmds: for cmd in all_cmds:
if self.should_list_cmd(cmd, caller): if self.should_list_cmd(cmd, caller):
key = (cmd.auto_help_display_key key = (cmd.auto_help_display_key
if hasattr(cmd, "auto_help_display_key") else cmd.key) if hasattr(cmd, "auto_help_display_key") else cmd.key)
hdict_cmd[cmd.help_category].append(key) cmd_help_dict[cmd.help_category].append(key)
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
# report back for db_topic in all_db_topics:
self.msg_help(self.format_help_list(hdict_cmd, hdict_topic)) db_help_dict[db_topic.help_category].append(db_topic.key)
output = self.format_help_index(cmd_help_dict, db_help_dict)
self.msg_help(output)
return return
# Try to access a particular help entry or category # We have a query - try to find a specific topic/category using the
# Lunr search engine
# all available options
entries = [cmd for cmd in all_cmds if cmd] + list(HelpEntry.objects.all()) + all_categories entries = [cmd for cmd in all_cmds if cmd] + list(HelpEntry.objects.all()) + all_categories
match, suggestions = None, None
for match_query in [f"{query}~1", f"{query}*"]: for match_query in [f"{query}~1", f"{query}*"]:
# We first do an exact word-match followed by a start-by query # We first do an exact word-match followed by a start-by query
# the return of this will either be a HelpCategory, a Command or a HelpEntry.
matches, suggestions = help_search_with_index( matches, suggestions = help_search_with_index(
match_query, entries, suggestion_maxnum=self.suggestion_maxnum match_query, entries, suggestion_maxnum=self.suggestion_maxnum
) )
if matches: if matches:
match = matches[0] match = matches[0]
break
if not match:
# no exact matches found. Just give suggestions.
output = self.format_help_entry(
topic="",
help_text=f"No help entry found for '{query}'",
suggested=suggestions
)
self.msg_help(output)
return
if isinstance(match, HelpCategory): if isinstance(match, HelpCategory):
formatted = self.format_help_list( # no subtopics for categories - these are just lists of topics
output = self.format_help_index(
{ {
match.key: [ match.key: [
cmd.key cmd.key
@ -458,31 +523,60 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
] ]
}, },
) )
elif inherits_from(match, "evennia.commands.command.Command"): self.msg_help(output)
formatted = self.format_help_entry(
match.key,
match.get_help(caller, cmdset),
aliases=match.aliases,
suggested=suggestions[1:],
)
else:
formatted = self.format_help_entry(
match.key,
match.entrytext,
aliases=match.aliases.all(),
suggested=suggestions[1:],
)
self.msg_help(formatted)
return return
# no exact matches found. Just give suggestions. if inherits_from(match, "evennia.commands.command.Command"):
self.msg( # a command match
self.format_help_entry( topic = match.key
"", f"No help entry found for '{query}'", None, suggested=suggestions help_text = match.get_help(caller, cmdset)
), aliases = match.aliases,
options={"type": "help"}, suggested=suggestions[1:]
else:
# a database match
topic = match.key
help_text = match.entrytext
aliases = match.aliases.all()
suggested = suggestions[1:]
# parse for subtopics. The subtopic_map is a dict with the current topic/subtopic
# text is stored under a `None` key and all other keys are subtopic titles pointing
# to nested dicts.
subtopic_map = parse_entry_for_subcategories(help_text)
help_text = subtopic_map[None]
subtopic_index = [subtopic for subtopic in subtopic_map if subtopic is not None]
if subtopics:
# if we asked for subtopics, parse the found topic_text to see if any match.
# the subtopics is a list describing the path through the subtopic_map.
for subtopic_query in subtopics:
subtopic_query_lower = subtopic_query.lower()
checked_topic = topic + f" / {subtopic_query.lower().capitalize()}"
subtopic_index = [subtopic for subtopic in subtopic_map if subtopic is not None]
if subtopic_query_lower() in subtopic_index:
# keep stepping down into the tree
subtopic_map = subtopic_map.pop(subtopic_query)
topic = checked_topic
else:
output = self.format_help_entry(
topic=topic,
help_text=f"No help entry found for '{checked_topic}'",
subtopics=subtopic_index
) )
self.msg_help(output)
return
# we reached the bottom of the topic tree
help_text = subtopic_map[None]
output = self.format_help_entry(
topic=topic,
help_text=help_text,
aliases=aliases if not subtopics else None,
subtopics=subtopic_index
)
self.msg_help(output)
def _loadhelp(caller): def _loadhelp(caller):