diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index adb4c7d5a9..8528e75678 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -76,7 +76,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): channel/unban[/quiet] channelname[, channelname, ...] = subscribername channel/who channelname - This handles all operations on channels. + # help-subcategories + ## channel/list + + This handles all operations on channels. Note that the default operation is to + assign a nick/alias for sending to a channel. This would mean you can send + using 'foo Hello world' instead of using 'channel foo = Hello world'. Note that + aliases set when creating the channel are made available as aliases to subscribers + automatically. """ key = "channel" diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 280f03559d..b1ea35c310 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -6,6 +6,7 @@ set. The normal, database-tied help system is used for collaborative creation of other help topics such as RP help or game-world aides. """ +import re from django.conf import settings from collections import defaultdict from evennia.utils.utils import fill, dedent @@ -25,6 +26,12 @@ 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*?help[- ]subtopics\s*?$", re.I + re.M) +_RE_HELP_SUBTOPIC_SPLIT = re.compile(r"^\s*?(\#{2,6}\s*?\w+?)$", re.M) +_RE_HELP_SUBTOPIC_PARSE = re.compile( + r"^(?P\#{2,6})\s*?(?P.*?)$", re.I + re.M) + # limit symbol import for API __all__ = ("CmdHelp", "CmdSetHelp") _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH @@ -152,6 +159,122 @@ 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): """ @@ -260,6 +383,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): self.original_args = self.args.strip() self.args = self.args.strip().lower() + def func(self): """ Run the dynamic help entry creator. diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 02dbaaa898..47f1491b0d 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -23,7 +23,7 @@ from evennia import DefaultRoom, DefaultExit, ObjectDB from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.utils.test_resources import EvenniaTest from evennia.commands.default import ( - help, + help as help_module, general, system, admin, @@ -407,6 +407,9 @@ class TestGeneral(CommandTest): class TestHelp(CommandTest): + + maxDiff = None + def setUp(self): super().setUp() # we need to set up a logger here since lunr takes over the logger otherwise @@ -421,15 +424,68 @@ class TestHelp(CommandTest): logging.disable(level=logging.ERROR) def test_help(self): - self.call(help.CmdHelp(), "", "Admin", cmdset=CharacterCmdSet()) + self.call(help_module.CmdHelp(), "", "Admin", cmdset=CharacterCmdSet()) def test_set_help(self): self.call( - help.CmdSetHelp(), + help_module.CmdSetHelp(), "testhelp, General = This is a test", "Topic 'testhelp' was successfully created.", ) - self.call(help.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet()) + self.call(help_module.CmdHelp(), "testhelp", "Help for testhelp", cmdset=CharacterCmdSet()) + + def test_parse_entry(self): + """ + Test for subcategories + + """ + entry = """ + Main topic text + + # help-subtopics + + ## foo + + Foo sub-category + + ### moo + + Foo/Moo subsub-category + + #### dum + + Foo/Moo/Dum subsubsub-category + + ## bar + + Bar subcategory + + ### moo + + Bar/Moo subcategory + + """ + expected = { + None: "Main topic text", + "foo": { + None: "Foo sub-category", + "moo": { + None: "Foo/Moo subsub-category", + "dum": { + None: "Foo/Moo/Dum subsubsub-category", + } + }, + }, + "bar": { + None: "Bar subcategory", + "moo": { + None: "Bar/Moo subcategory" + } + } + } + + actual_result = help_module.CmdHelp.parse_entry_for_subcategories(entry) + self.assertEqual(expected, actual_result) class TestSystem(CommandTest):