diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index ddbbc5269d..83839ee800 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -31,6 +31,7 @@ from evennia.utils.utils import ( iter_to_str, lazy_property, make_iter, + compress_whitespace, to_str, variable_from_module, ) @@ -221,7 +222,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): {header} |c{name}{extra_name_info}|n {desc} -{exits}{characters}{things} +{exits} +{characters} +{things} {footer} """ # on-object properties @@ -1585,7 +1588,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): char.get_display_name(looker, **kwargs) for char in characters ) - return f"\n|wCharacters:|n {character_names}" if character_names else "" + return f"|wCharacters:|n {character_names}" if character_names else "" def get_display_things(self, looker, **kwargs): """ @@ -1616,7 +1619,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): singular, plural = thing.get_numbered_name(nthings, looker, key=thingname) thing_names.append(singular if nthings == 1 else plural) thing_names = iter_to_str(thing_names) - return f"\n|wYou see:|n {thing_names}" if thing_names else "" + return f"|wYou see:|n {thing_names}" if thing_names else "" def get_display_footer(self, looker, **kwargs): """ @@ -1643,7 +1646,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): str: The final formatted output. """ - return appearance.strip() + return compress_whitespace(appearance).strip() def return_appearance(self, looker, **kwargs): """ diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 628037b418..17f6c4144a 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -53,6 +53,31 @@ class TestDedent(TestCase): self.assertEqual(expected_string, utils.dedent(input_string)) +class TestCompressWhitespace(TestCase): + def test_compress_whitespace(self): + # No text, return no text + self.assertEqual("", utils.compress_whitespace("")) + # If no whitespace is exceeded, should return the same + self.assertEqual("One line\nTwo spaces", utils.compress_whitespace("One line\nTwo spaces")) + # Extra newlines are removed + self.assertEqual("First line\nSecond line", utils.compress_whitespace("First line\n\nSecond line")) + # Extra spaces are removed + self.assertEqual("Too many spaces", utils.compress_whitespace("Too many spaces")) + # "Invisible" extra lines with whitespace are removed + self.assertEqual("First line\nSecond line", utils.compress_whitespace("First line\n \n \nSecond line")) + # Max kwargs are respected + self.assertEqual("First line\n\nSecond line", utils.compress_whitespace("First line\n\nSecond line", max_spacing=1, max_linebreaks=2)) + + def test_preserve_indents(self): + """Ensure that indentation spacing is preserved.""" + indented = """\ +Hanging Indents + they're great + let's keep them\ +""" + # since there is no doubled-up spacing besides indents, input should equal output + self.assertEqual(indented, utils.compress_whitespace(indented)) + class TestListToString(TestCase): """ Default function header from time.py: diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index b56ae3e7b2..a3e3633a94 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -472,6 +472,31 @@ def iter_to_str(iterable, sep=",", endsep=", and", addquote=False): list_to_string = iter_to_str iter_to_string = iter_to_str +re_empty = re.compile("\n\s*\n") + +def compress_whitespace(text, max_linebreaks=1, max_spacing=2): + """ + Removes extra sequential whitespace in a block of text. This will also remove any trailing + whitespace at the end. + + Args: + text (str): A string which may contain excess internal whitespace. + + Keyword args: + max_linebreaks (int): How many linebreak characters are allowed to occur in a row. + max_spacing (int): How many spaces are allowed to occur in a row. + + """ + text = text.rstrip() + # replaces any non-visible lines that are just whitespace characters with actual empty lines + # this allows the blank-line compression to eliminate them if needed + text = re_empty.sub("\n\n", text) + # replace groups of extra spaces with the maximum number of spaces + text = re.sub(f"(?<=\S) {{{max_spacing},}}", " "*max_spacing, text) + # replace groups of extra newlines with the maximum number of newlines + text = re.sub(f"\n{{{max_linebreaks},}}", "\n"*max_linebreaks, text) + return text + def wildcard_to_regexp(instring): """