mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Start update help command further
This commit is contained in:
parent
e301a1410f
commit
062aba2926
1 changed files with 292 additions and 198 deletions
|
|
@ -103,17 +103,137 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5):
|
|||
)
|
||||
|
||||
|
||||
def parse_entry_for_subcategories(entry):
|
||||
"""
|
||||
Parse a command docstring for special sub-category blocks:
|
||||
|
||||
Args:
|
||||
entry (str): A help entry to parse
|
||||
|
||||
Returns:
|
||||
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
|
||||
zero or more keys holding the subcategories. Each is itself
|
||||
a dict with a key `None` for the main text of that subcategory
|
||||
followed by any sub-sub-categories down to a max-depth of 5.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
'''
|
||||
Main topic text
|
||||
|
||||
# HELP-SUBCATEGORIES
|
||||
|
||||
## foo
|
||||
|
||||
A subcategory of the main entry, accessible as `help topic foo`
|
||||
(or using /, like `help topic/foo`)
|
||||
|
||||
## bar
|
||||
|
||||
Another subcategory, accessed as `help topic bar`
|
||||
(or `help topic/bar`)
|
||||
|
||||
### moo
|
||||
|
||||
A subcategory of bar, accessed as `help bar moo`
|
||||
(or `help bar/moo`)
|
||||
|
||||
#### dum
|
||||
|
||||
A subcategory of moo, accessed `help bar moo dum`
|
||||
(or `help bar/moo/dum`)
|
||||
|
||||
'''
|
||||
|
||||
This will result in this returned entry structure:
|
||||
::
|
||||
|
||||
{
|
||||
None: "Main topic text":
|
||||
"foo": {
|
||||
None: "main topic/foo text"
|
||||
},
|
||||
"bar": {
|
||||
None: "Main topic/bar text",
|
||||
"moo": {
|
||||
None: "topic/bar/moo text"
|
||||
"dum": {
|
||||
None: "topic/bar/moo/dum text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Apart from making
|
||||
sub-categories at the bottom of the entry.
|
||||
|
||||
This will be applied both to command docstrings and database-based help
|
||||
entries.
|
||||
|
||||
"""
|
||||
topic, *subtopics = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1)
|
||||
structure = {None: topic.strip()}
|
||||
subtopics_index = []
|
||||
|
||||
if subtopics:
|
||||
subctopics = subtopics[0]
|
||||
else:
|
||||
return structure
|
||||
|
||||
keypath = []
|
||||
current_nesting = 0
|
||||
subtopic = None
|
||||
|
||||
for part in _RE_HELP_SUBTOPIC_SPLIT.split(subtopics):
|
||||
subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part)
|
||||
if subtopic_match:
|
||||
# a new sub(-sub..) category starts.
|
||||
mdict = subtopic_match.groupdict()
|
||||
subtopic = mdict['name'].strip()
|
||||
subtopic_index.append(subtopic)
|
||||
new_nesting = len(mdict['nesting']) - 1
|
||||
nestdiff = new_nesting - current_nesting
|
||||
if nestdiff < 0:
|
||||
# jumping back up in nesting
|
||||
for _ in range(abs(nestdiff) + 1):
|
||||
try:
|
||||
keypath.pop()
|
||||
except IndexError:
|
||||
pass
|
||||
keypath.append(subtopic)
|
||||
current_nesting = new_nesting
|
||||
else:
|
||||
# an entry belonging to a subtopic - find the nested location
|
||||
dct = structure
|
||||
if not keypath and subtopic is not None:
|
||||
structure[subtopic] = part.strip()
|
||||
else:
|
||||
for key in keypath:
|
||||
if key in dct:
|
||||
dct = dct[key]
|
||||
else:
|
||||
dct[key] = {
|
||||
None: part.strip()
|
||||
}
|
||||
return structure, subtopic_index
|
||||
|
||||
|
||||
class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
View help or a list of topics
|
||||
|
||||
Usage:
|
||||
help <topic or command>
|
||||
help list
|
||||
help all
|
||||
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.
|
||||
|
||||
This will search for help on commands and other
|
||||
topics related to the game.
|
||||
"""
|
||||
|
||||
key = "help"
|
||||
|
|
@ -136,6 +256,9 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
# 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
|
||||
|
|
@ -160,165 +283,74 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
self.msg(text=(text, {"type": "help"}))
|
||||
|
||||
@staticmethod
|
||||
def parse_entry_for_subcategories(entry):
|
||||
"""
|
||||
Parse a command docstring for special sub-category blocks:
|
||||
|
||||
Args:
|
||||
entry (str): A help entry to parse
|
||||
|
||||
Returns:
|
||||
dict: A mapping that splits the entry into subcategories. This
|
||||
will always hold a key `None` for the main help entry and
|
||||
zero or more keys holding the subcategories. Each is itself
|
||||
a dict with a key `None` for the main text of that subcategory
|
||||
followed by any sub-sub-categories down to a max-depth of 5.
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
'''
|
||||
Main topic text
|
||||
|
||||
# help-subcategories
|
||||
|
||||
## foo
|
||||
|
||||
A subcategory of the main entry, accessible as `help topic foo`
|
||||
(or using /, like `help topic/foo`)
|
||||
|
||||
## bar
|
||||
|
||||
Another subcategory, accessed as `help topic bar`
|
||||
(or `help topic/bar`)
|
||||
|
||||
### moo
|
||||
|
||||
A subcategory of bar, accessed as `help bar moo`
|
||||
(or `help bar/moo`)
|
||||
|
||||
#### dum
|
||||
|
||||
A subcategory of moo, accessed `help bar moo dum`
|
||||
(or `help bar/moo/dum`)
|
||||
|
||||
'''
|
||||
|
||||
This will result in this returned entry structure:
|
||||
::
|
||||
|
||||
{
|
||||
None: "Main topic text":
|
||||
"foo": {
|
||||
None: "main topic/foo text"
|
||||
},
|
||||
"bar": {
|
||||
None: "Main topic/bar text",
|
||||
"moo": {
|
||||
None: "topic/bar/moo text"
|
||||
"dum": {
|
||||
None: "topic/bar/moo/dum text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Apart from making
|
||||
sub-categories at the bottom of the entry.
|
||||
|
||||
This will be applied both to command docstrings and database-based help
|
||||
entries.
|
||||
|
||||
"""
|
||||
topic, *subcategories = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1)
|
||||
structure = {None: topic.strip()}
|
||||
|
||||
if subcategories:
|
||||
subcategories = subcategories[0]
|
||||
else:
|
||||
return structure
|
||||
|
||||
keypath = []
|
||||
current_nesting = 0
|
||||
subtopic = None
|
||||
|
||||
for part in _RE_HELP_SUBTOPIC_SPLIT.split(subcategories):
|
||||
subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part)
|
||||
if subtopic_match:
|
||||
# a new sub(-sub..) category starts.
|
||||
mdict = subtopic_match.groupdict()
|
||||
subtopic = mdict['name'].strip()
|
||||
new_nesting = len(mdict['nesting']) - 1
|
||||
nestdiff = new_nesting - current_nesting
|
||||
if nestdiff < 0:
|
||||
# jumping back up in nesting
|
||||
for _ in range(abs(nestdiff) + 1):
|
||||
try:
|
||||
keypath.pop()
|
||||
except IndexError:
|
||||
pass
|
||||
keypath.append(subtopic)
|
||||
current_nesting = new_nesting
|
||||
else:
|
||||
# an entry belonging to a subtopic - find the nested location
|
||||
dct = structure
|
||||
if not keypath and subtopic is not None:
|
||||
structure[subtopic] = part.strip()
|
||||
else:
|
||||
for key in keypath:
|
||||
if key in dct:
|
||||
dct = dct[key]
|
||||
else:
|
||||
dct[key] = {
|
||||
None: part.strip()
|
||||
}
|
||||
return structure
|
||||
|
||||
@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 method can be overriden to customize the way a help
|
||||
entry is displayed.
|
||||
|
||||
Args:
|
||||
title (str): the title of the help entry.
|
||||
help_text (str): the text of the help entry.
|
||||
aliases (list of str or None): the list of aliases.
|
||||
suggested (list of str or None): suggested reading.
|
||||
title (str): The title of the help entry.
|
||||
help_text (str): Text of the help entry.
|
||||
aliases (list): List of help-aliases (displayed in header).
|
||||
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.
|
||||
|
||||
"""
|
||||
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 = (
|
||||
" |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n" for ali in aliases))
|
||||
if aliases else "")
|
||||
if aliases else ""
|
||||
)
|
||||
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 = (
|
||||
"\n\n|CSuggested:|n {}".format(
|
||||
fill("|C,|n ".join(f"|w{sug}|n" for sug in suggested)))
|
||||
if suggested else "")
|
||||
fill("|C,|n ".join(f"|w{sug}|n" for sug in suggested))
|
||||
)
|
||||
if suggested else ""
|
||||
)
|
||||
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
|
||||
respectively. You can override this method to return a
|
||||
custom display of the list of commands and topics.
|
||||
|
||||
"""
|
||||
category_clr = "|w"
|
||||
topic_clr = "|G"
|
||||
width = self.client_width()
|
||||
grid = []
|
||||
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()} "
|
||||
grid.append(
|
||||
|
|
@ -328,7 +360,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
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)
|
||||
|
||||
gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements)
|
||||
|
|
@ -379,111 +411,173 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
def parse(self):
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Run the dynamic help entry creator.
|
||||
"""
|
||||
query, cmdset = self.args, self.cmdset
|
||||
caller = self.caller
|
||||
|
||||
query, subtopics, cmdset = self.topic, self.subtopics, self.cmdset
|
||||
suggestion_cutoff = self.suggestion_cutoff
|
||||
suggestion_maxnum = self.suggestion_maxnum
|
||||
|
||||
if not query:
|
||||
query = "all"
|
||||
|
||||
# removing doublets in cmdset, caused by cmdhandler
|
||||
# having to allow doublet commands to manage exits etc.
|
||||
cmdset.make_unique(caller)
|
||||
|
||||
# retrieve all available commands and database topics
|
||||
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)
|
||||
]
|
||||
all_categories = list(
|
||||
set(
|
||||
[HelpCategory(cmd.help_category) for cmd in all_cmds]
|
||||
+ [HelpCategory(topic.help_category) for topic in all_topics]
|
||||
all_categories = list(set(
|
||||
[HelpCategory(cmd.help_category) for cmd in all_cmds]
|
||||
+ [HelpCategory(topic.help_category) for topic in all_db_topics]
|
||||
)
|
||||
)
|
||||
|
||||
if query in ("list", "all"):
|
||||
# we want to list all available help entries, grouped by category
|
||||
hdict_cmd = defaultdict(list)
|
||||
hdict_topic = defaultdict(list)
|
||||
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
|
||||
if not query:
|
||||
# list all available help entries, grouped by category. We want to
|
||||
# build dictionaries {category: [topic, topic, ...], ...}
|
||||
cmd_help_dict = defaultdict(list)
|
||||
db_help_dict = defaultdict(list)
|
||||
|
||||
# Filter commands that should be reached by the help
|
||||
# system, but not be displayed in the table, or be displayed differently.
|
||||
for cmd in all_cmds:
|
||||
if self.should_list_cmd(cmd, caller):
|
||||
key = (cmd.auto_help_display_key
|
||||
if hasattr(cmd, "auto_help_display_key") else cmd.key)
|
||||
hdict_cmd[cmd.help_category].append(key)
|
||||
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
|
||||
# report back
|
||||
self.msg_help(self.format_help_list(hdict_cmd, hdict_topic))
|
||||
cmd_help_dict[cmd.help_category].append(key)
|
||||
|
||||
for db_topic in all_db_topics:
|
||||
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
|
||||
|
||||
# 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
|
||||
match, suggestions = None, None
|
||||
|
||||
for match_query in [f"{query}~1", f"{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(
|
||||
match_query, entries, suggestion_maxnum=self.suggestion_maxnum
|
||||
)
|
||||
|
||||
if matches:
|
||||
match = matches[0]
|
||||
if isinstance(match, HelpCategory):
|
||||
formatted = self.format_help_list(
|
||||
{
|
||||
match.key: [
|
||||
cmd.key
|
||||
for cmd in all_cmds
|
||||
if match.key.lower() == cmd.help_category
|
||||
]
|
||||
},
|
||||
{
|
||||
match.key: [
|
||||
topic.key
|
||||
for topic in all_topics
|
||||
if match.key.lower() == topic.help_category
|
||||
]
|
||||
},
|
||||
)
|
||||
elif inherits_from(match, "evennia.commands.command.Command"):
|
||||
formatted = self.format_help_entry(
|
||||
match.key,
|
||||
match.get_help(caller, cmdset),
|
||||
aliases=match.aliases,
|
||||
suggested=suggestions[1:],
|
||||
)
|
||||
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):
|
||||
# no subtopics for categories - these are just lists of topics
|
||||
output = self.format_help_index(
|
||||
{
|
||||
match.key: [
|
||||
cmd.key
|
||||
for cmd in all_cmds
|
||||
if match.key.lower() == cmd.help_category
|
||||
]
|
||||
},
|
||||
{
|
||||
match.key: [
|
||||
topic.key
|
||||
for topic in all_topics
|
||||
if match.key.lower() == topic.help_category
|
||||
]
|
||||
},
|
||||
)
|
||||
self.msg_help(output)
|
||||
return
|
||||
|
||||
if inherits_from(match, "evennia.commands.command.Command"):
|
||||
# a command match
|
||||
topic = match.key
|
||||
help_text = match.get_help(caller, cmdset)
|
||||
aliases = match.aliases,
|
||||
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:
|
||||
formatted = self.format_help_entry(
|
||||
match.key,
|
||||
match.entrytext,
|
||||
aliases=match.aliases.all(),
|
||||
suggested=suggestions[1:],
|
||||
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]
|
||||
|
||||
self.msg_help(formatted)
|
||||
return
|
||||
|
||||
# no exact matches found. Just give suggestions.
|
||||
self.msg(
|
||||
self.format_help_entry(
|
||||
"", f"No help entry found for '{query}'", None, suggested=suggestions
|
||||
),
|
||||
options={"type": "help"},
|
||||
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):
|
||||
entry = caller.db._editing_help
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue