New FileHelp system to create help entries from external files
This commit is contained in:
parent
8a7e19db16
commit
f5fd398480
10 changed files with 432 additions and 290 deletions
|
|
@ -8,229 +8,53 @@ creation of other help topics such as RP help or game-world aides.
|
|||
"""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from django.conf import settings
|
||||
from collections import defaultdict
|
||||
from evennia.utils.utils import fill, dedent
|
||||
from evennia.help.models import HelpEntry
|
||||
from evennia.utils import create, evmore
|
||||
from evennia.utils.ansi import ANSIString
|
||||
from evennia.help.filehelp import FILE_HELP_ENTRIES
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.utils import (
|
||||
class_from_module,
|
||||
inherits_from,
|
||||
format_grid, pad
|
||||
)
|
||||
from evennia.help.utils import help_search_with_index, parse_entry_for_subcategories
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
HELP_MORE = settings.HELP_MORE
|
||||
CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
|
||||
|
||||
_RE_HELP_SUBTOPICS_START = re.compile(
|
||||
r"^\s*?#\s*?subtopics\s*?$", re.I + re.M)
|
||||
_RE_HELP_SUBTOPIC_SPLIT = re.compile(r"^\s*?(\#{2,6}\s*?\w+?[a-z0-9 \-\?!,\.]*?)$", re.M + re.I)
|
||||
_RE_HELP_SUBTOPIC_PARSE = re.compile(
|
||||
r"^(?P<nesting>\#{2,6})\s*?(?P<name>.*?)$", re.I + re.M)
|
||||
MAX_SUBTOPIC_NESTING = 5
|
||||
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdHelp", "CmdSetHelp")
|
||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
_SEP = "|C" + "-" * _DEFAULT_WIDTH + "|n"
|
||||
|
||||
_LUNR = None
|
||||
_LUNR_EXCEPTION = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class HelpCategory:
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
"""
|
||||
Mock 'help entry' to search categories with the same code.
|
||||
|
||||
"""
|
||||
key: str
|
||||
|
||||
@property
|
||||
def search_index_entry(self):
|
||||
return {
|
||||
"key": str(self),
|
||||
"key": self.key,
|
||||
"aliases": "",
|
||||
"category": self.key,
|
||||
"tags": "",
|
||||
"text": "",
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"Category: {self.key}"
|
||||
|
||||
def __eq__(self, other):
|
||||
return str(self).lower() == str(other).lower()
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
|
||||
def help_search_with_index(query, candidate_entries, suggestion_maxnum=5):
|
||||
"""
|
||||
Lunr-powered fast index search and suggestion wrapper
|
||||
"""
|
||||
global _LUNR, _LUNR_EXCEPTION
|
||||
if not _LUNR:
|
||||
# we have to delay-load lunr because it messes with logging if it's imported
|
||||
# before twisted's logging has been set up
|
||||
from lunr import lunr as _LUNR
|
||||
from lunr.exceptions import QueryParseError as _LUNR_EXCEPTION
|
||||
|
||||
indx = [cnd.search_index_entry for cnd in candidate_entries]
|
||||
mapping = {indx[ix]["key"]: cand for ix, cand in enumerate(candidate_entries)}
|
||||
|
||||
search_index = _LUNR(
|
||||
ref="key",
|
||||
fields=[
|
||||
{"field_name": "key", "boost": 10},
|
||||
{"field_name": "aliases", "boost": 9},
|
||||
{"field_name": "category", "boost": 8},
|
||||
{"field_name": "tags", "boost": 5},
|
||||
{"field_name": "text", "boost": 1},
|
||||
],
|
||||
documents=indx,
|
||||
)
|
||||
try:
|
||||
matches = search_index.search(query)[:suggestion_maxnum]
|
||||
except _LUNR_EXCEPTION:
|
||||
# this is a user-input problem
|
||||
matches = []
|
||||
|
||||
# matches (objs), suggestions (strs)
|
||||
return (
|
||||
[mapping[match["ref"]] for match in matches],
|
||||
[str(match["ref"]) for match in matches], # + f" (score {match['score']})") # good debug
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
|
||||
# SUBTOPICS
|
||||
|
||||
## 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()}
|
||||
|
||||
if subtopics:
|
||||
subtopics = subtopics[0]
|
||||
else:
|
||||
return structure
|
||||
|
||||
keypath = []
|
||||
current_nesting = 0
|
||||
subtopic = None
|
||||
|
||||
# from evennia import set_trace;set_trace()
|
||||
for part in _RE_HELP_SUBTOPIC_SPLIT.split(subtopics.strip()):
|
||||
|
||||
subtopic_match = _RE_HELP_SUBTOPIC_PARSE.match(part.strip())
|
||||
if subtopic_match:
|
||||
# a new sub(-sub..) category starts.
|
||||
mdict = subtopic_match.groupdict()
|
||||
subtopic = mdict['name'].lower().strip()
|
||||
new_nesting = len(mdict['nesting']) - 1
|
||||
|
||||
if new_nesting > MAX_SUBTOPIC_NESTING:
|
||||
raise RuntimeError(
|
||||
f"Can have max {MAX_SUBTOPIC_NESTING} levels of nested help subtopics.")
|
||||
|
||||
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
|
||||
elif nestdiff == 0:
|
||||
# don't add a deeper nesting but replace the current
|
||||
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] = dedent(part.strip())
|
||||
else:
|
||||
for key in keypath:
|
||||
if key in dct:
|
||||
dct = dct[key]
|
||||
else:
|
||||
dct[key] = {
|
||||
None: dedent(part.strip())
|
||||
}
|
||||
return structure
|
||||
return hash(id(self))
|
||||
|
||||
|
||||
class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -329,7 +153,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
aliases = ''
|
||||
|
||||
help_text = f"\n\n{dedent(' ' + help_text.strip())}\n" if help_text else ""
|
||||
help_text = "\n\n" + dedent(help_text.strip(), indent=0) + "\n" if help_text else ""
|
||||
|
||||
if subtopics:
|
||||
subtopics = (
|
||||
|
|
@ -503,11 +327,17 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
# having to allow doublet commands to manage exits etc.
|
||||
cmdset.make_unique(caller)
|
||||
|
||||
# retrieve all available commands and database topics
|
||||
# retrieve all available commands and database / file-help topics
|
||||
all_cmds = [cmd for cmd in cmdset if self.check_show_help(cmd, caller)]
|
||||
all_db_topics = [
|
||||
topic for topic in HelpEntry.objects.all() if topic.access(caller, "view", default=True)
|
||||
]
|
||||
|
||||
# we group the file-help topics with the db ones, giving the db ones priority
|
||||
file_help_topics = FILE_HELP_ENTRIES.all(return_dict=True)
|
||||
db_topics = {
|
||||
topic.key.lower().strip(): topic for topic in HelpEntry.objects.all()
|
||||
if topic.access(caller, "view", default=True)
|
||||
}
|
||||
all_db_topics = list({**file_help_topics, **db_topics}.values())
|
||||
|
||||
all_categories = list(set(
|
||||
[HelpCategory(cmd.help_category) for cmd in all_cmds]
|
||||
+ [HelpCategory(topic.help_category) for topic in all_db_topics]
|
||||
|
|
@ -539,7 +369,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
# 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] + all_db_topics + all_categories
|
||||
match, suggestions = None, None
|
||||
|
||||
for match_query in [f"{query}~1", f"{query}*"]:
|
||||
|
|
@ -590,10 +420,10 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
|
|||
aliases = match.aliases
|
||||
suggested = suggestions[1:]
|
||||
else:
|
||||
# a database match
|
||||
# a database (or file-help) match
|
||||
topic = match.key
|
||||
help_text = match.entrytext
|
||||
aliases = match.aliases.all()
|
||||
aliases = match.aliases if isinstance(match.aliases, list) else match.aliases.all()
|
||||
suggested = suggestions[1:]
|
||||
|
||||
# parse for subtopics. The subtopic_map is a dict with the current topic/subtopic
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue